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
26/// The default (unbranded) slug the access-request CRD lives under.
27pub const DEFAULT_BRAND: &str = "alien";
28
29/// Derived, white-labeled names for the access-request custom resource.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct AccessRequestCrdNames {
32    /// API group, e.g. `accessrequests.acme`.
33    pub group: String,
34    /// Resource kind, e.g. `AcmeAccessRequest`.
35    pub kind: String,
36    /// Plural resource name, e.g. `acmeaccessrequests`.
37    pub plural: String,
38    /// Singular resource name, e.g. `acmeaccessrequest`.
39    pub singular: String,
40    /// Short name, e.g. `acmear`.
41    pub short_name: String,
42    /// The API version, e.g. `accessrequests.acme/v1alpha1`.
43    pub api_version: String,
44    /// The CRD object's `metadata.name`, e.g.
45    /// `acmeaccessrequests.accessrequests.acme`.
46    pub crd_name: String,
47}
48
49/// The CRD version served (single alpha version for now).
50pub const ACCESS_REQUEST_CRD_VERSION: &str = "v1alpha1";
51
52/// Derive the access-request-CRD names from a brand name (e.g.
53/// `Some("acme")`, or `Some("Acme Corp")`). `None`/empty → the Alien defaults.
54///
55/// The brand slug is the input's first DNS label lowercased and stripped of
56/// non-alphanumerics (`Acme Corp` → `acmecorp`, `acme.dev` → `acme`). The
57/// kind capitalizes it (`Acme` → `AcmeAccessRequest`).
58pub fn access_request_crd_names(brand_name: Option<&str>) -> AccessRequestCrdNames {
59    let name = brand_name
60        .map(str::trim)
61        .filter(|n| !n.is_empty())
62        .unwrap_or(DEFAULT_BRAND);
63
64    let brand = brand_slug(name);
65    let group = format!("accessrequests.{brand}");
66    let plural = format!("{brand}accessrequests");
67    let singular = format!("{brand}accessrequest");
68    let short_name = format!("{brand}ar");
69    let kind = format!("{}AccessRequest", capitalize(&brand));
70
71    AccessRequestCrdNames {
72        api_version: format!("{group}/{ACCESS_REQUEST_CRD_VERSION}"),
73        crd_name: format!("{plural}.{group}"),
74        group,
75        kind,
76        plural,
77        singular,
78        short_name,
79    }
80}
81
82/// The lowercase alphanumeric brand slug from a name's first dot-separated
83/// label (so a real domain's first label still works as input), with any
84/// remaining non-alphanumerics (spaces, punctuation) stripped out.
85fn brand_slug(name: &str) -> String {
86    let first_label = name.split('.').next().unwrap_or(name);
87    let slug: String = first_label
88        .chars()
89        .filter(|c| c.is_ascii_alphanumeric())
90        .map(|c| c.to_ascii_lowercase())
91        .collect();
92    if slug.is_empty() {
93        "alien".to_string()
94    } else {
95        slug
96    }
97}
98
99/// Capitalize the first character (ASCII).
100fn capitalize(s: &str) -> String {
101    let mut chars = s.chars();
102    match chars.next() {
103        Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
104        None => String::new(),
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn default_is_alien() {
114        let n = access_request_crd_names(None);
115        assert_eq!(n.group, "accessrequests.alien");
116        assert_eq!(n.kind, "AlienAccessRequest");
117        assert_eq!(n.plural, "alienaccessrequests");
118        assert_eq!(n.short_name, "alienar");
119        assert_eq!(n.crd_name, "alienaccessrequests.accessrequests.alien");
120        assert_eq!(n.api_version, "accessrequests.alien/v1alpha1");
121    }
122
123    #[test]
124    fn brands_from_domain() {
125        let n = access_request_crd_names(Some("acme.dev"));
126        assert_eq!(n.group, "accessrequests.acme");
127        assert_eq!(n.kind, "AcmeAccessRequest");
128        assert_eq!(n.plural, "acmeaccessrequests");
129        assert_eq!(n.singular, "acmeaccessrequest");
130        assert_eq!(n.short_name, "acmear");
131        assert_eq!(n.crd_name, "acmeaccessrequests.accessrequests.acme");
132    }
133
134    #[test]
135    fn brands_from_plain_name() {
136        let n = access_request_crd_names(Some("My Cool App"));
137        assert_eq!(n.group, "accessrequests.mycoolapp");
138        assert_eq!(n.kind, "MycoolappAccessRequest");
139        assert_eq!(n.plural, "mycoolappaccessrequests");
140        assert_eq!(n.short_name, "mycoolappar");
141    }
142
143    #[test]
144    fn plural_is_brand_prefixed_accessrequests() {
145        // The vendor-facing command reads `kubectl get <brand>accessrequests`.
146        let n = access_request_crd_names(Some("globex.dev"));
147        assert_eq!(n.plural, "globexaccessrequests");
148        assert_eq!(n.kind, "GlobexAccessRequest");
149        assert_eq!(n.group, "accessrequests.globex");
150    }
151
152    #[test]
153    fn strips_non_alphanumerics_from_slug() {
154        let n = access_request_crd_names(Some("my-startup.io"));
155        assert_eq!(n.group, "accessrequests.mystartup");
156        assert_eq!(n.kind, "MystartupAccessRequest");
157        assert_eq!(n.plural, "mystartupaccessrequests");
158    }
159
160    #[test]
161    fn empty_domain_falls_back() {
162        assert_eq!(
163            access_request_crd_names(Some("")).kind,
164            "AlienAccessRequest"
165        );
166        assert_eq!(
167            access_request_crd_names(Some("   ")).kind,
168            "AlienAccessRequest"
169        );
170    }
171}