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
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
#[derive(Debug, Parser)]
#[command(name = "clever-project", version, about = "Sync a project description with Clever Cloud", long_about = None)]
pub struct Cli {
/// Verbose output
#[arg(short, long, global = true)]
pub verbose: bool,
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
/// Read resources from a Clever Cloud org into a project file
Read(ReadArgs),
/// Create or update resources from a project file
Apply(ApplyArgs),
/// Delete resources listed in a project file
Delete(DeleteArgs),
/// Validate a project file (syntax, variables, dependencies, sizing,
/// kinds, regions). Doesn't modify anything.
Check(CheckArgs),
}
#[derive(Debug, Args)]
pub struct ReadArgs {
/// Target organisation (overrides project file)
#[arg(long)]
pub org: String,
/// App name to read (can be repeated)
#[arg(long = "app")]
pub apps: Vec<String>,
/// Addon name to read (can be repeated)
#[arg(long = "addon")]
pub addons: Vec<String>,
/// Read every app and addon in the org
#[arg(long, conflicts_with_all = ["apps", "addons"])]
pub all: bool,
/// Output file path (.yaml/.yml/.json)
#[arg(short = 'o', long)]
pub output: PathBuf,
}
#[derive(Debug, Args)]
pub struct ApplyArgs {
/// Project file path (.yaml/.yml/.json)
pub file: PathBuf,
/// Override the organisation defined in the project file
#[arg(long)]
pub org: Option<String>,
/// Override the default region defined in the project file
#[arg(long)]
pub region: Option<String>,
/// Value for the special variable `${env}` (default `prod`)
#[arg(long)]
pub env: Option<String>,
/// Set a variable (key=value). Overrides values from the project file
/// and from --variable-path.
#[arg(long = "variable", value_parser = parse_kv)]
pub variables: Vec<(String, String)>,
/// Load variable overrides from a YAML/JSON file (flat key/value
/// mapping). Can be repeated; later files override earlier ones, and
/// --variable beats anything from these files.
#[arg(long = "variable-path")]
pub variable_paths: Vec<PathBuf>,
/// Explicit path to a secrets file. When omitted, secrets are
/// auto-discovered next to the project file (`<stem>.secrets` and
/// `<stem>.<env>.secrets`).
#[arg(long)]
pub secrets_path: Option<PathBuf>,
/// Plan only: read current state and log what would change without
/// mutating anything on Clever Cloud.
#[arg(long)]
pub dry_run: bool,
}
#[derive(Debug, Args)]
pub struct CheckArgs {
/// Project file path (.yaml/.yml/.json)
pub file: PathBuf,
/// Override the organisation defined in the project file
#[arg(long)]
pub org: Option<String>,
/// Override the default region defined in the project file
#[arg(long)]
pub region: Option<String>,
/// Value for the special variable `${env}` (default `prod`)
#[arg(long)]
pub env: Option<String>,
/// Set a variable (key=value). Overrides values from the project file
/// and from --variable-path.
#[arg(long = "variable", value_parser = parse_kv)]
pub variables: Vec<(String, String)>,
/// Load variable overrides from a YAML/JSON file (repeatable).
#[arg(long = "variable-path")]
pub variable_paths: Vec<PathBuf>,
/// Explicit path to a secrets file.
#[arg(long)]
pub secrets_path: Option<PathBuf>,
/// Skip live validation against Clever's API (addon catalog, app
/// instance flavors). Useful in CI environments without `clever login`.
/// Static validation (syntax, variables, kinds, regions, dependencies,
/// uniqueness) is always performed.
#[arg(long)]
pub offline: bool,
}
#[derive(Debug, Args)]
pub struct DeleteArgs {
/// Project file path (.yaml/.yml/.json)
pub file: PathBuf,
/// Override the organisation defined in the project file
#[arg(long)]
pub org: Option<String>,
/// Override the default region defined in the project file
#[arg(long)]
pub region: Option<String>,
/// Value for the special variable `${env}` (default `prod`)
#[arg(long)]
pub env: Option<String>,
/// Set a variable (key=value). Overrides values from the project file
/// and from --variable-path.
#[arg(long = "variable", value_parser = parse_kv)]
pub variables: Vec<(String, String)>,
/// Load variable overrides from a YAML/JSON file (flat key/value
/// mapping). Can be repeated; later files override earlier ones, and
/// --variable beats anything from these files.
#[arg(long = "variable-path")]
pub variable_paths: Vec<PathBuf>,
/// Explicit path to a secrets file. When omitted, secrets are
/// auto-discovered next to the project file (`<stem>.secrets` and
/// `<stem>.<env>.secrets`).
#[arg(long)]
pub secrets_path: Option<PathBuf>,
/// Plan only: log what would be deleted without mutating anything.
#[arg(long)]
pub dry_run: bool,
}
fn parse_kv(s: &str) -> Result<(String, String), String> {
let (k, v) = s
.split_once('=')
.ok_or_else(|| format!("expected key=value, got `{s}`"))?;
if k.is_empty() {
return Err(format!("empty key in `{s}`"));
}
Ok((k.to_string(), v.to_string()))
}