Skip to main content

cargo_athena_api/
lib.rs

1//! Argo Workflows API types — a hand-owned, curated subset.
2//!
3//! We only emit a narrow, stable slice of Argo (WorkflowTemplate/Workflow;
4//! templates with container/dag/steps; artifacts/volumes/params/
5//! nodeSelector/SA). These are plain `serde` structs (no protobuf/prost):
6//! the IDL bought us nothing here, and conformance is guarded empirically
7//! by the kind e2e (`scripts/e2e-test.sh`) running against a real Argo.
8//!
9//! Serialization rules (so the emitted YAML is Argo-correct):
10//! every struct is `rename_all = "camelCase"`, every field is
11//! `skip_serializing_if = "ser::skip"` (omit empties) + `default` (for
12//! round-trip deserialization).
13//!
14//! Also re-exports the [`munge`] module of pure name/path/env-var
15//! derivers shared between `cargo-athena-core` (emit side) and
16//! `cargo-athena-macros` (proc-macro / user-build side). Living here
17//! lets the proc-macro crate depend on `api` for these helpers
18//! without pulling in any runtime-only crates (kube, reqwest, etc.),
19//! so both sides compute the same string for the same input by
20//! construction rather than via mirrored copies + pin tests.
21
22pub mod munge;
23
24/// `skip_serializing_if` support: one generic "is this empty?" predicate
25/// so every field can share `#[serde(skip_serializing_if = "ser::skip")]`.
26pub mod ser {
27    use std::collections::BTreeMap;
28
29    /// True when a value is "empty" and should be omitted from output.
30    /// Impls cover exactly the field types the `argo!` structs use.
31    pub trait Skip {
32        fn skip(&self) -> bool;
33    }
34
35    impl Skip for String {
36        fn skip(&self) -> bool {
37            self.is_empty()
38        }
39    }
40    impl Skip for bool {
41        fn skip(&self) -> bool {
42            !*self
43        }
44    }
45    impl<T> Skip for Option<T> {
46        fn skip(&self) -> bool {
47            self.is_none()
48        }
49    }
50    impl<T> Skip for Vec<T> {
51        fn skip(&self) -> bool {
52            self.is_empty()
53        }
54    }
55    impl<K, V> Skip for BTreeMap<K, V> {
56        fn skip(&self) -> bool {
57            self.is_empty()
58        }
59    }
60    impl Skip for serde_norway::Value {
61        fn skip(&self) -> bool {
62            matches!(self, serde_norway::Value::Null)
63        }
64    }
65
66    /// The function named in every field's `skip_serializing_if`.
67    pub fn skip<T: Skip>(value: &T) -> bool {
68        value.skip()
69    }
70}
71
72use serde::{Deserialize, Serialize};
73use std::collections::BTreeMap;
74
75/// `#[derive]` + `serde` boilerplate shared by every message, and a
76/// `skip`/`default` field attribute on each field.
77macro_rules! argo {
78    ($(
79        $(#[$m:meta])*
80        pub struct $name:ident { $(
81            $(#[$fm:meta])*
82            pub $fld:ident : $ty:ty
83        ),* $(,)? }
84    )*) => {$(
85        $(#[$m])*
86        #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
87        #[serde(rename_all = "camelCase")]
88        pub struct $name {
89            $(
90                $(#[$fm])*
91                #[serde(default, skip_serializing_if = "crate::ser::skip")]
92                pub $fld : $ty,
93            )*
94        }
95    )*};
96}
97
98argo! {
99    pub struct Workflow {
100        pub api_version: String,
101        pub kind: String,
102        pub metadata: Option<ObjectMeta>,
103        pub spec: Option<WorkflowSpec>,
104    }
105
106    /// A reusable, independently-addressable template resource. Every
107    /// `#[workflow]`/`#[container]` emits one; cross-template calls
108    /// reference it by name via `TemplateRef`.
109    pub struct WorkflowTemplate {
110        pub api_version: String,
111        pub kind: String,
112        pub metadata: Option<ObjectMeta>,
113        pub spec: Option<WorkflowSpec>,
114    }
115
116    pub struct ObjectMeta {
117        pub name: String,
118        pub generate_name: String,
119        pub namespace: String,
120        pub labels: BTreeMap<String, String>,
121        pub annotations: BTreeMap<String, String>,
122    }
123
124    pub struct WorkflowSpec {
125        pub entrypoint: String,
126        pub templates: Vec<Template>,
127        pub arguments: Option<Arguments>,
128        /// Set on a runnable Workflow that just invokes a WorkflowTemplate.
129        pub workflow_template_ref: Option<WorkflowTemplateRef>,
130        pub service_account_name: String,
131        /// Root-scoped pod scheduling for the *submitted* Workflow
132        /// (Argo applies it to every pod). Only `cargo athena submit
133        /// --node-selector` sets this; emit never does (skip-empty ⇒
134        /// existing goldens unaffected).
135        pub node_selector: BTreeMap<String, String>,
136        /// Whole-workflow lifecycle hooks. Key `exit` is the exit handler
137        /// (runs once when the Workflow finishes). We use this (with a
138        /// `templateRef`) rather than the legacy `spec.onExit` string,
139        /// which only resolves a *local* template name — unusable across
140        /// the one-WT-per-template wormhole.
141        pub hooks: BTreeMap<String, LifecycleHook>,
142        /// Workflow-scoped TTL GC (`#[…(ttl(..))]`).
143        pub ttl_strategy: Option<TtlStrategy>,
144        /// Workflow-scoped pod GC (`#[…(pod_gc(strategy=..))]`). camelCase
145        /// of `pod_gc` is `podGc`, but Argo's field is `podGC` — the
146        /// `argo!` macro forwards this explicit rename ahead of its
147        /// `rename_all`, so it wins.
148        #[serde(rename = "podGC")]
149        pub pod_gc: Option<PodGc>,
150        /// Root-scoped Argo `WorkflowSpec.activeDeadlineSeconds` — the
151        /// genuine whole-workflow runtime cap, from
152        /// `#[…(active_deadline_if_root=..)]`. (`int64` in Argo;
153        /// camelCase of the field name already matches.) skip-empty ⇒
154        /// existing goldens stay byte-identical.
155        pub active_deadline_seconds: Option<i64>,
156        /// Root-scoped `Synchronization` — workflow-level mutexes
157        /// (`#[…(mutexes_if_root = [{ name = … }])]`). Argo's sync
158        /// manager keys on `<ns>/Mutex/<name>` globally per controller,
159        /// so two SEPARATE Workflow runs (not just `templateRef`'d
160        /// sub-workflows in one run) contend on the same name —
161        /// empirically verified on v4.0.5, holder key `<ns>/<wf>`.
162        pub synchronization: Option<Synchronization>,
163        /// Workflow priority (int32). The Argo controller schedules
164        /// higher-priority workflows first when its parallelism limit
165        /// is hit. Today only `cargo athena submit --priority` sets
166        /// this; emit never does (skip-empty ⇒ existing goldens stay
167        /// byte-identical). A future `#[workflow(priority_if_root=N)]`
168        /// would land here too (spec-scoped, hence the `_if_root`
169        /// convention).
170        pub priority: Option<i32>,
171        /// Root-scoped tolerations (3rd tier of Argo's `tmpl → boundary
172        /// → wfSpec` pod-scheduling lookup). From
173        /// `#[…(tolerations_if_root = [...])]`. Applies to every pod
174        /// in the run that doesn't have its own template- or
175        /// boundary-level override (same cascade as `node_selector`).
176        pub tolerations: Vec<Toleration>,
177        /// Root-scoped pod affinity (3rd tier). Opaque YAML/JSON value
178        /// from `#[…(affinity_if_root = "...")]`; athena does NOT model
179        /// the deeply-nested `apiv1.Affinity` schema by design.
180        pub affinity: Option<serde_norway::Value>,
181        /// Root-scoped strategic-merge patch applied to every pod in
182        /// the run after Argo renders the podSpec. From
183        /// `#[workflow(pod_spec_patch_if_root = "...")]`. Substituted at
184        /// pod-creation time (`workflow/controller/workflowpod.go:89`
185        /// `processPodSpecPatch` → `ProcessArgs`) so
186        /// `{{workflow.parameters.X}}` resolves; per-template
187        /// `Template.pod_spec_patch` is concat'd onto this one. The
188        /// universal escape hatch for any podSpec field athena hasn't
189        /// lifted to a first-class attr.
190        pub pod_spec_patch: Option<String>,
191        /// Root-only `WorkflowSpec.ImagePullSecrets` — Secret references
192        /// the kubelet uses to authenticate against private image
193        /// registries when pulling each pod's image. From
194        /// `#[…(image_pull_secrets_if_root = ["regcred", ...])]`.
195        /// K8s/Argo expose this only at workflow scope (no
196        /// per-template knob); per-container needs go through
197        /// `pod_spec_patch`. skip-empty ⇒ existing goldens stay
198        /// byte-identical.
199        pub image_pull_secrets: Vec<LocalObjectReference>,
200        /// Root-scoped pod-concurrency cap. From
201        /// `#[workflow(parallelism_if_root = N)]`. Argo's CRD enforces
202        /// `+kubebuilder:validation:Minimum=1` so the macro rejects
203        /// `<= 0` at compile time (would otherwise be rejected by the
204        /// API server at submit anyway).
205        pub parallelism: Option<i64>,
206        /// PVCs Argo creates per workflow run and deletes when it
207        /// finishes. One entry per `#[ephemeral_pvc]` type reachable
208        /// from the submitted root. Each pod mounts them via
209        /// `claimName: <metadata.name>`. Root-only by Argo's design
210        /// (only the submitted Workflow's spec is honored).
211        pub volume_claim_templates: Vec<PersistentVolumeClaim>,
212    }
213
214    /// K8s `Toleration`: tolerate a node taint. Operator ∈
215    /// `"Equal" | "Exists"`; effect ∈ `"NoSchedule" | "PreferNoSchedule"
216    /// | "NoExecute"`. `value` is required only when operator is
217    /// `Equal`; `toleration_seconds` only meaningful for `NoExecute`.
218    pub struct Toleration {
219        pub key: String,
220        pub operator: String,
221        pub value: String,
222        pub effect: String,
223        pub toleration_seconds: Option<i64>,
224    }
225
226    /// K8s `LocalObjectReference` — just a Secret name in the
227    /// workflow's own namespace. Argo's `ImagePullSecrets` is
228    /// `[]LocalObjectReference`.
229    pub struct LocalObjectReference {
230        pub name: String,
231    }
232
233    /// Argo `ttlStrategy`: delete the finished Workflow after the given
234    /// seconds. Each bound is independent (`#[…(ttl(after_completion=…,
235    /// after_success=…, after_failure=…))]`).
236    pub struct TtlStrategy {
237        pub seconds_after_completion: Option<i32>,
238        pub seconds_after_success: Option<i32>,
239        pub seconds_after_failure: Option<i32>,
240    }
241
242    /// Argo `podGC`: when to delete the Workflow's pods.
243    pub struct PodGc {
244        pub strategy: String,
245    }
246
247    /// Points a runnable Workflow at a WorkflowTemplate resource.
248    pub struct WorkflowTemplateRef {
249        pub name: String,
250        pub cluster_scope: bool,
251    }
252
253    /// A DAG task's reference to a template in another WorkflowTemplate.
254    pub struct TemplateRef {
255        pub name: String,
256        pub template: String,
257        pub cluster_scope: bool,
258    }
259
260    pub struct Template {
261        pub name: String,
262        /// Argo `Template.metadata` — annotations + labels on the
263        /// pod/dag/steps template. Optional, skip-serialized when
264        /// empty, so existing goldens stay byte-identical for
265        /// templates that don't use the `annotations = {…}` attr.
266        pub metadata: Option<ObjectMeta>,
267        pub inputs: Option<Inputs>,
268        pub outputs: Option<Outputs>,
269        // Exactly one of the following describes the template body.
270        pub container: Option<Container>,
271        pub dag: Option<DagTemplate>,
272        /// `#[container(daemon)]` → Argo `Template.daemon: true`. The pod
273        /// is treated as long-running: the workflow proceeds to dependents
274        /// as soon as the container reaches READINESS (not completion), and
275        /// Argo terminates the daemon when its boundary node finishes.
276        /// Container/script templates only (no-op on dag/steps). Two Argo
277        /// gotchas the user must know: a daemon pod that exits `Succeeded`
278        /// is marked FAILED (daemons are expected to run indefinitely), and
279        /// `retryStrategy` only covers the startup phase (failures after
280        /// readiness are ignored). athena has no readinessProbe attr — use
281        /// `pod_spec_patch` to add one if Ready timing matters.
282        pub daemon: Option<bool>,
283        pub volumes: Vec<Volume>,
284        /// Per-template SA override (Argo runs the pod as this).
285        pub service_account_name: String,
286        /// Template-level pod scheduling (container templates).
287        pub node_selector: BTreeMap<String, String>,
288        /// `#[workflow(steps)]` body: Argo `steps` is a list of lists —
289        /// inner runs in parallel, outer sequentially. Plain serde nests
290        /// `Vec<Vec<_>>` natively (no proto wrapper needed).
291        pub steps: Vec<Vec<DagTask>>,
292        /// Template-level retry policy (`#[container/workflow(retry(..))]`).
293        pub retry_strategy: Option<RetryStrategy>,
294        /// Template-level timeout duration (`#[…(timeout = "5m")]`).
295        pub timeout: String,
296        /// Template-level deadline (`#[…(active_deadline = …)]`) →
297        /// Argo `Template.activeDeadlineSeconds` (per-pod; applies even
298        /// when this template is `templateRef`'d — NOT root-only).
299        pub active_deadline_seconds: Option<i32>,
300        /// Template-level `Synchronization` — per-step mutexes
301        /// (`#[…(mutexes = [{ name = … }])]`). Holder key is
302        /// `<ns>/<wf>/<node>`, so within ONE run two nodes
303        /// referencing the same template-level mutex serialize, AND
304        /// nodes across separate runs (same name + ns) also serialize.
305        /// Both `inputs.parameters` and `workflow.parameters`
306        /// substitution resolve at this scope (no nodeSelector-style
307        /// boundary-copy footgun — proven v4.0.5 2026-05-25).
308        pub synchronization: Option<Synchronization>,
309        /// Template-level tolerations on a container WT, from
310        /// `#[container(tolerations = [...])]`. Substitution at this
311        /// scope is safe for the template's own pod (the pod renders
312        /// from the substituted template); empirically verified on
313        /// v4.0.5 2026-05-26.
314        pub tolerations: Vec<Toleration>,
315        /// Template-level pod affinity on a container WT, from
316        /// `#[container(affinity = "...")]`. Opaque YAML/JSON value
317        /// (athena does NOT model `apiv1.Affinity` by design — use
318        /// `pod_spec_patch` as the all-purpose alternative).
319        pub affinity: Option<serde_norway::Value>,
320        /// Template-level strategic-merge patch applied to this
321        /// template's pod after Argo renders it. From
322        /// `#[container(pod_spec_patch = "...")]`. Substituted at
323        /// pod-creation time, so `{{inputs.parameters.X}}` resolves
324        /// (proven v4.0.5 2026-05-26 - the patch goes through the
325        /// same `processPodSpecPatch` → `ProcessArgs` pass that
326        /// renders the substituted template, so the leaf-pod inherits
327        /// the resolved value without the nodeSelector-style
328        /// boundary-copy footgun).
329        pub pod_spec_patch: Option<String>,
330        /// Template-level pod-concurrency cap on this dag/steps. From
331        /// `#[workflow(parallelism = N)]`. Caps concurrent children
332        /// scheduled under THIS template invocation only — pods
333        /// created by nested templates don't count. Argo's CRD
334        /// enforces `+kubebuilder:validation:Minimum=1`.
335        pub parallelism: Option<i64>,
336    }
337
338    /// Argo `Synchronization`: workflow- or template-scoped mutex /
339    /// semaphore registry. We surface mutexes only (semaphores TBD);
340    /// `database` (Argo's per-cluster mutex DB toggle) is deferred.
341    pub struct Synchronization {
342        pub mutexes: Vec<Mutex>,
343    }
344
345    /// Argo `Mutex`: a named lock. `namespace` defaults to the
346    /// workflow's namespace if empty (per
347    /// `workflow/sync/lock_name.go:58-67`); set it to coordinate
348    /// across namespaces (lock key becomes `<namespace>/Mutex/<name>`).
349    pub struct Mutex {
350        pub name: String,
351        pub namespace: String,
352    }
353
354    /// Argo `retryStrategy`: re-run the template on failure. Nil `limit`
355    /// == unlimited; `retry_policy` empty == Argo default (`OnFailure`).
356    pub struct RetryStrategy {
357        pub limit: Option<i32>,
358        pub retry_policy: String,
359        pub backoff: Option<Backoff>,
360    }
361
362    /// Exponential back-off between retries.
363    pub struct Backoff {
364        pub duration: String,
365        pub factor: Option<i32>,
366        pub max_duration: String,
367    }
368
369    pub struct Inputs {
370        pub parameters: Vec<Parameter>,
371        pub artifacts: Vec<Artifact>,
372    }
373
374    pub struct Outputs {
375        pub parameters: Vec<Parameter>,
376        pub artifacts: Vec<Artifact>,
377    }
378
379    pub struct Parameter {
380        pub name: String,
381        pub value: Option<String>,
382        pub default: Option<String>,
383        pub value_from: Option<ValueFrom>,
384    }
385
386    pub struct ValueFrom {
387        pub path: String,
388        pub parameter: String,
389        /// An Argo expr (`expr-lang`) evaluated after the DAG/steps
390        /// finish. Used by a synthesized `if` wrapper to select the
391        /// taken branch's `return` (skip-serialized so unaffected
392        /// templates stay byte-identical).
393        pub expression: String,
394    }
395
396    pub struct Artifact {
397        pub name: String,
398        pub path: String,
399        /// Where the artifact lives (binary tarball / load-save ports).
400        pub s3: Option<S3Artifact>,
401        /// `none` => deliver the raw object; bootstrap untars itself.
402        pub archive: Option<ArchiveStrategy>,
403        /// Octal file mode applied to the downloaded file.
404        pub mode: Option<i32>,
405        /// DAG-wired artifact reference: `from: "{{tasks.<dep>.outputs
406        /// .artifacts.return}}"` on an `arguments.artifacts[]` slot (or
407        /// the equivalent bubble on a sub-workflow's own
408        /// `outputs.artifacts[]`). Skip-empty so existing artifact
409        /// emissions (binary tarball / `save_artifact!` ports) stay
410        /// byte-identical.
411        pub from: String,
412    }
413
414    /// Mirrors a k8s SecretKeySelector — a key in a Secret. `optional`
415    /// is K8s's "don't fail pod-start if missing" flag, surfaced by
416    /// `cargo_athena::secret_opt!` (skip-serialized when false, so
417    /// existing S3Artifact users stay byte-identical).
418    pub struct SecretKeySelector {
419        pub name: String,
420        pub key: String,
421        pub optional: bool,
422    }
423
424    /// Mirrors Argo's S3Artifact.
425    pub struct S3Artifact {
426        pub endpoint: String,
427        pub bucket: String,
428        pub region: String,
429        pub insecure: bool,
430        pub key: String,
431        pub access_key_secret: Option<SecretKeySelector>,
432        pub secret_key_secret: Option<SecretKeySelector>,
433    }
434
435    pub struct ArchiveStrategy {
436        /// Present (and empty) means "do not archive/extract".
437        pub none: Option<NoneStrategy>,
438    }
439
440    pub struct NoneStrategy {}
441
442    pub struct Arguments {
443        pub parameters: Vec<Parameter>,
444        pub artifacts: Vec<Artifact>,
445    }
446
447    pub struct DagTemplate {
448        pub tasks: Vec<DagTask>,
449    }
450
451    pub struct DagTask {
452        pub name: String,
453        /// Empty when `template_ref` is set.
454        pub template: String,
455        pub dependencies: Vec<String>,
456        pub arguments: Option<Arguments>,
457        pub template_ref: Option<TemplateRef>,
458        // Declared last + skip-if-empty so tasks that use neither leave
459        // every existing golden byte-identical.
460        pub continue_on: Option<ContinueOn>,
461        /// Argo lifecycle hooks: arbitrary key -> hook. Key `exit` is the
462        /// special unconditional on-completion hook; others fire when
463        /// their `expression` holds.
464        pub hooks: BTreeMap<String, LifecycleHook>,
465        /// Fan-out: a JSON-array string; the task runs once per element
466        /// with `{{item}}` bound. Empty == no fan-out (skip-serialized).
467        pub with_param: String,
468        /// Conditional execution: an Argo expr (`expr-lang`). The task
469        /// runs only when it evaluates truthy; else it is Skipped. Empty
470        /// == unconditional (skip-serialized so existing goldens are
471        /// byte-identical).
472        pub when: String,
473    }
474
475    /// Proceed to dependents even if this task fails/errors.
476    pub struct ContinueOn {
477        pub error: bool,
478        pub failed: bool,
479    }
480
481    /// A hook that runs a template on a lifecycle event. `expression`
482    /// empty == the special `exit` hook (runs on completion).
483    pub struct LifecycleHook {
484        pub template_ref: Option<TemplateRef>,
485        pub arguments: Option<Arguments>,
486        pub expression: String,
487    }
488
489    pub struct Container {
490        pub image: String,
491        pub command: Vec<String>,
492        pub args: Vec<String>,
493        pub env: Vec<EnvVar>,
494        pub volume_mounts: Vec<VolumeMount>,
495        pub working_dir: String,
496        /// K8s `securityContext` on this container. Only `privileged`
497        /// is exposed today (`#[container(privileged = true)]`); other
498        /// fields can join when there's a real use case. Skip-empty
499        /// keeps existing goldens byte-identical.
500        pub security_context: Option<SecurityContext>,
501    }
502
503    /// K8s `SecurityContext` on a container. Minimal: only the fields
504    /// we expose. Each field skip-serializes its default so the
505    /// produced YAML stays terse.
506    pub struct SecurityContext {
507        pub privileged: bool,
508    }
509
510    pub struct EnvVar {
511        pub name: String,
512        pub value: String,
513        /// Pulled from a `valueFrom` source instead of a literal. Used
514        /// by `cargo_athena::secret!`/`secret_opt!` (secretKeyRef).
515        pub value_from: Option<EnvVarSource>,
516    }
517
518    /// Argo `EnvVarSource`. Only `secretKeyRef` is exposed today; this
519    /// can grow as we surface more (configMapKeyRef, fieldRef, …).
520    pub struct EnvVarSource {
521        pub secret_key_ref: Option<SecretKeySelector>,
522    }
523
524    pub struct Volume {
525        pub name: String,
526        pub host_path: Option<HostPathVolumeSource>,
527        pub empty_dir: Option<EmptyDirVolumeSource>,
528        /// Mount a PersistentVolumeClaim. The `claim_name` is either a
529        /// pre-existing PVC's name (`#[external_pvc]`) or the name of a
530        /// transient PVC Argo creates for the run via
531        /// `WorkflowSpec.volumeClaimTemplates[]` (`#[ephemeral_pvc]`).
532        pub persistent_volume_claim: Option<PersistentVolumeClaimVolumeSource>,
533    }
534
535    pub struct HostPathVolumeSource {
536        pub path: String,
537        pub r#type: String,
538    }
539
540    /// Present (and empty) => a pod-scoped scratch dir (`emptyDir: {}`).
541    pub struct EmptyDirVolumeSource {}
542
543    pub struct PersistentVolumeClaimVolumeSource {
544        pub claim_name: String,
545        pub read_only: bool,
546    }
547
548    pub struct VolumeMount {
549        pub name: String,
550        pub mount_path: String,
551        pub read_only: bool,
552    }
553
554    /// `WorkflowSpec.volumeClaimTemplates[]` entry: a PVC Argo
555    /// dynamically creates per workflow run (and deletes when it
556    /// finishes). Athena emits one per `#[ephemeral_pvc]` reachable
557    /// from the submitted root.
558    pub struct PersistentVolumeClaim {
559        pub metadata: Option<ObjectMeta>,
560        pub spec: Option<PersistentVolumeClaimSpec>,
561    }
562
563    pub struct PersistentVolumeClaimSpec {
564        /// `["ReadWriteOnce" | "ReadWriteMany" | "ReadOnlyMany" |
565        /// "ReadWriteOncePod"]`. Required by the K8s schema for a
566        /// dynamically provisioned claim.
567        pub access_modes: Vec<String>,
568        pub resources: Option<VolumeResourceRequirements>,
569        /// `""` means "use the cluster's default StorageClass".
570        pub storage_class_name: String,
571    }
572
573    /// `PersistentVolumeClaimSpec.resources` — only `requests.storage`
574    /// matters for a PVC; we don't model `limits` here. The k8s API
575    /// field is `resources: ResourceRequirements`; this is a trimmed
576    /// shape that serializes identically.
577    pub struct VolumeResourceRequirements {
578        pub requests: BTreeMap<String, String>,
579    }
580}
581
582/// Argo's `apiVersion` for `Workflow`/`WorkflowTemplate` resources.
583pub const API_VERSION: &str = "argoproj.io/v1alpha1";
584/// Argo's `kind` for `Workflow` resources.
585pub const KIND_WORKFLOW: &str = "Workflow";
586/// Argo's `kind` for `WorkflowTemplate` resources.
587pub const KIND_WORKFLOW_TEMPLATE: &str = "WorkflowTemplate";