kopiur_api/creds.rs
1//! Pure credential/volume metadata over the CRD types: which Secrets a
2//! repository's mover needs. Shared by the controller (envFrom projection,
3//! referent watches) and external tooling (`kubectl kopiur doctor`), so the
4//! "what credentials does this backend reference" answer cannot fork.
5
6use crate::backend::{Backend, WorkloadIdentity};
7use crate::common::Encryption;
8
9/// The backend credentials Secret name for an object-store backend, if any.
10///
11/// Exhaustive over [`Backend`] (ADR §5.5): a new backend cannot compile until its
12/// credential source is decided here. Object stores read keys (e.g.
13/// `AWS_ACCESS_KEY_ID`) from `auth.secretRef`; Rclone reads its config from
14/// `configSecretRef`; Filesystem has no backend credentials. This Secret is
15/// mounted into the mover Job alongside the encryption-password Secret so kopia
16/// can reach the store (the in-process filesystem path never needs it).
17pub fn backend_auth_secret_ref(backend: &Backend) -> Option<&crate::common::SecretRef> {
18 match backend {
19 Backend::S3(b) => b.auth.as_ref().and_then(|a| a.secret_ref.as_ref()),
20 Backend::Azure(b) => b.auth.as_ref().and_then(|a| a.secret_ref.as_ref()),
21 Backend::Gcs(b) => b.auth.as_ref().and_then(|a| a.secret_ref.as_ref()),
22 Backend::B2(b) => b.auth.as_ref().and_then(|a| a.secret_ref.as_ref()),
23 Backend::Sftp(b) => b.auth.as_ref().and_then(|a| a.secret_ref.as_ref()),
24 Backend::WebDav(b) => b.auth.as_ref().and_then(|a| a.secret_ref.as_ref()),
25 Backend::Rclone(b) => b.config_secret_ref.as_ref(),
26 Backend::Gdrive(b) => b.credentials_secret_ref.as_ref(),
27 Backend::Filesystem(_) => None,
28 }
29}
30
31/// Which cloud IAM plane a workload-identity backend federates with. Drives the
32/// cloud-specific mover wiring (the Azure pod label; docs/messages naming the
33/// right SA annotation).
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum WorkloadIdentityCloud {
36 /// AWS: IRSA (web-identity token) or EKS Pod Identity via the minio-go
37 /// credential chain.
38 S3,
39 /// Azure Workload Identity: the azure-workload-identity webhook injects
40 /// `AZURE_TENANT_ID`/`AZURE_CLIENT_ID`/`AZURE_FEDERATED_TOKEN_FILE` into
41 /// pods carrying the opt-in label and running as the federated SA.
42 Azure,
43 /// GKE Workload Identity: ambient ADC via the GKE metadata server.
44 Gcs,
45}
46
47/// The backend's workload-identity binding, if any, with its cloud plane.
48///
49/// Exhaustive over [`Backend`] (ADR §5.5): only S3/Azure/GCS can carry one —
50/// the other backends' auth types make it unrepresentable, and a new backend
51/// cannot compile until its arm is decided here.
52pub fn backend_workload_identity(
53 backend: &Backend,
54) -> Option<(&WorkloadIdentity, WorkloadIdentityCloud)> {
55 match backend {
56 Backend::S3(b) => b
57 .auth
58 .as_ref()
59 .and_then(|a| a.workload_identity.as_ref())
60 .map(|wi| (wi, WorkloadIdentityCloud::S3)),
61 Backend::Azure(b) => b
62 .auth
63 .as_ref()
64 .and_then(|a| a.workload_identity.as_ref())
65 .map(|wi| (wi, WorkloadIdentityCloud::Azure)),
66 Backend::Gcs(b) => b
67 .auth
68 .as_ref()
69 .and_then(|a| a.workload_identity.as_ref())
70 .map(|wi| (wi, WorkloadIdentityCloud::Gcs)),
71 Backend::B2(_)
72 | Backend::Sftp(_)
73 | Backend::WebDav(_)
74 | Backend::Rclone(_)
75 | Backend::Gdrive(_)
76 | Backend::Filesystem(_) => None,
77 }
78}
79
80/// A credential Secret a mover Job needs, with the namespace it is sourced from.
81/// `namespace` is the resolved *source* namespace (where the operator reads the
82/// Secret when projecting), not the Job's namespace. `None` only when neither the
83/// reference nor the repository carries one — which projection treats as an
84/// actionable error (a `ClusterRepository` reference must pin a namespace).
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct CredsSecretRef {
87 /// Name of the credential `Secret`.
88 pub name: String,
89 /// Resolved source namespace, if known.
90 pub namespace: Option<String>,
91}
92
93/// The distinct credential Secrets a mover Job for `backend` + `encryption` needs
94/// as `envFrom`, each with its resolved *source* namespace: always the
95/// encryption-password Secret, plus the backend `auth` Secret when present and
96/// differently named. Deduped by name, order-stable (password first).
97///
98/// `repo_namespace` is the referencing repository's own namespace (a namespaced
99/// `Repository`), used as the source-namespace fallback when a reference omits
100/// one; pass `None` for a cluster-scoped `ClusterRepository`, whose references
101/// pin their own namespace. This is the single source of the dedup/order contract
102/// that [`mover_creds_secrets`] (names only) is built on.
103pub fn mover_creds_secret_refs(
104 backend: &Backend,
105 enc: &Encryption,
106 repo_namespace: Option<&str>,
107) -> Vec<CredsSecretRef> {
108 let source_ns = |ns: Option<String>| ns.or_else(|| repo_namespace.map(str::to_string));
109 let mut refs = vec![CredsSecretRef {
110 name: enc.password_secret_ref.name.clone(),
111 namespace: source_ns(enc.password_secret_ref.namespace.clone()),
112 }];
113 if let Some(auth) = backend_auth_secret_ref(backend)
114 && !refs.iter().any(|r| r.name == auth.name)
115 {
116 refs.push(CredsSecretRef {
117 name: auth.name.clone(),
118 namespace: source_ns(auth.namespace.clone()),
119 });
120 }
121 refs
122}
123
124/// The distinct credential Secret names a mover Job for `backend` + `encryption`
125/// needs as `envFrom`: always the encryption-password Secret, plus the backend
126/// `auth` Secret when present and different. Deduped, order-stable (password
127/// first). The common single-secret setup (password + keys in one Secret)
128/// collapses to one entry. Names-only projection of [`mover_creds_secret_refs`].
129pub fn mover_creds_secrets(backend: &Backend, enc: &Encryption) -> Vec<String> {
130 mover_creds_secret_refs(backend, enc, None)
131 .into_iter()
132 .map(|r| r.name)
133 .collect()
134}
135
136/// Env-var prefix under which a replication mover receives the **destination**
137/// backend's credential Secret (`envFrom.prefix`). The source backend's Secret is
138/// delivered unprefixed (kopia reads the plain names at `repository connect` and
139/// persists them), so the two sides never collide even when both are the same
140/// backend family with different keys (issue #200). The mover reads these back to
141/// authenticate the `kopia repository sync-to` destination.
142///
143/// The credential env-var *names* a backend reads (`AWS_*`, …) live on
144/// [`ConnectSpec::direct_credential_env_names`](../../kopiur_kopia/client/enum.ConnectSpec.html)
145/// in the kopia crate — that is the type that knows kopia's env binding, and the
146/// replication mover builds its remap from the `ConnectSpec` it hands to `sync-to`.
147pub const DEST_ENV_PREFIX: &str = "KOPIUR_DEST_";
148
149#[cfg(test)]
150mod dest_env_tests {
151 use super::*;
152
153 #[test]
154 fn dest_env_prefix_is_stable() {
155 assert_eq!(DEST_ENV_PREFIX, "KOPIUR_DEST_");
156 }
157}