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}