diff --git a/crates/alien-core/src/access_request_crd.rs b/crates/alien-core/src/access_request_crd.rs index 51f97ce10..a3455e879 100644 --- a/crates/alien-core/src/access_request_crd.rs +++ b/crates/alien-core/src/access_request_crd.rs @@ -2,29 +2,34 @@ //! //! The operator manifest (`alien-helm`) and the operator runtime //! (`alien-access-request-crd-loop`) BOTH derive the CRD's group/kind/plural -//! from the deployment's branding domain here, so the resource the manifest +//! from the deployment's brand name here, so the resource the manifest //! registers is exactly the one the operator creates and watches — they can't //! drift. //! -//! For a vendor whose branded domain is `acme.dev`, the access-request CRD is: +//! Kubernetes requires the API group to be *shaped* like a DNS subdomain, but +//! never resolves it — so the brand isn't a domain the vendor owns, just a +//! stable, customer-facing identity (e.g. the project name) slugified into +//! that shape. +//! +//! For a vendor branded `acme`, the access-request CRD is: //! //! ```text -//! group: accessrequests.acme.dev +//! group: accessrequests.acme //! kind: AcmeAccessRequest //! plural: acmeaccessrequests //! short: acmear //! ``` //! -//! When no branding domain is set it falls back to the Alien defaults -//! (`accessrequests.alien.dev` / `AlienAccessRequest`). +//! When no brand is set it falls back to the Alien defaults +//! (`accessrequests.alien` / `AlienAccessRequest`). -/// The default (unbranded) DNS domain the access-request CRD lives under. -pub const DEFAULT_LABEL_DOMAIN: &str = "alien.dev"; +/// The default (unbranded) slug the access-request CRD lives under. +pub const DEFAULT_BRAND: &str = "alien"; /// Derived, white-labeled names for the access-request custom resource. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AccessRequestCrdNames { - /// API group, e.g. `accessrequests.acme.dev`. + /// API group, e.g. `accessrequests.acme`. pub group: String, /// Resource kind, e.g. `AcmeAccessRequest`. pub kind: String, @@ -34,30 +39,30 @@ pub struct AccessRequestCrdNames { pub singular: String, /// Short name, e.g. `acmear`. pub short_name: String, - /// The API version, e.g. `accessrequests.acme.dev/v1alpha1`. + /// The API version, e.g. `accessrequests.acme/v1alpha1`. pub api_version: String, /// The CRD object's `metadata.name`, e.g. - /// `acmeaccessrequests.accessrequests.acme.dev`. + /// `acmeaccessrequests.accessrequests.acme`. pub crd_name: String, } /// The CRD version served (single alpha version for now). pub const ACCESS_REQUEST_CRD_VERSION: &str = "v1alpha1"; -/// Derive the access-request-CRD names from a branding domain (e.g. -/// `Some("acme.dev")`). `None`/empty → the Alien defaults. +/// Derive the access-request-CRD names from a brand name (e.g. +/// `Some("acme")`, or `Some("Acme Corp")`). `None`/empty → the Alien defaults. /// -/// The brand slug is the domain's first DNS label lowercased and stripped of -/// non-alphanumerics (`acme.dev` → `acme`, `my-startup.io` → `mystartup`). The +/// The brand slug is the input's first DNS label lowercased and stripped of +/// non-alphanumerics (`Acme Corp` → `acmecorp`, `acme.dev` → `acme`). The /// kind capitalizes it (`Acme` → `AcmeAccessRequest`). -pub fn access_request_crd_names(label_domain: Option<&str>) -> AccessRequestCrdNames { - let domain = label_domain +pub fn access_request_crd_names(brand_name: Option<&str>) -> AccessRequestCrdNames { + let name = brand_name .map(str::trim) - .filter(|d| !d.is_empty()) - .unwrap_or(DEFAULT_LABEL_DOMAIN); + .filter(|n| !n.is_empty()) + .unwrap_or(DEFAULT_BRAND); - let brand = brand_slug(domain); - let group = format!("accessrequests.{domain}"); + let brand = brand_slug(name); + let group = format!("accessrequests.{brand}"); let plural = format!("{brand}accessrequests"); let singular = format!("{brand}accessrequest"); let short_name = format!("{brand}ar"); @@ -74,9 +79,11 @@ pub fn access_request_crd_names(label_domain: Option<&str>) -> AccessRequestCrdN } } -/// The lowercase alphanumeric brand slug from a domain's first label. -fn brand_slug(domain: &str) -> String { - let first_label = domain.split('.').next().unwrap_or(domain); +/// The lowercase alphanumeric brand slug from a name's first dot-separated +/// label (so a real domain's first label still works as input), with any +/// remaining non-alphanumerics (spaces, punctuation) stripped out. +fn brand_slug(name: &str) -> String { + let first_label = name.split('.').next().unwrap_or(name); let slug: String = first_label .chars() .filter(|c| c.is_ascii_alphanumeric()) @@ -105,23 +112,32 @@ mod tests { #[test] fn default_is_alien() { let n = access_request_crd_names(None); - assert_eq!(n.group, "accessrequests.alien.dev"); + assert_eq!(n.group, "accessrequests.alien"); assert_eq!(n.kind, "AlienAccessRequest"); assert_eq!(n.plural, "alienaccessrequests"); assert_eq!(n.short_name, "alienar"); - assert_eq!(n.crd_name, "alienaccessrequests.accessrequests.alien.dev"); - assert_eq!(n.api_version, "accessrequests.alien.dev/v1alpha1"); + assert_eq!(n.crd_name, "alienaccessrequests.accessrequests.alien"); + assert_eq!(n.api_version, "accessrequests.alien/v1alpha1"); } #[test] fn brands_from_domain() { let n = access_request_crd_names(Some("acme.dev")); - assert_eq!(n.group, "accessrequests.acme.dev"); + assert_eq!(n.group, "accessrequests.acme"); assert_eq!(n.kind, "AcmeAccessRequest"); assert_eq!(n.plural, "acmeaccessrequests"); assert_eq!(n.singular, "acmeaccessrequest"); assert_eq!(n.short_name, "acmear"); - assert_eq!(n.crd_name, "acmeaccessrequests.accessrequests.acme.dev"); + assert_eq!(n.crd_name, "acmeaccessrequests.accessrequests.acme"); + } + + #[test] + fn brands_from_plain_name() { + let n = access_request_crd_names(Some("My Cool App")); + assert_eq!(n.group, "accessrequests.mycoolapp"); + assert_eq!(n.kind, "MycoolappAccessRequest"); + assert_eq!(n.plural, "mycoolappaccessrequests"); + assert_eq!(n.short_name, "mycoolappar"); } #[test] @@ -130,13 +146,13 @@ mod tests { let n = access_request_crd_names(Some("globex.dev")); assert_eq!(n.plural, "globexaccessrequests"); assert_eq!(n.kind, "GlobexAccessRequest"); - assert_eq!(n.group, "accessrequests.globex.dev"); + assert_eq!(n.group, "accessrequests.globex"); } #[test] fn strips_non_alphanumerics_from_slug() { let n = access_request_crd_names(Some("my-startup.io")); - assert_eq!(n.group, "accessrequests.my-startup.io"); + assert_eq!(n.group, "accessrequests.mystartup"); assert_eq!(n.kind, "MystartupAccessRequest"); assert_eq!(n.plural, "mystartupaccessrequests"); } diff --git a/crates/alien-helm/src/generator.rs b/crates/alien-helm/src/generator.rs index b042beee8..03894f9bf 100644 --- a/crates/alien-helm/src/generator.rs +++ b/crates/alien-helm/src/generator.rs @@ -124,8 +124,10 @@ pub struct OperatorManifestOptions<'a> { /// `RawManifest`; ignored for `HelmTemplate`, which uses `.Release.Namespace`. /// In `Namespace` scope this is also the namespace observed. pub install_namespace: Option<&'a str>, - /// The vendor's branded DNS domain (e.g. `acme.dev`), used to white-label - /// the access-request CRD (group/kind/plural). `None` → the Alien defaults. + /// The vendor's brand name (e.g. `acme`, or a real owned domain like + /// `acme.dev`), used to white-label the access-request CRD + /// (group/kind/plural). It's slugified, never resolved as DNS — any + /// stable customer-facing identity works. `None` → the Alien defaults. /// Same value the operator carries at runtime, so both agree on the CRD. pub label_domain: Option<&'a str>, pub scope: OperatorScope, @@ -4277,7 +4279,7 @@ mod tests { // The access-request CRD is the operator's own control resource: it // materializes access requests and records the approval window in // status, but never deletes them. - if api_groups == vec!["accessrequests.alien.dev"] { + if api_groups == vec!["accessrequests.alien"] { assert!( verbs.iter().all(|v| matches!( *v, @@ -4358,9 +4360,11 @@ mod tests { #[test] fn access_request_crd_is_white_labeled_from_the_brand_domain() { - // A vendor whose branded domain is acme.dev gets AcmeAccessRequest, not + // A vendor branded acme.dev gets AcmeAccessRequest, not // AlienAccessRequest — the CRD, RBAC, and (elsewhere) the operator - // runtime all derive from the same domain. + // runtime all derive from the same brand slug. The brand's DNS shape + // is never required — it's slugified into the CRD group, never + // resolved. let manifest = generate_operator_manifest(OperatorManifestOptions { manager_url: "https://manager.example.com", group_token: "ax_dg_test", @@ -4386,11 +4390,11 @@ mod tests { .expect("manifest should include the access-request CRD"); assert_eq!( yaml_path(&crd, &["metadata", "name"]).and_then(YamlValue::as_str), - Some("acmeaccessrequests.accessrequests.acme.dev") + Some("acmeaccessrequests.accessrequests.acme") ); assert_eq!( yaml_path(&crd, &["spec", "group"]).and_then(YamlValue::as_str), - Some("accessrequests.acme.dev") + Some("accessrequests.acme") ); assert_eq!( yaml_path(&crd, &["spec", "names", "kind"]).and_then(YamlValue::as_str), @@ -4407,7 +4411,7 @@ mod tests { "no alien-named resource in a branded build" ); assert!( - !text.contains("accessrequests.alien.dev"), + !text.contains("accessrequests.alien"), "no alien group in a branded build" ); @@ -4421,10 +4425,7 @@ mod tests { .any(|r| { r.get("apiGroups") .and_then(YamlValue::as_sequence) - .map(|g| { - g.iter() - .any(|x| x.as_str() == Some("accessrequests.acme.dev")) - }) + .map(|g| g.iter().any(|x| x.as_str() == Some("accessrequests.acme"))) .unwrap_or(false) }); assert!( diff --git a/crates/alien-operator/src/cli.rs b/crates/alien-operator/src/cli.rs index 1be996a69..e7aa04952 100644 --- a/crates/alien-operator/src/cli.rs +++ b/crates/alien-operator/src/cli.rs @@ -445,11 +445,17 @@ async fn run( .maybe_label_selector(args.operator_label_selector) .observe_all_namespaces(args.operator_observe_all_namespaces) .maybe_app_version(args.operator_release_version) - .maybe_label_domain( - embedded_config - .as_ref() - .and_then(|config| config.label_domain.clone()), - ) + .maybe_label_domain(embedded_config.as_ref().and_then(|config| { + // `brand` is the already-slugged identity `access_request_crd_names` + // expects (see alien-core::access_request_crd) — packages-builder + // computes it once and embeds both fields, but `label_domain` is + // the raw, pre-slug config value (e.g. a placeholder like + // "acme.local"), which can disagree with what the manifest + // generator derived the CRD/RBAC from. Prefer `brand`; fall back + // to `label_domain` only for older embedded binaries that + // predate the `brand` field. + config.brand.clone().or_else(|| config.label_domain.clone()) + })) .maybe_collector_token(collector_token) .maybe_public_endpoints(public_endpoints) .stack_settings(stack_settings)