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 branding domain here, so the resource the manifest
6//! registers is exactly the one the operator creates and watches — they can't
7//! drift.
8//!
9//! For a vendor whose branded domain is `acme.dev`, the access-request CRD is:
10//!
11//! ```text
12//! group:  accessrequests.acme.dev
13//! kind:   AcmeAccessRequest
14//! plural: acmeaccessrequests
15//! short:  acmear
16//! ```
17//!
18//! When no branding domain is set it falls back to the Alien defaults
19//! (`accessrequests.alien.dev` / `AlienAccessRequest`).
20
21/// The default (unbranded) DNS domain the access-request CRD lives under.
22pub const DEFAULT_LABEL_DOMAIN: &str = "alien.dev";
23
24/// Derived, white-labeled names for the access-request custom resource.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct AccessRequestCrdNames {
27    /// API group, e.g. `accessrequests.acme.dev`.
28    pub group: String,
29    /// Resource kind, e.g. `AcmeAccessRequest`.
30    pub kind: String,
31    /// Plural resource name, e.g. `acmeaccessrequests`.
32    pub plural: String,
33    /// Singular resource name, e.g. `acmeaccessrequest`.
34    pub singular: String,
35    /// Short name, e.g. `acmear`.
36    pub short_name: String,
37    /// The API version, e.g. `accessrequests.acme.dev/v1alpha1`.
38    pub api_version: String,
39    /// The CRD object's `metadata.name`, e.g.
40    /// `acmeaccessrequests.accessrequests.acme.dev`.
41    pub crd_name: String,
42}
43
44/// The CRD version served (single alpha version for now).
45pub const ACCESS_REQUEST_CRD_VERSION: &str = "v1alpha1";
46
47/// Derive the access-request-CRD names from a branding domain (e.g.
48/// `Some("acme.dev")`). `None`/empty → the Alien defaults.
49///
50/// The brand slug is the domain's first DNS label lowercased and stripped of
51/// non-alphanumerics (`acme.dev` → `acme`, `my-startup.io` → `mystartup`). The
52/// kind capitalizes it (`Acme` → `AcmeAccessRequest`).
53pub fn access_request_crd_names(label_domain: Option<&str>) -> AccessRequestCrdNames {
54    let domain = label_domain
55        .map(str::trim)
56        .filter(|d| !d.is_empty())
57        .unwrap_or(DEFAULT_LABEL_DOMAIN);
58
59    let brand = brand_slug(domain);
60    let group = format!("accessrequests.{domain}");
61    let plural = format!("{brand}accessrequests");
62    let singular = format!("{brand}accessrequest");
63    let short_name = format!("{brand}ar");
64    let kind = format!("{}AccessRequest", capitalize(&brand));
65
66    AccessRequestCrdNames {
67        api_version: format!("{group}/{ACCESS_REQUEST_CRD_VERSION}"),
68        crd_name: format!("{plural}.{group}"),
69        group,
70        kind,
71        plural,
72        singular,
73        short_name,
74    }
75}
76
77/// The lowercase alphanumeric brand slug from a domain's first label.
78fn brand_slug(domain: &str) -> String {
79    let first_label = domain.split('.').next().unwrap_or(domain);
80    let slug: String = first_label
81        .chars()
82        .filter(|c| c.is_ascii_alphanumeric())
83        .map(|c| c.to_ascii_lowercase())
84        .collect();
85    if slug.is_empty() {
86        "alien".to_string()
87    } else {
88        slug
89    }
90}
91
92/// Capitalize the first character (ASCII).
93fn capitalize(s: &str) -> String {
94    let mut chars = s.chars();
95    match chars.next() {
96        Some(first) => first.to_ascii_uppercase().to_string() + chars.as_str(),
97        None => String::new(),
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn default_is_alien() {
107        let n = access_request_crd_names(None);
108        assert_eq!(n.group, "accessrequests.alien.dev");
109        assert_eq!(n.kind, "AlienAccessRequest");
110        assert_eq!(n.plural, "alienaccessrequests");
111        assert_eq!(n.short_name, "alienar");
112        assert_eq!(n.crd_name, "alienaccessrequests.accessrequests.alien.dev");
113        assert_eq!(n.api_version, "accessrequests.alien.dev/v1alpha1");
114    }
115
116    #[test]
117    fn brands_from_domain() {
118        let n = access_request_crd_names(Some("acme.dev"));
119        assert_eq!(n.group, "accessrequests.acme.dev");
120        assert_eq!(n.kind, "AcmeAccessRequest");
121        assert_eq!(n.plural, "acmeaccessrequests");
122        assert_eq!(n.singular, "acmeaccessrequest");
123        assert_eq!(n.short_name, "acmear");
124        assert_eq!(n.crd_name, "acmeaccessrequests.accessrequests.acme.dev");
125    }
126
127    #[test]
128    fn plural_is_brand_prefixed_accessrequests() {
129        // The vendor-facing command reads `kubectl get <brand>accessrequests`.
130        let n = access_request_crd_names(Some("globex.dev"));
131        assert_eq!(n.plural, "globexaccessrequests");
132        assert_eq!(n.kind, "GlobexAccessRequest");
133        assert_eq!(n.group, "accessrequests.globex.dev");
134    }
135
136    #[test]
137    fn strips_non_alphanumerics_from_slug() {
138        let n = access_request_crd_names(Some("my-startup.io"));
139        assert_eq!(n.group, "accessrequests.my-startup.io");
140        assert_eq!(n.kind, "MystartupAccessRequest");
141        assert_eq!(n.plural, "mystartupaccessrequests");
142    }
143
144    #[test]
145    fn empty_domain_falls_back() {
146        assert_eq!(access_request_crd_names(Some("")).kind, "AlienAccessRequest");
147        assert_eq!(access_request_crd_names(Some("   ")).kind, "AlienAccessRequest");
148    }
149}