Skip to main content

hyperi_rustlib/deployment/generate/
argocd.rs

1// Project:   hyperi-rustlib
2// File:      src/deployment/generate/argocd.rs
3// Purpose:   ArgoCD Application generation
4// Language:  Rust
5//
6// License:   BUSL-1.1
7// Copyright: (c) 2026 HYPERI PTY LIMITED
8
9#![allow(clippy::format_push_string)]
10
11use crate::deployment::contract::DeploymentContract;
12
13// ============================================================================
14// ArgoCD Application
15// ============================================================================
16
17/// Configuration for ArgoCD `Application` generation.
18///
19/// All fields have sensible defaults. The Helm chart that the Application
20/// points at is the one [`generate_chart`](crate::deployment::generate_chart)
21/// writes to a repo path.
22#[derive(Debug, Clone)]
23pub struct ArgocdConfig {
24    /// ArgoCD namespace (where the Application CR lives). Default: `argocd`.
25    pub argocd_namespace: String,
26    /// Destination namespace for the deployed app. Default: `dfe`.
27    pub dest_namespace: String,
28    /// Destination cluster (`server` field). Default: `https://kubernetes.default.svc`.
29    pub dest_server: String,
30    /// Source git repo URL (where the Helm chart lives). Required.
31    pub repo_url: String,
32    /// Source git revision (branch/tag). Default: `main`.
33    pub target_revision: String,
34    /// Path within the repo to the Helm chart. Default: `chart`.
35    pub chart_path: String,
36    /// ArgoCD project. Default: `default`.
37    pub project: String,
38    /// Sync wave (lower runs first). Default: [`crate::deployment::WAVE_APPS`].
39    pub sync_wave: i32,
40    /// Additional `ignoreDifferences` entries appended to the canonical
41    /// defaults (HPA replicas, ClusterIP, webhook caBundle). Each entry
42    /// is a raw YAML fragment matching the ArgoCD `ignoreDifferences` item
43    /// shape. The fragment must start with `- group:` and use two-space
44    /// indentation internally (the generator indents each line by four
45    /// spaces to nest under `ignoreDifferences:`).
46    ///
47    /// Example:
48    /// ```text
49    /// "- group: apps\n  kind: Deployment\n  jsonPointers:\n    - /spec/template/spec/containers/0/image"
50    /// ```
51    ///
52    /// Defaults to empty (no extra entries beyond the canonical defaults).
53    pub extra_ignore_differences: Vec<String>,
54}
55
56impl Default for ArgocdConfig {
57    fn default() -> Self {
58        Self {
59            argocd_namespace: "argocd".into(),
60            dest_namespace: "dfe".into(),
61            dest_server: "https://kubernetes.default.svc".into(),
62            repo_url: String::new(),
63            target_revision: "main".into(),
64            chart_path: "chart".into(),
65            project: "default".into(),
66            sync_wave: crate::deployment::WAVE_APPS,
67            extra_ignore_differences: Vec::new(),
68        }
69    }
70}
71
72/// Generate an ArgoCD `Application` custom-resource YAML from the deployment
73/// contract.
74///
75/// The CR points at a Helm chart in a git repo (typically the chart that
76/// [`generate_chart`](crate::deployment::generate_chart) produces). Apply with:
77///
78/// ```bash
79/// kubectl apply -n argocd -f application.yaml
80/// ```
81///
82/// # Example
83///
84/// ```rust,no_run
85/// use hyperi_rustlib::deployment::{ArgocdConfig, generate_argocd_application};
86/// # use hyperi_rustlib::deployment::DeploymentContract;
87/// # let contract: DeploymentContract = unimplemented!();
88/// let argo = ArgocdConfig {
89///     repo_url: "https://github.com/hyperi-io/dfe-loader".into(),
90///     ..Default::default()
91/// };
92/// let yaml = generate_argocd_application(&contract, &argo, None);
93/// ```
94#[must_use]
95pub fn generate_argocd_application(
96    contract: &DeploymentContract,
97    argo: &ArgocdConfig,
98    identity: Option<&crate::deployment::ContractIdentity>,
99) -> String {
100    // Contract Identity Annotation Scheme v1 -- three extra annotations
101    // alongside the existing sync-wave entry. Indented to match the
102    // 4-space `metadata.annotations:` block below.
103    let identity_block = identity
104        .map(|id| format!("\n{ann}", ann = id.as_yaml_annotations(4)))
105        .unwrap_or_default();
106
107    // Build the extras block: each entry is a raw YAML fragment starting with
108    // `- group: ...`. Indent every line by 4 spaces to nest under
109    // `ignoreDifferences:`.
110    let extras_block = if argo.extra_ignore_differences.is_empty() {
111        String::new()
112    } else {
113        let mut buf = String::new();
114        for entry in &argo.extra_ignore_differences {
115            for line in entry.lines() {
116                buf.push_str("    ");
117                buf.push_str(line);
118                buf.push('\n');
119            }
120        }
121        buf
122    };
123
124    format!(
125        r#"# AUTOGENERATED -- do not edit by hand.
126# Generated by hyperi-rustlib::deployment::generate_argocd_application()
127# Schema version: {schema_version}
128# Source contract: {app_name}::deployment::contract()
129# Regenerate with: `{binary} emit-argocd > application.yaml`
130apiVersion: argoproj.io/v1alpha1
131kind: Application
132metadata:
133  name: {app_name}
134  namespace: {argocd_namespace}
135  annotations:
136    argocd.argoproj.io/sync-wave: "{sync_wave}"{identity_block}
137  finalizers:
138    - resources-finalizer.argocd.argoproj.io
139spec:
140  project: {project}
141
142  source:
143    repoURL: {repo_url}
144    targetRevision: {target_revision}
145    path: {chart_path}
146    helm:
147      releaseName: {app_name}
148
149  destination:
150    server: {dest_server}
151    namespace: {dest_namespace}
152
153  syncPolicy:
154    automated:
155      prune: true
156      selfHeal: true
157      allowEmpty: false
158    syncOptions:
159      - CreateNamespace=true
160      - PrunePropagationPolicy=foreground
161      - PruneLast=true
162      - ServerSideApply=true
163    retry:
164      limit: 5
165      backoff:
166        duration: 5s
167        factor: 2
168        maxDuration: 3m
169
170  ignoreDifferences:
171    - group: apps
172      kind: Deployment
173      jsonPointers:
174        - /spec/replicas
175    - group: ""
176      kind: Service
177      jsonPointers:
178        - /spec/clusterIP
179        - /spec/clusterIPs
180    - group: admissionregistration.k8s.io
181      kind: ValidatingWebhookConfiguration
182      jqPathExpressions:
183        - .webhooks[].clientConfig.caBundle
184{extras_block}"#,
185        schema_version = contract.schema_version,
186        app_name = contract.app_name,
187        binary = contract.binary(),
188        argocd_namespace = argo.argocd_namespace,
189        sync_wave = argo.sync_wave,
190        project = argo.project,
191        repo_url = argo.repo_url,
192        target_revision = argo.target_revision,
193        chart_path = argo.chart_path,
194        dest_server = argo.dest_server,
195        dest_namespace = argo.dest_namespace,
196        extras_block = extras_block,
197        identity_block = identity_block,
198    )
199}