Skip to main content

alien_core/
access_request_crd.rs

1//! White-labeled naming for the access-request custom resource.
2//!
3//! The operator manifest (`alien-helm`) and the operator runtime
4//! (`alien-access-request-crd-loop`) BOTH derive the CRD's group/kind/plural
5//! from the deployment's brand name here, so the resource the manifest
6//! registers is exactly the one the operator creates and watches — they can't
7//! drift.
8//!
9//! Kubernetes requires the API group to be *shaped* like a DNS subdomain, but
10//! never resolves it — so the brand isn't a domain the vendor owns, just a
11//! stable, customer-facing identity (e.g. the project name) slugified into
12//! that shape.
13//!
14//! For a vendor branded `acme`, the access-request CRD is:
15//!
16//! ```text
17//! group:  accessrequests.acme
18//! kind:   AcmeAccessRequest
19//! plural: acmeaccessrequests
20//! short:  acmear
21//! ```
22//!
23//! When no brand is set it falls back to the Alien defaults
24//! (`accessrequests.alien` / `AlienAccessRequest`).
25
26use crate::DEFAULT_ALIEN_LABEL_DOMAIN;
27
28/// The default (unbranded) slug the access-request CRD lives under.
29pub const DEFAULT_BRAND: &str = "alien";
30
31/// Derived, white-labeled names for the access-request custom resource.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct AccessRequestCrdNames {
34    /// API group, e.g. `accessrequests.acme`.
35    pub group: String,
36    /// Resource kind, e.g. `AcmeAccessRequest`.
37    pub kind: String,
38    /// Plural resource name, e.g. `acmeaccessrequests`.
39    pub plural: String,
40    /// Singular resource name, e.g. `acmeaccessrequest`.
41    pub singular: String,
42    /// Short name, e.g. `acmear`.
43    pub short_name: String,
44    /// The API version, e.g. `accessrequests.acme/v1alpha1`.
45    pub api_version: String,
46    /// The CRD object's `metadata.name`, e.g.
47    /// `acmeaccessrequests.accessrequests.acme`.
48    pub crd_name: String,
49}
50
51/// The CRD version served (single alpha version for now).
52pub const ACCESS_REQUEST_CRD_VERSION: &str = "v1alpha1";
53
54/// Derive the access-request-CRD names from a brand name (e.g.
55/// `Some("acme")`, or `Some("Acme Corp")`). `None`/empty → the Alien defaults.
56///
57/// The brand slug is the input's first DNS label lowercased and stripped of
58/// non-alphanumerics (`Acme Corp` → `acmecorp`, `acme.dev` → `acme`). The
59/// kind capitalizes it (`Acme` → `AcmeAccessRequest`).
60pub fn access_request_crd_names(brand_name: Option<&str>) -> AccessRequestCrdNames {
61    let name = brand_name
62        .map(str::trim)
63        .filter(|n| !n.is_empty())
64        .unwrap_or(DEFAULT_BRAND);
65
66    let brand = brand_slug(name);
67    let group = format!("accessrequests.{brand}");
68    let plural = format!("{brand}accessrequests");
69    let singular = format!("{brand}accessrequest");
70    let short_name = format!("{brand}ar");
71    let kind = format!("{}AccessRequest", capitalize(&brand));
72
73    AccessRequestCrdNames {
74        api_version: format!("{group}/{ACCESS_REQUEST_CRD_VERSION}"),
75        crd_name: format!("{plural}.{group}"),
76        group,
77        kind,
78        plural,
79        singular,
80        short_name,
81    }
82}
83
84/// The lowercase alphanumeric brand slug from a name's first dot-separated
85/// label (so a real domain's first label still works as input), with any
86/// remaining non-alphanumerics (spaces, punctuation) stripped out.
87pub fn brand_slug(name: &str) -> String {
88    let first_label = name.split('.').next().unwrap_or(name);
89    let slug: String = first_label
90        .chars()
91        .filter(|c| c.is_ascii_alphanumeric())
92        .map(|c| c.to_ascii_lowercase())
93        .collect();
94    if slug.is_empty() {
95        "alien".to_string()
96    } else {
97        slug
98    }
99}
100
101/// Returns the DNS-safe label domain used by current Kubernetes resources.
102pub fn current_kubernetes_label_domain(configured: &str) -> String {
103    if configured == DEFAULT_ALIEN_LABEL_DOMAIN {
104        configured.to_string()
105    } else {
106        brand_slug(configured)
107    }
108}
109
110/// Returns a valid, distinct legacy label domain for a branded deployment.
111pub fn explicit_legacy_kubernetes_label_domain(configured: &str) -> Option<&str> {
112    let current = current_kubernetes_label_domain(configured);
113    (configured != current && is_valid_kubernetes_label_domain(configured)).then_some(configured)
114}
115
116/// Whether a value can safely serve as the domain portion of a Kubernetes label key.
117pub fn is_valid_kubernetes_label_domain(value: &str) -> bool {
118    !value.is_empty()
119        && value.len() <= 253
120        && value.split('.').all(|label| {
121            !label.is_empty()
122                && label.len() <= 63
123                && label
124                    .bytes()
125                    .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
126                && label
127                    .as_bytes()
128                    .first()
129                    .is_some_and(u8::is_ascii_alphanumeric)
130                && label
131                    .as_bytes()
132                    .last()
133                    .is_some_and(u8::is_ascii_alphanumeric)
134        })
135}
136
137/// Capitalize the first character (ASCII).
138fn capitalize(s: &str) -> String {
139    let mut chars = s.chars();
140    match chars.next() {
141        Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
142        None => String::new(),
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn default_is_alien() {
152        let n = access_request_crd_names(None);
153        assert_eq!(n.group, "accessrequests.alien");
154        assert_eq!(n.kind, "AlienAccessRequest");
155        assert_eq!(n.plural, "alienaccessrequests");
156        assert_eq!(n.short_name, "alienar");
157        assert_eq!(n.crd_name, "alienaccessrequests.accessrequests.alien");
158        assert_eq!(n.api_version, "accessrequests.alien/v1alpha1");
159    }
160
161    #[test]
162    fn brands_from_domain() {
163        let n = access_request_crd_names(Some("acme.dev"));
164        assert_eq!(n.group, "accessrequests.acme");
165        assert_eq!(n.kind, "AcmeAccessRequest");
166        assert_eq!(n.plural, "acmeaccessrequests");
167        assert_eq!(n.singular, "acmeaccessrequest");
168        assert_eq!(n.short_name, "acmear");
169        assert_eq!(n.crd_name, "acmeaccessrequests.accessrequests.acme");
170    }
171
172    #[test]
173    fn brands_from_plain_name() {
174        let n = access_request_crd_names(Some("My Cool App"));
175        assert_eq!(n.group, "accessrequests.mycoolapp");
176        assert_eq!(n.kind, "MycoolappAccessRequest");
177        assert_eq!(n.plural, "mycoolappaccessrequests");
178        assert_eq!(n.short_name, "mycoolappar");
179    }
180
181    #[test]
182    fn plural_is_brand_prefixed_accessrequests() {
183        // The vendor-facing command reads `kubectl get <brand>accessrequests`.
184        let n = access_request_crd_names(Some("globex.dev"));
185        assert_eq!(n.plural, "globexaccessrequests");
186        assert_eq!(n.kind, "GlobexAccessRequest");
187        assert_eq!(n.group, "accessrequests.globex");
188    }
189
190    #[test]
191    fn strips_non_alphanumerics_from_slug() {
192        let n = access_request_crd_names(Some("my-startup.io"));
193        assert_eq!(n.group, "accessrequests.mystartup");
194        assert_eq!(n.kind, "MystartupAccessRequest");
195        assert_eq!(n.plural, "mystartupaccessrequests");
196    }
197
198    #[test]
199    fn empty_domain_falls_back() {
200        assert_eq!(
201            access_request_crd_names(Some("")).kind,
202            "AlienAccessRequest"
203        );
204        assert_eq!(
205            access_request_crd_names(Some("   ")).kind,
206            "AlienAccessRequest"
207        );
208    }
209}