Skip to content

feat: add Azure Blob Storage channel support - #2654

Open
wolfv wants to merge 106 commits into
conda:mainfrom
wolfv:pr-2615-merged-main
Open

feat: add Azure Blob Storage channel support#2654
wolfv wants to merge 106 commits into
conda:mainfrom
wolfv:pr-2615-merged-main

Conversation

@wolfv

@wolfv wolfv commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Note

This supersedes #2615 so the changes can be merged and released directly from a maintainer-owned branch. It includes the latest main and resolves the original PR's merge conflicts.

Description

Adds Azure Blob Storage as a first-class conda channel backend across rattler,
addressed through az:// URLs — the Azure counterpart to the existing S3 (s3://)
and GCS support. Channels hosted in a blob container can be fetched, solved,
indexed, and uploaded to
with no separate endpoint/config block.

Closes #2285.

URL model

A channel is addressed by its full blob host:

az://<account>.blob.core.windows.net/<container>/<prefix>

The account, endpoint, container and root prefix are all derived from the URL, so
the channel is spelled the same way it appears on the wire. Sovereign clouds and
the Azurite emulator work automatically because the endpoint lives in the host —
no allow-list of accounts or endpoints is baked in.

What's included

  • rattler_azure (new crate): az:// URL parsing into typed
    AzureCoordinates { account, container }, AzureCredentials
    (account key / SAS token), and optional az login → short-lived
    user-delegation SAS minting for write paths (behind the clap feature).
  • rattler_networking: AzureMiddleware — rewrites az://https://
    (a plain scheme swap) and signs via reqsign's DefaultCredentialProvider
    chain (env vars → workload/managed identity → az login). Public/anonymous
    containers work with zero credentials; a configured-but-broken credential is
    a hard error rather than a silent anonymous downgrade.
  • rattler_repodata_gateway: accepts the az:// channel scheme for
    (sharded) repodata fetch.
  • rattler_index: azblob indexing backend (opendal), CLI subcommand.
  • rattler_upload: az:// upload path with concurrent block upload.
  • rattler-bin: registers AzureMiddleware behind an azure feature.
  • py-rattler: AzureMiddleware binding, included in the default client stack.

Authentication

Source How
Account key --account-key / AZURE_STORAGE_KEY
SAS token --sas-token / AZURE_STORAGE_SAS_TOKEN
az login --azure-cli mints a short-lived user-delegation SAS (write paths)
Ambient (read) reqsign chain: env → managed/workload identity → az login

Security / trust model

The URL host is trusted verbatim and becomes the request target, so az://
userinfo is rejected. This is a pure safety gate that removes no
functionality: Azure Blob has no user:pass@ auth mode — credentials come from
Shared Key headers, SAS query strings, or AAD bearer tokens, and the storage
account is the host's first label (account.blob.core.windows.net), never
userinfo. So userinfo in an az:// URL can do exactly one thing: hide the real
host — az://trusted.blob.core.windows.net@attacker.example/… targets
attacker.example. Rejecting it closes that host-spoofing vector while leaving
every legitimate URL untouched. (Scoped to az://; https:// channels that
legitimately use Basic-auth userinfo are unaffected.)

On Windows the Azure CLI is resolved via which and invoked directly rather than
through cmd /C, avoiding a metacharacter-injection surface.

Known limitations (documented in-code)

  • opendal 0.57's azblob backend honours if_not_exists only on the single-shot
    Put Blob path (≤ 10 MiB), not the multi-block path used for larger packages,
    so the upload overwrite guard does an explicit pre-write stat to cover all
    sizes (with a small, documented stat→write TOCTOU window).
  • A user-delegation SAS minted against a flat container is container-scoped,
    not prefix-scoped; a short TTL bounds the blast radius.

How Has This Been Tested?

  • Unit tests across rattler_azure, rattler_networking (URL rewrite, userinfo
    rejection, credential-source detection, unsigned fallback, SAS pass-through),
    and rattler_upload.
  • Azurite-backed integration tests for the index path.
  • Live-Azure gated tests (az login AAD fetch) against a real account.
  • py-rattler: unit test asserting AzureMiddleware is in the default client stack.
  • Full workspace cargo build, cargo clippy -D warnings, cargo fmt, and
    cargo nextest all green.
  • Running in production on our internal build farm, where it indexes and
    serves conda packages from an Azure Blob-hosted channel.

Platform caveat: all testing has been on Linux only — I have no access
to Windows or macOS machines. In particular the Windows Azure CLI handling (the
which-based az/az.cmd resolution) is written but has not been exercised on
a real Windows host; a review pass or CI on those platforms would be welcome.

AI Disclosure

  • This PR contains AI-generated content.
    • I have tested any AI-generated content in my PR.
    • I take responsibility for any AI-generated content in my PR.

Tools: Claude Code (Claude Opus 4.8)

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added sufficient tests to cover my changes.

Additional validation after updating to current main

  • cargo fmt --all -- --check
  • Workspace cargo check with CI features
  • cargo check --manifest-path py-rattler/Cargo.toml
  • cargo clippy --all-targets --workspace -- -D warnings -Dclippy::dbg_macro
Prompt added for this replacement PR
Can you rebase this PR https://github.com/conda/rattler/pull/2615 and review it?

I was thinking just straight to rattler so that I can merge and release with these changes today

tenzinplatter and others added 30 commits June 12, 2026 09:35
* feat(rattler_index): add Azure Blob backend

* fix(rattler_index): disable preconditions on azure (opendal azblob lacks conditional writes)

* fix(rattler_index): re-index packages whose blob size changed
* fix(rattler_index): re-index packages whose blob size changed

* fix: move to md5 checks instead of size

* feat(rattler_index): add Azure Blob backend

* fix(rattler_index): disable preconditions on azure (opendal azblob lacks conditional writes)

* fix(rattler_index): base64-decode backend md5 before comparing

* test(rattler_index): probe azure content_md5 on list

* chore: lint
tenzinplatter and others added 26 commits August 3, 2026 10:00
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	crates/rattler-bin/src/commands/client.rs
#	py-rattler/Cargo.lock
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

cargo-semver-checks detected API breaking changes compared with the pull request's base revision.

Details
--- failure constructible_struct_adds_field: externally-constructible struct adds field ---

Description:
A pub struct constructible with a struct literal has a new pub field. Existing struct literals must be updated to include the new field.
        ref: https://doc.rust-lang.org/reference/expressions/struct-expr.html
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/constructible_struct_adds_field.ron

Failed in:
  field CommonConfig.azure_options in /home/runner/work/rattler/rattler/crates/rattler_config/src/config.rs:206
  field CommonConfig.azure_options in /home/runner/work/rattler/rattler/crates/rattler_config/src/config.rs:206
    Building rattler_config v0.6.2 (current)
       Built [  21.730s] (current)
     Parsing rattler_config v0.6.2 (current)
      Parsed [   0.019s] (current)
    Building rattler_config v0.6.2 (baseline)
       Built [  21.818s] (baseline)
     Parsing rattler_config v0.6.2 (baseline)
      Parsed [   0.019s] (baseline)
    Checking rattler_config v0.6.2 -> v0.6.2 (assume minor change)
     Checked [   0.141s] 196 checks: 195 pass, 1 fail, 0 warn, 57 skip

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [  45.846s] rattler_config

    Building rattler_git v0.2.1 (current)
       Built [  14.858s] (current)
     Parsing rattler_git v0.2.1 (current)
      Parsed [   0.011s] (current)
    Building rattler_git v0.2.1 (baseline)
       Built [  14.924s] (baseline)
     Parsing rattler_git v0.2.1 (baseline)
      Parsed [   0.011s] (baseline)
    Checking rattler_git v0.2.1 -> v0.2.1 (assume minor change)
     Checked [   0.119s] 196 checks: 196 pass, 57 skip
     Summary no semver update required
    Finished [  31.129s] rattler_git

    Building rattler_index v0.30.11 (current)
       Built [  77.727s] (current)
     Parsing rattler_index v0.30.11 (current)
      Parsed [   0.024s] (current)
    Building rattler_index v0.30.11 (baseline)
       Built [  74.695s] (baseline)
     Parsing rattler_index v0.30.11 (baseline)
      Parsed [   0.023s] (baseline)
    Checking rattler_index v0.30.11 -> v0.30.11 (assume minor change)
     Checked [   0.086s] 196 checks: 196 pass, 57 skip
     Summary no semver update required
    Finished [ 156.146s] rattler_index

    Building rattler_networking v0.30.3 (current)
       Built [  69.139s] (current)
     Parsing rattler_networking v0.30.3 (current)
      Parsed [   0.041s] (current)
    Building rattler_networking v0.30.3 (baseline)
       Built [  68.116s] (baseline)
     Parsing rattler_networking v0.30.3 (baseline)
      Parsed [   0.040s] (baseline)
    Checking rattler_networking v0.30.3 -> v0.30.3 (assume minor change)
     Checked [   0.197s] 196 checks: 196 pass, 57 skip
     Summary no semver update required
    Finished [ 140.226s] rattler_networking

    Building rattler_redaction v0.2.2 (current)
       Built [  29.505s] (current)
     Parsing rattler_redaction v0.2.2 (current)
      Parsed [   0.006s] (current)
    Building rattler_redaction v0.2.2 (baseline)
       Built [  28.996s] (baseline)
     Parsing rattler_redaction v0.2.2 (baseline)
      Parsed [   0.006s] (baseline)
    Checking rattler_redaction v0.2.2 -> v0.2.2 (assume minor change)
     Checked [   0.064s] 196 checks: 196 pass, 57 skip
     Summary no semver update required
    Finished [  60.079s] rattler_redaction

    Building rattler_repodata_gateway v0.31.0 (current)
       Built [  57.654s] (current)
     Parsing rattler_repodata_gateway v0.31.0 (current)
      Parsed [   0.053s] (current)
    Building rattler_repodata_gateway v0.31.0 (baseline)
       Built [  57.172s] (baseline)
     Parsing rattler_repodata_gateway v0.31.0 (baseline)
      Parsed [   0.053s] (baseline)
    Checking rattler_repodata_gateway v0.31.0 -> v0.31.0 (assume minor change)
     Checked [   0.180s] 196 checks: 196 pass, 57 skip
     Summary no semver update required
    Finished [ 117.761s] rattler_repodata_gateway


--- failure enum_variant_added: enum variant added on exhaustive enum ---

Description:
A publicly-visible enum without #[non_exhaustive] has a new variant.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#enum-variant-new
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/enum_variant_added.ron

Failed in:
  variant ServerType:Azure in /home/runner/work/rattler/rattler/crates/rattler_upload/src/upload/opt.rs:130
    Building rattler_upload v0.10.1 (current)
       Built [  79.063s] (current)
     Parsing rattler_upload v0.10.1 (current)
      Parsed [   0.039s] (current)
    Building rattler_upload v0.10.1 (baseline)
       Built [  77.441s] (baseline)
     Parsing rattler_upload v0.10.1 (baseline)
      Parsed [   0.033s] (baseline)
    Checking rattler_upload v0.10.1 -> v0.10.1 (assume minor change)
     Checked [   0.124s] 196 checks: 195 pass, 1 fail, 0 warn, 57 skip

     Summary semver requires new major version: 1 major and 0 minor checks failed
    Finished [ 160.322s] rattler_upload

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Azure Blob Middleware to rattler_networking

2 participants