Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add S3 support #1008

Open
wants to merge 82 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 68 commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
2af5d8d
feat: Add S3 support
pavelzw Jan 2, 2025
72d763a
fmt
pavelzw Jan 2, 2025
973b183
Add unit tests + empty integration test
delsner Jan 2, 2025
58c2f10
Implement integration test
delsner Jan 2, 2025
aa3c921
Add s3 to default features
delsner Jan 2, 2025
7820c5c
Fix unwraps
delsner Jan 2, 2025
1bd0889
Add middleware
delsner Jan 2, 2025
18fee26
fixes
pavelzw Jan 2, 2025
56a1d5d
.
pavelzw Jan 2, 2025
592139a
revert bad formatting
pavelzw Jan 2, 2025
f64d091
fix
pavelzw Jan 2, 2025
22205e8
fix
pavelzw Jan 2, 2025
c5052b3
fix
pavelzw Jan 2, 2025
c3c5689
install minio-server and client
pavelzw Jan 2, 2025
bdb4f68
only install when testing
pavelzw Jan 2, 2025
0d79272
Add s3 to allowed url schemes
delsner Jan 3, 2025
604c0c8
Fix leading slash issue
delsner Jan 3, 2025
97421ee
add minio-client
pavelzw Jan 3, 2025
cbf2cc3
different api
pavelzw Jan 3, 2025
4d8dfc2
adjust py-rattler
pavelzw Jan 3, 2025
c59a3f4
Make client creation public
delsner Jan 3, 2025
9838334
Add todo to infer path style
delsner Jan 3, 2025
f8fde26
Fix integration test and authstorage lookup
delsner Jan 3, 2025
e095b8e
make enum instead of option
pavelzw Jan 3, 2025
1b1fb87
fix
pavelzw Jan 3, 2025
8cb2cf8
wip
pavelzw Jan 3, 2025
d8b2cc8
.
pavelzw Jan 3, 2025
3563994
fix python
pavelzw Jan 3, 2025
259a744
fix
pavelzw Jan 3, 2025
d7bd54e
fmt
pavelzw Jan 3, 2025
2781798
Add support for public buckets and fix tests
delsner Jan 5, 2025
93eb440
Apply suggestions from code review
delsner Jan 5, 2025
521beb1
Add xref for localhost heuristic
delsner Jan 5, 2025
bba66f4
Allow FromAWS for public bucket
delsner Jan 5, 2025
cd7cdaf
empty commit
pavelzw Jan 5, 2025
87b3424
Merge branch 'main' into s3
pavelzw Jan 5, 2025
89758bc
fix
pavelzw Jan 5, 2025
e1ca81d
fix
pavelzw Jan 5, 2025
382474e
fix tests, add custom + public integration test
pavelzw Jan 5, 2025
b97a0d3
Fix some tests
delsner Jan 10, 2025
0042983
Merge branch 'main' into s3
pavelzw Jan 10, 2025
d319fcb
add debug statement
pavelzw Jan 5, 2025
95225a2
fix clippy
pavelzw Jan 10, 2025
8537405
fmt
pavelzw Jan 10, 2025
a616161
Properly run minio as background process
delsner Jan 12, 2025
21d2bef
Remove nextest config
delsner Jan 12, 2025
d206864
Merge branch 'main' into s3
pavelzw Jan 12, 2025
d908785
Remove killing of minio server
delsner Jan 12, 2025
0d941e9
empty commit
pavelzw Jan 12, 2025
9a581e6
Cleanup Cargo.toml
delsner Jan 12, 2025
e213be0
Add cli args conditions
delsner Jan 12, 2025
6304919
Update crates/rattler/src/cli/auth.rs
pavelzw Jan 12, 2025
5600720
Adjust error messages
delsner Jan 12, 2025
d17bc89
Fix windows test
delsner Jan 12, 2025
87bb481
empty commit
pavelzw Jan 12, 2025
f92386e
Debug windows test
delsner Jan 12, 2025
642dc81
empty commit
pavelzw Jan 12, 2025
b671bde
More debug output on windows
delsner Jan 12, 2025
0193055
Fix
delsner Jan 12, 2025
8434a39
Test if executables are available in PATH
delsner Jan 12, 2025
c71f413
Test with mc directly again
delsner Jan 12, 2025
150e3ad
Debug minio windows
delsner Jan 13, 2025
c06489e
Debug minio server
delsner Jan 13, 2025
ba42437
Add cloudflare R2 integration test
pavelzw Jan 13, 2025
90ce8c2
fix
pavelzw Jan 13, 2025
120c762
fix
pavelzw Jan 14, 2025
efa6625
wip auth storage
pavelzw Jan 14, 2025
5348436
fix error, add sso feature
pavelzw Jan 16, 2025
321a16d
fix
pavelzw Jan 16, 2025
fb7352d
fix
pavelzw Jan 16, 2025
a365c8c
fmt
pavelzw Jan 16, 2025
ef60b64
whatever, don't annoy me
pavelzw Jan 16, 2025
369c181
empty commit
pavelzw Jan 16, 2025
dbf0db6
Merge remote-tracking branch 'upstream/main' into s3
pavelzw Jan 17, 2025
73bdffb
fix after merge
pavelzw Jan 17, 2025
87f8858
fmt
pavelzw Jan 17, 2025
e1fbc82
does pwsh work?
pavelzw Jan 17, 2025
ffae735
?
pavelzw Jan 17, 2025
ca7e4d7
?
pavelzw Jan 17, 2025
73eed8a
?
pavelzw Jan 17, 2025
11f07b0
fix
pavelzw Jan 17, 2025
90fd779
Merge branch 'main' into s3
pavelzw Jan 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 37 additions & 13 deletions .github/workflows/rust-compile.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
on:
push:
branches: [ main ]
branches: [main]
pull_request:
paths:
# When we change pyproject.toml, we want to ensure that the maturin builds still work
Expand All @@ -22,7 +22,7 @@ env:
RUST_BACKTRACE: 1
RUSTFLAGS: "-D warnings"
CARGO_TERM_COLOR: always
DEFAULT_FEATURES: indicatif,tokio,serde,wasm,reqwest,sparse,gateway,resolvo,libsolv_c
DEFAULT_FEATURES: indicatif,tokio,serde,wasm,reqwest,sparse,gateway,resolvo,libsolv_c,s3

jobs:
check-rustdoc-links:
Expand Down Expand Up @@ -54,28 +54,27 @@ jobs:
build:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
needs: [ format_and_lint ]
needs: [format_and_lint]
strategy:
fail-fast: false
matrix:
include:
- { name: "Linux-x86_64", target: x86_64-unknown-linux-musl, os: ubuntu-22.04 }
- { name: "Linux-aarch64", target: aarch64-unknown-linux-musl, os: ubuntu-latest, skip-tests: true }
- { name: "Linux-arm", target: arm-unknown-linux-musleabi, os: ubuntu-latest, use-cross: true, skip-tests: true }

# - { name: "Linux-mips", target: mips-unknown-linux-musl, os: ubuntu-latest, use-cross: true, skip-tests: true }
# - { name: "Linux-mipsel", target: mipsel-unknown-linux-musl, os: ubuntu-latest, use-cross: true, skip-tests: true }
# - { name: "Linux-mips64", target: mips64-unknown-linux-muslabi64, os: ubuntu-latest, use-cross: true, skip-tests: true }
# - { name: "Linux-mips64el", target: mips64el-unknown-linux-muslabi64, os: ubuntu-latest, use-cross: true, skip-tests: true }

# - { name: "Linux-powerpc", target: powerpc-unknown-linux-gnu, os: ubuntu-latest, use-cross: true, skip-tests: true }
# - { name: "Linux-powerpc", target: powerpc-unknown-linux-gnu, os: ubuntu-latest, use-cross: true, skip-tests: true }
baszalmstra marked this conversation as resolved.
Show resolved Hide resolved
- { name: "Linux-powerpc64", target: powerpc64-unknown-linux-gnu, os: ubuntu-latest, use-cross: true, skip-tests: true }
- { name: "Linux-powerpc64le", target: powerpc64le-unknown-linux-gnu, os: ubuntu-latest, use-cross: true, skip-tests: true }

- { name: "Linux-s390x", target: s390x-unknown-linux-gnu, os: ubuntu-latest, use-cross: true, skip-tests: true }

- { name: "macOS-x86_64", target: x86_64-apple-darwin, os: macOS-latest }
- { name: "macOS-aarch64", target: aarch64-apple-darwin, os: macOS-latest, skip-tests: true }
baszalmstra marked this conversation as resolved.
Show resolved Hide resolved
- { name: "macOS-aarch64", target: aarch64-apple-darwin, os: macOS-latest }

- { name: "Windows-x86_64", target: x86_64-pc-windows-msvc, os: windows-latest }
- { name: "Windows-aarch64", target: aarch64-pc-windows-msvc, os: windows-latest, skip-tests: true }
Expand Down Expand Up @@ -121,11 +120,11 @@ jobs:

- name: Build
run: >
cargo build
--all-targets
cargo build
--all-targets
--features ${{ env.DEFAULT_FEATURES }}
--target ${{ matrix.target }}
${{ steps.build-options.outputs.CARGO_BUILD_OPTIONS}}
${{ steps.build-options.outputs.CARGO_BUILD_OPTIONS }}

- name: Disable testing the tools crate if cross compiling
id: test-options
Expand All @@ -139,17 +138,42 @@ jobs:
with:
tool: cargo-nextest

- name: Set up pixi
if: ${{ !matrix.skip-tests }}
uses: prefix-dev/setup-pixi@v0.8.1
with:
run-install: false
# on windows, the minio server doesn't get shutdown properly so there is still a lock on this file
post-cleanup: false

- name: Install minio for integration tests
if: ${{ !matrix.skip-tests }}
run: |
pixi global install minio-client
pavelzw marked this conversation as resolved.
Show resolved Hide resolved
pixi global install minio-server

- name: Start minio server
if: ${{ !matrix.skip-tests }}
run: |
minio server --help
mkdir -p ${{ runner.temp }}/minio-data
minio server --address 127.0.0.1:9000 ${{ runner.temp }}/minio-data &
sleep 5
curl -I http://localhost:9000/minio/health/live
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of always waiting 5 seconds, could we do something like:

# Wait until the URL is reachable
while ! curl -s --head --fail "$URL" > /dev/null; do
    echo "Waiting for the server to be ready..."
    sleep 1
done

?

Copy link
Contributor Author

@pavelzw pavelzw Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically yes, practically this will probably break powershell support 😅 this script currently works on both powershell and bash. we could set the shell to bash for all systems but i'm not sure whether this will screw up the rust/c toolchain in some way

should i try anyway?


- name: Run tests
if: ${{ !matrix.skip-tests }}
env:
GOOGLE_CLOUD_TEST_KEY_JSON: ${{ secrets.GOOGLE_CLOUD_TEST_KEY_JSON }}
RATTLER_TEST_R2_ACCESS_KEY_ID: ${{ secrets.RATTLER_TEST_R2_ACCESS_KEY_ID }}
RATTLER_TEST_R2_SECRET_ACCESS_KEY: ${{ secrets.RATTLER_TEST_R2_SECRET_ACCESS_KEY }}
run: >
cargo nextest run
--workspace
--features ${{ env.DEFAULT_FEATURES }}
--workspace
--features ${{ env.DEFAULT_FEATURES }}
--target ${{ matrix.target }}
${{ steps.build-options.outputs.CARGO_BUILD_OPTIONS}}
${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}}
${{ steps.build-options.outputs.CARGO_BUILD_OPTIONS }}
${{ steps.test-options.outputs.CARGO_TEST_OPTIONS }}

- name: Run doctests
if: ${{ !matrix.skip-tests }}
Expand Down
13 changes: 12 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ getrandom = { version = "0.2.15", default-features = false }
glob = "0.3.2"
google-cloud-auth = { version = "0.17.2", default-features = false }
google-cloud-token = "0.1.2"
aws-config = { version = "1.5.14", default-features = false, features = [
"rt-tokio",
"rustls",
"sso",
] }
aws-sdk-sso = { version = "1.54.0", default-features = false }
aws-sdk-s3 = { version = "1.69.0", default-features = false, features = [
"rt-tokio",
"rustls",
"sigv4a",
] }
hex = "0.4.3"
hex-literal = "0.4.1"
http = "1.2"
Expand Down Expand Up @@ -142,7 +153,7 @@ sysinfo = "0.33.1"
tar = "0.4.43"
tempdir = "0.3.7"
tempfile = "3.15.0"
temp-env = "0.3.6"
temp-env = { version = "0.3.6", features = ["async_closure"] }
test-log = "0.2.16"
thiserror = "2.0"
tokio = { version = "1.42.0", default-features = false }
Expand Down
2 changes: 1 addition & 1 deletion crates/rattler-bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ indicatif = { workspace = true }
once_cell = { workspace = true }
rattler = { path="../rattler", version = "0.28.12", default-features = false, features = ["indicatif"] }
rattler_conda_types = { path="../rattler_conda_types", version = "0.29.10", default-features = false }
rattler_networking = { path="../rattler_networking", version = "0.21.10", default-features = false, features = ["gcs"] }
rattler_networking = { path="../rattler_networking", version = "0.21.10", default-features = false, features = ["gcs", "s3"] }
rattler_repodata_gateway = { path="../rattler_repodata_gateway", version = "0.21.32", default-features = false, features = ["gateway"] }
rattler_solve = { path="../rattler_solve", version = "1.3.4", default-features = false, features = ["resolvo", "libsolv_c"] }
rattler_virtual_packages = { path="../rattler_virtual_packages", version = "1.2.0", default-features = false }
Expand Down
8 changes: 7 additions & 1 deletion crates/rattler-bin/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ use rattler_conda_types::{
Channel, ChannelConfig, GenericVirtualPackage, MatchSpec, ParseStrictness, Platform,
PrefixRecord, RepoDataRecord, Version,
};
use rattler_networking::{AuthenticationMiddleware, AuthenticationStorage};
use rattler_networking::{
s3_middleware::S3Config, AuthenticationMiddleware, AuthenticationStorage,
};
use rattler_repodata_gateway::{Gateway, RepoData, SourceConfig};
use rattler_solve::{
libsolv_c::{self},
Expand Down Expand Up @@ -153,6 +155,10 @@ pub async fn create(opt: Opt) -> anyhow::Result<()> {
authentication_storage,
)))
.with(rattler_networking::OciMiddleware)
.with(rattler_networking::S3Middleware::new(
S3Config::FromAWS,
AuthenticationStorage::default(),
))
.with(rattler_networking::GCSMiddleware)
.build();

Expand Down
35 changes: 34 additions & 1 deletion crates/rattler/src/cli/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ pub struct LoginArgs {
/// The token to use on anaconda.org / quetz authentication
#[clap(long)]
conda_token: Option<String>,

/// The S3 access key ID
#[clap(long, requires_all = ["s3_secret_access_key"], conflicts_with_all = ["token", "username", "password", "conda_token"])]
s3_access_key_id: Option<String>,
pavelzw marked this conversation as resolved.
Show resolved Hide resolved

/// The S3 secret access key
#[clap(long, requires_all = ["s3_access_key_id"])]
s3_secret_access_key: Option<String>,

/// The S3 session token
#[clap(long, requires_all = ["s3_access_key_id"])]
s3_session_token: Option<String>,
}

#[derive(Parser, Debug)]
Expand Down Expand Up @@ -71,6 +83,10 @@ pub enum AuthenticationCLIError {
#[error("Authentication with anaconda.org requires a conda token. Use `--conda-token` to provide one")]
AnacondaOrgBadMethod,

/// Bad authentication method when using S3
#[error("Authentication with S3 requires a S3 access key ID and a secret access key. Use `--s3-access-key-id` and `--s3-secret-access-key` to provide them")]
S3BadMethod,

/// Wrapper for errors that are generated from the underlying storage system
/// (keyring or file system)
#[error("Failed to interact with the authentication storage system")]
Expand All @@ -79,7 +95,7 @@ pub enum AuthenticationCLIError {

fn get_url(url: &str) -> Result<String, AuthenticationCLIError> {
// parse as url and extract host without scheme or port
let host = if url.contains("://") {
let host = if url.contains("http://") || url.contains("https://") {
url::Url::parse(url)?.host_str().unwrap().to_string()
} else {
url.to_string()
Expand Down Expand Up @@ -110,6 +126,15 @@ fn login(args: LoginArgs, storage: AuthenticationStorage) -> Result<(), Authenti
}
} else if let Some(token) = args.token {
Authentication::BearerToken(token)
} else if let (Some(access_key_id), Some(secret_access_key)) =
(args.s3_access_key_id, args.s3_secret_access_key)
{
let session_token = args.s3_session_token;
Authentication::S3Credentials {
access_key_id,
secret_access_key,
session_token,
}
} else {
return Err(AuthenticationCLIError::NoAuthenticationMethod);
};
Expand All @@ -122,6 +147,14 @@ fn login(args: LoginArgs, storage: AuthenticationStorage) -> Result<(), Authenti
return Err(AuthenticationCLIError::AnacondaOrgBadMethod);
}

if host.starts_with("s3://") && !matches!(auth, Authentication::S3Credentials { .. }) {
return Err(AuthenticationCLIError::S3BadMethod);
}

if matches!(auth, Authentication::S3Credentials { .. }) && !host.starts_with("s3://") {
return Err(AuthenticationCLIError::S3BadMethod);
}

storage
.store(&host, &auth)
.map_err(AuthenticationCLIError::StorageError)?;
Expand Down
15 changes: 13 additions & 2 deletions crates/rattler_networking/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ license.workspace = true
readme.workspace = true

[features]
default = ["native-tls"]
default = ["native-tls", "s3"]
delsner marked this conversation as resolved.
Show resolved Hide resolved
pavelzw marked this conversation as resolved.
Show resolved Hide resolved
native-tls = ["reqwest/native-tls", "google-cloud-auth?/default-tls"]
rustls-tls = ["reqwest/rustls-tls", "google-cloud-auth?/rustls-tls"]
gcs = ["google-cloud-auth", "google-cloud-token"]
s3 = ["aws-config", "aws-sdk-s3"]

[dependencies]
anyhow = { workspace = true }
Expand All @@ -25,9 +26,17 @@ dirs = { workspace = true }
fslock = { workspace = true }
google-cloud-auth = { workspace = true, optional = true }
google-cloud-token = { workspace = true, optional = true }
aws-config = { workspace = true, optional = true }
aws-sdk-s3 = { workspace = true, optional = true }
http = { workspace = true }
itertools = { workspace = true }
keyring = { workspace = true, features = ["apple-native", "windows-native", "async-secret-service", "async-io", "crypto-rust"] }
keyring = { workspace = true, features = [
"apple-native",
"windows-native",
"async-secret-service",
"async-io",
"crypto-rust",
] }
netrc-rs = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
reqwest-middleware = { workspace = true }
Expand All @@ -50,3 +59,5 @@ axum = { workspace = true }
reqwest-retry = { workspace = true }
sha2 = { workspace = true }
temp-env = { workspace = true }
rstest = { workspace = true }
rand = { workspace = true }
2 changes: 1 addition & 1 deletion crates/rattler_networking/src/authentication_middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ impl AuthenticationMiddleware {
.insert(reqwest::header::AUTHORIZATION, header_value);
Ok(req)
}
Authentication::CondaToken(_) => Ok(req),
Authentication::CondaToken(_) | Authentication::S3Credentials { .. } => Ok(req),
}
} else {
Ok(req)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ pub enum Authentication {
},
/// A conda token is sent in the URL as `/t/{TOKEN}/...`
CondaToken(String),
/// S3 credentials
S3Credentials {
/// The access key ID to use for S3 authentication
access_key_id: String,
/// The secret access key to use for S3 authentication
secret_access_key: String,
/// The session token to use for S3 authentication
session_token: Option<String>,
},
}

/// An error that can occur when parsing an authentication string
Expand Down
26 changes: 26 additions & 0 deletions crates/rattler_networking/src/authentication_storage/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,32 @@ impl AuthenticationStorage {
Ok(Some(credentials)) => return Ok((url, Some(credentials))),
};

// S3 protocol URLs need to be treated separately since they follow a different schema
if url.scheme() == "s3" {
let mut current_url = url.clone();
loop {
match self.get(current_url.as_str()) {
Ok(None) => {
let possible_rest =
current_url.as_str().rsplit_once('/').map(|(rest, _)| rest);

match possible_rest {
Some(rest) => {
if let Ok(new_url) = Url::parse(rest) {
current_url = new_url;
} else {
return Ok((url, None));
}
}
_ => return Ok((url, None)), // No more subpaths to check
}
}
Ok(Some(credentials)) => return Ok((url, Some(credentials))),
Err(_) => return Ok((url, None)),
}
}
}

// Check for credentials under e.g. `*.prefix.dev`
let Some(mut domain) = url.domain() else {
return Ok((url, None));
Expand Down
5 changes: 5 additions & 0 deletions crates/rattler_networking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ pub mod gcs_middleware;
#[cfg(feature = "gcs")]
pub use gcs_middleware::GCSMiddleware;

#[cfg(feature = "s3")]
pub mod s3_middleware;
#[cfg(feature = "s3")]
pub use s3_middleware::S3Middleware;

pub mod authentication_middleware;
pub mod authentication_storage;

Expand Down
Loading
Loading