anodizer_stage_blob/lib.rs
1//! Blob stage — uploads release artifacts to S3 / GCS / Azure Blob.
2//!
3//! - [`Provider`] — `s3` / `gs` / `azblob` selection.
4//! - [`BlobStage`] — the [`anodizer_core::stage::Stage`] driver. Each config is
5//! prepared serially (template render, store build, KMS preflight) before the
6//! parallel upload, so credential/KMS errors surface before any bytes leave.
7
8mod kms;
9mod provider;
10pub mod publisher;
11mod run;
12mod store;
13mod upload;
14
15#[cfg(test)]
16mod tests;
17
18pub use provider::Provider;
19pub use publisher::BlobPublisher;
20pub use run::BlobStage;
21
22/// Environment requirements for the blob stage, derived per `blobs:` entry:
23///
24/// * `s3` with a custom `endpoint` (MinIO-style) — the static keypair
25/// (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`), any env vars the
26/// endpoint template references, and reachability of the rendered
27/// endpoint. Plain AWS (and `gs` / `azblob`) declare no credential
28/// requirement: those providers resolve ambient chains (instance
29/// metadata, workload identity, profiles) that are not env-observable,
30/// and requiring env vars there would false-fail valid setups.
31/// * a `kms_key` using a CLI-backed scheme — the matching cloud CLI
32/// (`aws` / `gcloud` / `az`).
33pub fn env_requirements(
34 ctx: &anodizer_core::context::Context,
35) -> Vec<anodizer_core::EnvRequirement> {
36 use anodizer_core::env_preflight::template_env_refs;
37 let mut out = Vec::new();
38 for c in anodizer_core::env_preflight::crate_universe(&ctx.config) {
39 for b in c.blobs.iter().flatten() {
40 // Unknown providers are config-validation territory, not preflight's.
41 let Ok(provider) = Provider::parse(&b.provider) else {
42 continue;
43 };
44 if provider == Provider::S3
45 && let Some(endpoint) = b.endpoint.as_deref()
46 {
47 out.push(anodizer_core::EnvRequirement::EnvAllOf {
48 vars: vec![
49 "AWS_ACCESS_KEY_ID".to_string(),
50 "AWS_SECRET_ACCESS_KEY".to_string(),
51 ],
52 });
53 let refs = template_env_refs(endpoint);
54 if !refs.is_empty() {
55 out.push(anodizer_core::EnvRequirement::EnvAllOf { vars: refs });
56 }
57 if let Ok(rendered) = anodizer_core::template::render(endpoint, ctx.template_vars())
58 && !rendered.trim().is_empty()
59 {
60 out.push(anodizer_core::EnvRequirement::Endpoint { url: rendered });
61 }
62 }
63 if let Some(kms_key) = b.kms_key.as_deref() {
64 let tool = match crate::kms::parse_kms_provider(kms_key) {
65 crate::kms::KmsProvider::Aws => Some("aws"),
66 crate::kms::KmsProvider::Gcp => Some("gcloud"),
67 crate::kms::KmsProvider::Azure => Some("az"),
68 _ => None,
69 };
70 if let Some(tool) = tool {
71 out.push(anodizer_core::EnvRequirement::Tool {
72 name: tool.to_string(),
73 });
74 }
75 }
76 }
77 }
78 out
79}