cargo-athena-api 0.5.2

Argo Workflows API types — a hand-owned, curated serde subset
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
//! Argo Workflows API types — a hand-owned, curated subset.
//!
//! We only emit a narrow, stable slice of Argo (WorkflowTemplate/Workflow;
//! templates with container/dag/steps; artifacts/volumes/params/
//! nodeSelector/SA). These are plain `serde` structs (no protobuf/prost):
//! the IDL bought us nothing here, and conformance is guarded empirically
//! by the kind e2e (`scripts/e2e-test.sh`) running against a real Argo.
//!
//! Serialization rules (so the emitted YAML is Argo-correct):
//! every struct is `rename_all = "camelCase"`, every field is
//! `skip_serializing_if = "ser::skip"` (omit empties) + `default` (for
//! round-trip deserialization).

/// `skip_serializing_if` support: one generic "is this empty?" predicate
/// so every field can share `#[serde(skip_serializing_if = "ser::skip")]`.
pub mod ser {
    use std::collections::{BTreeMap, HashMap};

    /// True when a value is "empty" and should be omitted from output.
    pub trait Skip {
        fn skip(&self) -> bool;
    }

    impl Skip for String {
        fn skip(&self) -> bool {
            self.is_empty()
        }
    }
    impl Skip for bool {
        fn skip(&self) -> bool {
            !*self
        }
    }
    impl Skip for i32 {
        fn skip(&self) -> bool {
            *self == 0
        }
    }
    impl<T> Skip for Option<T> {
        fn skip(&self) -> bool {
            self.is_none()
        }
    }
    impl<T> Skip for Vec<T> {
        fn skip(&self) -> bool {
            self.is_empty()
        }
    }
    impl<K, V> Skip for HashMap<K, V> {
        fn skip(&self) -> bool {
            self.is_empty()
        }
    }
    impl<K, V> Skip for BTreeMap<K, V> {
        fn skip(&self) -> bool {
            self.is_empty()
        }
    }

    /// The function named in every field's `skip_serializing_if`.
    pub fn skip<T: Skip>(value: &T) -> bool {
        value.skip()
    }
}

use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

/// `#[derive]` + `serde` boilerplate shared by every message, and a
/// `skip`/`default` field attribute on each field.
macro_rules! argo {
    ($(
        $(#[$m:meta])*
        pub struct $name:ident { $(
            $(#[$fm:meta])*
            pub $fld:ident : $ty:ty
        ),* $(,)? }
    )*) => {$(
        $(#[$m])*
        #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
        #[serde(rename_all = "camelCase")]
        pub struct $name {
            $(
                $(#[$fm])*
                #[serde(default, skip_serializing_if = "crate::ser::skip")]
                pub $fld : $ty,
            )*
        }
    )*};
}

argo! {
    pub struct Workflow {
        pub api_version: String,
        pub kind: String,
        pub metadata: Option<ObjectMeta>,
        pub spec: Option<WorkflowSpec>,
    }

    /// A reusable, independently-addressable template resource. Every
    /// `#[workflow]`/`#[container]` emits one; cross-template calls
    /// reference it by name via `TemplateRef`.
    pub struct WorkflowTemplate {
        pub api_version: String,
        pub kind: String,
        pub metadata: Option<ObjectMeta>,
        pub spec: Option<WorkflowSpec>,
    }

    pub struct ObjectMeta {
        pub name: String,
        pub generate_name: String,
        pub namespace: String,
        pub labels: BTreeMap<String, String>,
        pub annotations: BTreeMap<String, String>,
    }

    pub struct WorkflowSpec {
        pub entrypoint: String,
        pub templates: Vec<Template>,
        pub arguments: Option<Arguments>,
        /// Set on a runnable Workflow that just invokes a WorkflowTemplate.
        pub workflow_template_ref: Option<WorkflowTemplateRef>,
        pub service_account_name: String,
        /// Root-scoped pod scheduling for the *submitted* Workflow
        /// (Argo applies it to every pod). Only `cargo athena submit
        /// --node-selector` sets this; emit never does (skip-empty ⇒
        /// existing goldens unaffected).
        pub node_selector: BTreeMap<String, String>,
        /// Whole-workflow lifecycle hooks. Key `exit` is the exit handler
        /// (runs once when the Workflow finishes). We use this (with a
        /// `templateRef`) rather than the legacy `spec.onExit` string,
        /// which only resolves a *local* template name — unusable across
        /// the one-WT-per-template wormhole.
        pub hooks: BTreeMap<String, LifecycleHook>,
        /// Workflow-scoped TTL GC (`#[…(ttl(..))]`).
        pub ttl_strategy: Option<TtlStrategy>,
        /// Workflow-scoped pod GC (`#[…(pod_gc(strategy=..))]`). camelCase
        /// of `pod_gc` is `podGc`, but Argo's field is `podGC` — the
        /// `argo!` macro forwards this explicit rename ahead of its
        /// `rename_all`, so it wins.
        #[serde(rename = "podGC")]
        pub pod_gc: Option<PodGc>,
        /// Root-scoped Argo `WorkflowSpec.activeDeadlineSeconds` — the
        /// genuine whole-workflow runtime cap, from
        /// `#[…(active_deadline_if_root=..)]`. (`int64` in Argo;
        /// camelCase of the field name already matches.) skip-empty ⇒
        /// existing goldens stay byte-identical.
        pub active_deadline_seconds: Option<i64>,
        /// Root-scoped `Synchronization` — workflow-level mutexes
        /// (`#[…(mutexes_if_root = [{ name = … }])]`). Argo's sync
        /// manager keys on `<ns>/Mutex/<name>` globally per controller,
        /// so two SEPARATE Workflow runs (not just `templateRef`'d
        /// sub-workflows in one run) contend on the same name —
        /// empirically verified on v4.0.5, holder key `<ns>/<wf>`.
        pub synchronization: Option<Synchronization>,
        /// Workflow priority (int32). The Argo controller schedules
        /// higher-priority workflows first when its parallelism limit
        /// is hit. Today only `cargo athena submit --priority` sets
        /// this; emit never does (skip-empty ⇒ existing goldens stay
        /// byte-identical). A future `#[workflow(priority_if_root=N)]`
        /// would land here too (spec-scoped, hence the `_if_root`
        /// convention).
        pub priority: Option<i32>,
    }

    /// Argo `ttlStrategy`: delete the finished Workflow after the given
    /// seconds. Each bound is independent (`#[…(ttl(after_completion=…,
    /// after_success=…, after_failure=…))]`).
    pub struct TtlStrategy {
        pub seconds_after_completion: Option<i32>,
        pub seconds_after_success: Option<i32>,
        pub seconds_after_failure: Option<i32>,
    }

    /// Argo `podGC`: when to delete the Workflow's pods.
    pub struct PodGc {
        pub strategy: String,
    }

    /// Points a runnable Workflow at a WorkflowTemplate resource.
    pub struct WorkflowTemplateRef {
        pub name: String,
        pub cluster_scope: bool,
    }

    /// A DAG task's reference to a template in another WorkflowTemplate.
    pub struct TemplateRef {
        pub name: String,
        pub template: String,
        pub cluster_scope: bool,
    }

    pub struct Template {
        pub name: String,
        /// Argo `Template.metadata` — annotations + labels on the
        /// pod/dag/steps template. Optional, skip-serialized when
        /// empty, so existing goldens stay byte-identical for
        /// templates that don't use the `annotations = {…}` attr.
        pub metadata: Option<ObjectMeta>,
        pub inputs: Option<Inputs>,
        pub outputs: Option<Outputs>,
        // Exactly one of the following describes the template body.
        pub container: Option<Container>,
        pub dag: Option<DagTemplate>,
        pub script: Option<ScriptTemplate>,
        pub volumes: Vec<Volume>,
        /// Per-template SA override (Argo runs the pod as this).
        pub service_account_name: String,
        /// Template-level pod scheduling (container templates).
        pub node_selector: BTreeMap<String, String>,
        /// `#[workflow(steps)]` body: Argo `steps` is a list of lists —
        /// inner runs in parallel, outer sequentially. Plain serde nests
        /// `Vec<Vec<_>>` natively (no proto wrapper needed).
        pub steps: Vec<Vec<DagTask>>,
        /// Template-level retry policy (`#[container/workflow(retry(..))]`).
        pub retry_strategy: Option<RetryStrategy>,
        /// Template-level timeout duration (`#[…(timeout = "5m")]`).
        pub timeout: String,
        /// Template-level deadline (`#[…(active_deadline = …)]`) →
        /// Argo `Template.activeDeadlineSeconds` (per-pod; applies even
        /// when this template is `templateRef`'d — NOT root-only).
        pub active_deadline_seconds: Option<i32>,
        /// Template-level `Synchronization` — per-step mutexes
        /// (`#[…(mutexes = [{ name = … }])]`). Holder key is
        /// `<ns>/<wf>/<node>`, so within ONE run two nodes
        /// referencing the same template-level mutex serialize, AND
        /// nodes across separate runs (same name + ns) also serialize.
        /// Both `inputs.parameters` and `workflow.parameters`
        /// substitution resolve at this scope (no nodeSelector-style
        /// boundary-copy footgun — proven v4.0.5 2026-05-25).
        pub synchronization: Option<Synchronization>,
    }

    /// Argo `Synchronization`: workflow- or template-scoped mutex /
    /// semaphore registry. We surface mutexes only (semaphores TBD);
    /// `database` (Argo's per-cluster mutex DB toggle) is deferred.
    pub struct Synchronization {
        pub mutexes: Vec<Mutex>,
    }

    /// Argo `Mutex`: a named lock. `namespace` defaults to the
    /// workflow's namespace if empty (per
    /// `workflow/sync/lock_name.go:58-67`); set it to coordinate
    /// across namespaces (lock key becomes `<namespace>/Mutex/<name>`).
    pub struct Mutex {
        pub name: String,
        pub namespace: String,
    }

    /// Argo `retryStrategy`: re-run the template on failure. Nil `limit`
    /// == unlimited; `retry_policy` empty == Argo default (`OnFailure`).
    pub struct RetryStrategy {
        pub limit: Option<i32>,
        pub retry_policy: String,
        pub backoff: Option<Backoff>,
    }

    /// Exponential back-off between retries.
    pub struct Backoff {
        pub duration: String,
        pub factor: Option<i32>,
        pub max_duration: String,
    }

    pub struct Inputs {
        pub parameters: Vec<Parameter>,
        pub artifacts: Vec<Artifact>,
    }

    pub struct Outputs {
        pub parameters: Vec<Parameter>,
        pub artifacts: Vec<Artifact>,
    }

    pub struct Parameter {
        pub name: String,
        pub value: Option<String>,
        pub default: Option<String>,
        pub value_from: Option<ValueFrom>,
    }

    pub struct ValueFrom {
        pub path: String,
        pub parameter: String,
        /// An Argo expr (`expr-lang`) evaluated after the DAG/steps
        /// finish. Used by a synthesized `if` wrapper to select the
        /// taken branch's `return` (skip-serialized so unaffected
        /// templates stay byte-identical).
        pub expression: String,
    }

    pub struct Artifact {
        pub name: String,
        pub path: String,
        /// Where the artifact lives (binary tarball / load-save ports).
        pub s3: Option<S3Artifact>,
        /// `none` => deliver the raw object; bootstrap untars itself.
        pub archive: Option<ArchiveStrategy>,
        /// Octal file mode applied to the downloaded file.
        pub mode: Option<i32>,
        /// DAG-wired artifact reference: `from: "{{tasks.<dep>.outputs
        /// .artifacts.return}}"` on an `arguments.artifacts[]` slot (or
        /// the equivalent bubble on a sub-workflow's own
        /// `outputs.artifacts[]`). Skip-empty so existing artifact
        /// emissions (binary tarball / `save_artifact!` ports) stay
        /// byte-identical.
        pub from: String,
    }

    /// Mirrors a k8s SecretKeySelector — a key in a Secret. `optional`
    /// is K8s's "don't fail pod-start if missing" flag, surfaced by
    /// `cargo_athena::secret_opt!` (skip-serialized when false, so
    /// existing S3Artifact users stay byte-identical).
    pub struct SecretKeySelector {
        pub name: String,
        pub key: String,
        pub optional: bool,
    }

    /// Mirrors Argo's S3Artifact.
    pub struct S3Artifact {
        pub endpoint: String,
        pub bucket: String,
        pub region: String,
        pub insecure: bool,
        pub key: String,
        pub access_key_secret: Option<SecretKeySelector>,
        pub secret_key_secret: Option<SecretKeySelector>,
    }

    pub struct ArchiveStrategy {
        /// Present (and empty) means "do not archive/extract".
        pub none: Option<NoneStrategy>,
    }

    pub struct NoneStrategy {}

    pub struct Arguments {
        pub parameters: Vec<Parameter>,
        pub artifacts: Vec<Artifact>,
    }

    pub struct DagTemplate {
        pub tasks: Vec<DagTask>,
    }

    pub struct DagTask {
        pub name: String,
        /// Empty when `template_ref` is set.
        pub template: String,
        pub dependencies: Vec<String>,
        pub arguments: Option<Arguments>,
        pub template_ref: Option<TemplateRef>,
        // Declared last + skip-if-empty so tasks that use neither leave
        // every existing golden byte-identical.
        pub continue_on: Option<ContinueOn>,
        /// Argo lifecycle hooks: arbitrary key -> hook. Key `exit` is the
        /// special unconditional on-completion hook; others fire when
        /// their `expression` holds.
        pub hooks: BTreeMap<String, LifecycleHook>,
        /// Fan-out: a JSON-array string; the task runs once per element
        /// with `{{item}}` bound. Empty == no fan-out (skip-serialized).
        pub with_param: String,
        /// Conditional execution: an Argo expr (`expr-lang`). The task
        /// runs only when it evaluates truthy; else it is Skipped. Empty
        /// == unconditional (skip-serialized so existing goldens are
        /// byte-identical).
        pub when: String,
    }

    /// Proceed to dependents even if this task fails/errors.
    pub struct ContinueOn {
        pub error: bool,
        pub failed: bool,
    }

    /// A hook that runs a template on a lifecycle event. `expression`
    /// empty == the special `exit` hook (runs on completion).
    pub struct LifecycleHook {
        pub template_ref: Option<TemplateRef>,
        pub arguments: Option<Arguments>,
        pub expression: String,
    }

    pub struct Container {
        pub image: String,
        pub command: Vec<String>,
        pub args: Vec<String>,
        pub env: Vec<EnvVar>,
        pub volume_mounts: Vec<VolumeMount>,
        pub working_dir: String,
        /// K8s `securityContext` on this container. Only `privileged`
        /// is exposed today (`#[container(privileged = true)]`); other
        /// fields can join when there's a real use case. Skip-empty
        /// keeps existing goldens byte-identical.
        pub security_context: Option<SecurityContext>,
    }

    /// K8s `SecurityContext` on a container. Minimal: only the fields
    /// we expose. Each field skip-serializes its default so the
    /// produced YAML stays terse.
    pub struct SecurityContext {
        pub privileged: bool,
    }

    pub struct ScriptTemplate {
        pub image: String,
        pub command: Vec<String>,
        pub source: String,
    }

    pub struct EnvVar {
        pub name: String,
        pub value: String,
        /// Pulled from a `valueFrom` source instead of a literal. Used
        /// by `cargo_athena::secret!`/`secret_opt!` (secretKeyRef).
        pub value_from: Option<EnvVarSource>,
    }

    /// Argo `EnvVarSource`. Only `secretKeyRef` is exposed today; this
    /// can grow as we surface more (configMapKeyRef, fieldRef, …).
    pub struct EnvVarSource {
        pub secret_key_ref: Option<SecretKeySelector>,
    }

    pub struct Volume {
        pub name: String,
        pub host_path: Option<HostPathVolumeSource>,
        pub empty_dir: Option<EmptyDirVolumeSource>,
    }

    pub struct HostPathVolumeSource {
        pub path: String,
        pub r#type: String,
    }

    /// Present (and empty) => a pod-scoped scratch dir (`emptyDir: {}`).
    pub struct EmptyDirVolumeSource {}

    pub struct VolumeMount {
        pub name: String,
        pub mount_path: String,
        pub read_only: bool,
    }
}

/// Argo's `apiVersion` for `Workflow`/`WorkflowTemplate` resources.
pub const API_VERSION: &str = "argoproj.io/v1alpha1";
/// Argo's `kind` for `Workflow` resources.
pub const KIND_WORKFLOW: &str = "Workflow";
/// Argo's `kind` for `WorkflowTemplate` resources.
pub const KIND_WORKFLOW_TEMPLATE: &str = "WorkflowTemplate";