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
use serde::{Deserialize, Serialize};
use crate::types::{DeployTarget, ProjectConfig};
/// Exactly one deploy mode (CLI must enforce exclusivity).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DeployMode {
Plan,
Run,
Verify,
Promote,
Status,
History,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeployFlags {
pub dry_run: bool,
pub skip_oci: bool,
/// Skip docker build/push even when `services[].oci.dockerfile` is set.
#[serde(default)]
pub skip_build: bool,
/// Build images into the local Docker engine only (no registry push).
/// Preferred for docker-desktop / kind / minikube / k3d local clusters.
#[serde(default)]
pub local_image: bool,
/// When true, Cloudflare container verification fails if the image digest/tag
/// did not change after deploy. Default is false (config-only deploys allowed).
#[serde(default)]
pub require_new_container_image: bool,
/// When true (default), if a k8s service has a workload but no manifest paths,
/// generate and apply a minimal Deployment/Service (first-time bootstrap).
/// Disable with CLI `--no-bootstrap-workload`.
#[serde(default = "default_true")]
pub bootstrap_workload: bool,
/// Run full preflight doctor before apply (cluster, namespaces, CF builds, OCI).
#[serde(default)]
pub doctor: bool,
/// Skip deploy preflight doctor even when `doctor` would run automatically.
#[serde(default)]
pub skip_doctor: bool,
pub skip_apply: bool,
pub skip_rollout: bool,
/// Skip post-rollout port-forward / hosts DNS expose phase.
#[serde(default)]
pub skip_expose: bool,
pub skip_health: bool,
pub yes: bool,
pub json: bool,
pub namespace: Option<String>,
pub context: Option<String>,
pub promote_tag: Option<String>,
pub history_limit: usize,
pub output: Option<std::path::PathBuf>,
/// Selected deploy destinations from CLI (`--to cloudflare,kubernetes`).
/// Empty means: use each service's default `deploy.provider` (or its default destination).
#[serde(default)]
pub destinations: Vec<String>,
/// Keep only these service names after planning (`--only`). Empty = no only-filter.
#[serde(default)]
pub only_services: Vec<String>,
/// Drop these service names after planning (`--exclude`).
#[serde(default)]
pub exclude_services: Vec<String>,
}
/// Everything the engine needs; built by CLI, consumed by planner/runner/verifier.
#[derive(Debug, Clone)]
pub struct DeployContext {
pub env: String,
pub target: DeployTarget,
pub config: ProjectConfig,
pub flags: DeployFlags,
pub mode: DeployMode,
}
fn default_true() -> bool {
true
}
impl Default for DeployFlags {
fn default() -> Self {
Self {
dry_run: false,
skip_oci: false,
skip_build: false,
local_image: false,
require_new_container_image: false,
bootstrap_workload: true,
doctor: false,
skip_doctor: false,
skip_apply: false,
skip_rollout: false,
skip_expose: false,
skip_health: false,
yes: false,
json: false,
namespace: None,
context: None,
promote_tag: None,
history_limit: 20,
output: None,
destinations: Vec::new(),
only_services: Vec::new(),
exclude_services: Vec::new(),
}
}
}
impl DeployContext {
pub fn require_confirmation(&self) -> bool {
if self.flags.yes || self.flags.dry_run {
return false;
}
self.config
.kubernetes
.as_ref()
.map(|k| k.require_context_confirmation_for_apply)
.unwrap_or(true)
}
/// Preflight doctor: explicit `--doctor`, or auto on Run when not skipped.
pub fn should_run_doctor(&self) -> bool {
if self.flags.skip_doctor || self.flags.dry_run {
return false;
}
self.flags.doctor || matches!(self.mode, DeployMode::Run)
}
}