hyperi-rustlib 2.7.2

Opinionated, drop-in Rust toolkit for production services at scale. The patterns from blog posts as actual code: 8-layer config cascade, structured logging with PII masking, Prometheus + OpenTelemetry, Kafka/gRPC transports, tiered disk-spillover, adaptive worker pools, graceful shutdown.
// Project:   hyperi-rustlib
// File:      src/deployment/app_project.rs
// Purpose:   Generate ArgoCD AppProject CRs
//
// License:   FSL-1.1-ALv2
// Copyright: (c) 2026 HYPERI PTY LIMITED

//! ArgoCD `AppProject` generator.
//!
//! [`AppProjectContract`] describes a per-team or per-business-unit
//! ArgoCD project with restricted `sourceRepos`, `destinations`, and
//! `roles`. Consumer Applications reference the project via their
//! `spec.project` field.
//!
//! See the spec section 3.5 in
//! `docs/superpowers/specs/2026-05-15-argocd-enterprise-enhancements-spec.md`.

use std::fmt::Write as _;

/// Cluster + namespace pair an AppProject may target.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AppProjectDestination {
    /// Cluster server URL (e.g. `https://kubernetes.default.svc`).
    pub server: String,
    /// Namespace the project may deploy into. Use `"*"` to allow any.
    pub namespace: String,
}

/// `AppProject` declaration.
///
/// Becomes one `argoproj.io/v1alpha1 AppProject` CR scoped to a team.
#[derive(Debug, Clone)]
pub struct AppProjectContract {
    /// Project name. Used as `metadata.name`. Lowercase, RFC-1123-ish.
    pub name: String,
    /// ArgoCD namespace (where the CR lives). Usually `argocd`.
    pub argocd_namespace: String,
    /// Human-readable description. Becomes `spec.description`.
    pub description: String,
    /// Source repositories permitted for Applications in this project.
    /// Use `["*"]` for unrestricted (not recommended for enterprise).
    pub source_repos: Vec<String>,
    /// Cluster/namespace pairs permitted for Applications in this project.
    pub destinations: Vec<AppProjectDestination>,
    /// Cluster-scoped resource kinds that Applications may create.
    /// Format: `["<group>:<kind>"]` e.g. `["kafka.strimzi.io:KafkaTopic"]`.
    /// Use `["*:*"]` for unrestricted.
    pub cluster_resource_allow: Vec<String>,
    /// Namespaced resource kinds that Applications may create.
    /// Same format as `cluster_resource_allow`.
    pub namespace_resource_allow: Vec<String>,
}

impl Default for AppProjectContract {
    fn default() -> Self {
        Self {
            name: String::new(),
            argocd_namespace: "argocd".into(),
            description: String::new(),
            source_repos: vec!["*".into()],
            destinations: vec![AppProjectDestination {
                server: "https://kubernetes.default.svc".into(),
                namespace: "*".into(),
            }],
            cluster_resource_allow: vec!["*:*".into()],
            namespace_resource_allow: vec!["*:*".into()],
        }
    }
}

/// Generate an `AppProject` YAML manifest from a contract.
#[must_use]
pub fn generate_argocd_app_project(project: &AppProjectContract) -> String {
    let mut out = String::new();
    let _ = writeln!(out, "# AUTOGENERATED — do not edit by hand.");
    let _ = writeln!(
        out,
        "# Generated by hyperi-rustlib::deployment::generate_argocd_app_project()"
    );
    let _ = writeln!(out, "apiVersion: argoproj.io/v1alpha1");
    let _ = writeln!(out, "kind: AppProject");
    let _ = writeln!(out, "metadata:");
    let _ = writeln!(out, "  name: {}", project.name);
    let _ = writeln!(out, "  namespace: {}", project.argocd_namespace);
    let _ = writeln!(out, "spec:");
    let _ = writeln!(
        out,
        "  description: {}",
        quote_if_needed(&project.description)
    );
    let _ = writeln!(out, "  sourceRepos:");
    for repo in &project.source_repos {
        let _ = writeln!(out, "    - {repo}");
    }
    let _ = writeln!(out, "  destinations:");
    for dest in &project.destinations {
        let _ = writeln!(out, "    - server: {}", dest.server);
        let _ = writeln!(out, "      namespace: {}", dest.namespace);
    }
    let _ = writeln!(out, "  clusterResourceWhitelist:");
    for entry in &project.cluster_resource_allow {
        let (group, kind) = split_resource(entry);
        let _ = writeln!(out, "    - group: {group}");
        let _ = writeln!(out, "      kind: {kind}");
    }
    let _ = writeln!(out, "  namespaceResourceWhitelist:");
    for entry in &project.namespace_resource_allow {
        let (group, kind) = split_resource(entry);
        let _ = writeln!(out, "    - group: {group}");
        let _ = writeln!(out, "      kind: {kind}");
    }
    out
}

fn split_resource(entry: &str) -> (&str, &str) {
    entry.split_once(':').unwrap_or(("*", "*"))
}

fn quote_if_needed(s: &str) -> String {
    // Quote if contains special YAML characters or is empty
    if s.contains(':') || s.contains('#') || s.is_empty() {
        format!("\"{s}\"")
    } else {
        s.to_string()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn sample_project() -> AppProjectContract {
        AppProjectContract {
            name: "hyperi-platform".into(),
            description: "HyperI platform team".into(),
            source_repos: vec![
                "https://github.com/hyperi-io/gitops".into(),
                "oci://ghcr.io/hyperi-io/helm-charts".into(),
            ],
            destinations: vec![AppProjectDestination {
                server: "https://kubernetes.default.svc".into(),
                namespace: "hyperi-dfe".into(),
            }],
            cluster_resource_allow: vec!["kafka.strimzi.io:KafkaTopic".into()],
            namespace_resource_allow: vec!["*:*".into()],
            ..Default::default()
        }
    }

    #[test]
    fn generate_produces_appproject_yaml() {
        let yaml = generate_argocd_app_project(&sample_project());
        assert!(yaml.contains("kind: AppProject"));
        assert!(yaml.contains("name: hyperi-platform"));
    }

    #[test]
    fn includes_source_repos() {
        let yaml = generate_argocd_app_project(&sample_project());
        assert!(yaml.contains("https://github.com/hyperi-io/gitops"));
        assert!(yaml.contains("oci://ghcr.io/hyperi-io/helm-charts"));
    }

    #[test]
    fn includes_destinations() {
        let yaml = generate_argocd_app_project(&sample_project());
        assert!(yaml.contains("server: https://kubernetes.default.svc"));
        assert!(yaml.contains("namespace: hyperi-dfe"));
    }

    #[test]
    fn cluster_resource_allow_splits_group_kind() {
        let yaml = generate_argocd_app_project(&sample_project());
        assert!(yaml.contains("group: kafka.strimzi.io"));
        assert!(yaml.contains("kind: KafkaTopic"));
    }

    #[test]
    fn star_kind_split_handles_namespace_wildcards() {
        let yaml = generate_argocd_app_project(&sample_project());
        // namespace_resource_allow = ["*:*"]
        assert!(yaml.contains("group: *"));
        assert!(yaml.contains("kind: *"));
    }

    #[test]
    fn default_contract_yields_unrestricted_project() {
        let default = AppProjectContract::default();
        assert_eq!(default.source_repos, vec!["*"]);
        assert_eq!(default.cluster_resource_allow, vec!["*:*"]);
    }

    #[test]
    fn description_with_colon_is_quoted() {
        let project = AppProjectContract {
            name: "test".into(),
            description: "Team: platform".into(),
            ..Default::default()
        };
        let yaml = generate_argocd_app_project(&project);
        assert!(yaml.contains("description: \"Team: platform\""));
    }

    #[test]
    fn empty_description_is_quoted() {
        let project = AppProjectContract {
            name: "test".into(),
            description: String::new(),
            ..Default::default()
        };
        let yaml = generate_argocd_app_project(&project);
        assert!(yaml.contains("description: \"\""));
    }

    #[test]
    fn multiple_destinations_all_present() {
        let project = AppProjectContract {
            name: "multi".into(),
            destinations: vec![
                AppProjectDestination {
                    server: "https://prod.k8s".into(),
                    namespace: "prod".into(),
                },
                AppProjectDestination {
                    server: "https://staging.k8s".into(),
                    namespace: "staging".into(),
                },
            ],
            ..Default::default()
        };
        let yaml = generate_argocd_app_project(&project);
        assert!(yaml.contains("server: https://prod.k8s"));
        assert!(yaml.contains("namespace: prod"));
        assert!(yaml.contains("server: https://staging.k8s"));
        assert!(yaml.contains("namespace: staging"));
    }
}