Skip to main content

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}