xbp_cli/commands/workers/
mod.rs1mod deploy;
2mod project;
3mod worktree;
4mod wrangler;
5
6use crate::cli::commands::{
7 WorkersCmd, WorkersDeploySubCommand, WorkersSecretsBulkCmd, WorkersSecretsPutCmd,
8 WorkersSecretsSubCommand, WorkersSubCommand, WorkersTargetArgs, WorkersWorktreeSubCommand,
9 WorkersWranglerSubCommand,
10};
11use crate::config::{resolve_cloudflare_account_id, resolve_cloudflare_api_token};
12use crate::provider_support::CloudflareClient;
13use serde_json::{json, Map, Value};
14use std::fs;
15use std::io::{self, Read};
16use std::path::Path;
17
18pub async fn run_workers(cmd: WorkersCmd, _debug: bool) -> Result<(), String> {
19 let worker_root = project::resolve_workers_project_root(cmd.root.as_deref())?;
20
21 match cmd.command {
22 WorkersSubCommand::Secrets(secrets_cmd) => {
23 run_worker_secrets(
24 &worker_root,
25 cmd.token.as_deref(),
26 cmd.account_id.as_deref(),
27 secrets_cmd.target,
28 secrets_cmd.command,
29 )
30 .await
31 }
32 WorkersSubCommand::Settings(settings_cmd) => {
33 let local_env = load_local_env(&worker_root)?;
34 let script_name =
35 resolve_target_script_name(&worker_root, &settings_cmd.target, &local_env);
36 let client = build_cloudflare_client(
37 cmd.token.as_deref(),
38 cmd.account_id.as_deref(),
39 &local_env,
40 )?;
41 let settings = client.get_worker_settings(&script_name).await?;
42 match settings {
43 Some(settings) => print_json(&json!({
44 "script_name": script_name,
45 "settings": settings,
46 })),
47 None => Err(format!(
48 "Worker settings were not found for script `{}`.",
49 script_name
50 )),
51 }
52 }
53 WorkersSubCommand::Wrangler(wrangler_cmd) => match wrangler_cmd.command {
54 WorkersWranglerSubCommand::GenerateConfig(generate_cmd) => {
55 wrangler::run_generate_config(&worker_root, &generate_cmd.output)
56 }
57 WorkersWranglerSubCommand::ConfigPath(config_path_cmd) => {
58 match project::resolve_wrangler_config_path(
59 &worker_root,
60 &config_path_cmd.command_name,
61 &config_path_cmd.mode,
62 ) {
63 Some(path) => {
64 println!("{}", path);
65 Ok(())
66 }
67 None => {
68 println!();
69 Ok(())
70 }
71 }
72 }
73 },
74 WorkersSubCommand::D1(d1_cmd) => match d1_cmd.command {
75 crate::cli::commands::WorkersD1SubCommand::Migrations(migrations_cmd) => {
76 match migrations_cmd.command {
77 crate::cli::commands::WorkersD1MigrationsSubCommand::Apply(apply_cmd) => {
78 wrangler::run_d1_migrations_apply(&worker_root, &apply_cmd)
79 }
80 }
81 }
82 },
83 WorkersSubCommand::Deploy(deploy_cmd) => match deploy_cmd.command {
84 WorkersDeploySubCommand::Predeploy(predeploy_cmd) => {
85 deploy::run_predeploy(
86 &worker_root,
87 cmd.token.as_deref(),
88 cmd.account_id.as_deref(),
89 predeploy_cmd.ci,
90 )
91 .await
92 }
93 WorkersDeploySubCommand::SyncEnvLocal(_) => {
94 deploy::run_sync_env_local(
95 &worker_root,
96 cmd.token.as_deref(),
97 cmd.account_id.as_deref(),
98 )
99 .await
100 }
101 WorkersDeploySubCommand::Ci(ci_cmd) => {
102 deploy::run_deploy_ci_script(&worker_root, ci_cmd.version_upload)
103 }
104 WorkersDeploySubCommand::Select(select_cmd) => deploy::run_select_deploy_script(
105 &worker_root,
106 select_cmd.ci,
107 select_cmd.branch.as_deref(),
108 ),
109 },
110 WorkersSubCommand::Worktree(worktree_cmd) => match worktree_cmd.command {
111 WorkersWorktreeSubCommand::Paths(_) => worktree::print_worktree_paths(&worker_root),
112 WorkersWorktreeSubCommand::LinkDevVars(_) => {
113 worktree::link_dev_vars_from_primary_worktree(&worker_root)
114 }
115 },
116 WorkersSubCommand::Env(env_cmd) => {
117 let local_env = load_local_env(&worker_root)?;
118 let script_name = resolve_target_script_name(&worker_root, &env_cmd.target, &local_env);
119 let summary = wrangler::build_env_summary(
120 &worker_root,
121 &script_name,
122 &local_env,
123 env_cmd.show_values,
124 )?;
125 print_json(&summary)
126 }
127 }
128}
129
130async fn run_worker_secrets(
131 worker_root: &Path,
132 token_override: Option<&str>,
133 account_id_override: Option<&str>,
134 target: WorkersTargetArgs,
135 command: WorkersSecretsSubCommand,
136) -> Result<(), String> {
137 let local_env = load_local_env(worker_root)?;
138 let script_name = resolve_target_script_name(worker_root, &target, &local_env);
139 let client = build_cloudflare_client(token_override, account_id_override, &local_env)?;
140
141 match command {
142 WorkersSecretsSubCommand::List(_) => {
143 let secrets = client.list_worker_secrets(&script_name).await?;
144 print_json(&json!({
145 "script_name": script_name,
146 "secrets": secrets,
147 }))
148 }
149 WorkersSecretsSubCommand::Get(get_cmd) => {
150 let secret = client
151 .get_worker_secret(&script_name, &get_cmd.name)
152 .await?;
153 print_json(&json!({
154 "script_name": script_name,
155 "secret": secret,
156 }))
157 }
158 WorkersSecretsSubCommand::Put(put_cmd) => {
159 let secret_value = resolve_secret_value(&put_cmd)?;
160 let result = client
161 .put_worker_secret(&script_name, &put_cmd.name, &secret_value)
162 .await?;
163 print_json(&json!({
164 "script_name": script_name,
165 "secret": result,
166 }))
167 }
168 WorkersSecretsSubCommand::Delete(delete_cmd) => {
169 client
170 .delete_worker_secret(&script_name, &delete_cmd.name)
171 .await?;
172 println!(
173 "Deleted Worker secret `{}` from script `{}`.",
174 delete_cmd.name, script_name
175 );
176 Ok(())
177 }
178 WorkersSecretsSubCommand::Bulk(bulk_cmd) => {
179 let patch = read_bulk_secret_patch(&bulk_cmd)?;
180 let result = client
181 .patch_worker_secrets_bulk(&script_name, &patch)
182 .await?;
183 print_json(&json!({
184 "script_name": script_name,
185 "result": result,
186 }))
187 }
188 }
189}
190
191fn resolve_target_script_name(
192 worker_root: &Path,
193 target: &WorkersTargetArgs,
194 local_env: &std::collections::HashMap<String, String>,
195) -> String {
196 project::resolve_remote_script_name(
197 worker_root,
198 target.script.as_deref(),
199 target.worker.as_deref(),
200 target.environment.as_deref(),
201 local_env,
202 )
203}
204
205fn build_cloudflare_client(
206 token_override: Option<&str>,
207 account_id_override: Option<&str>,
208 local_env: &std::collections::HashMap<String, String>,
209) -> Result<CloudflareClient, String> {
210 let token = token_override
211 .map(str::trim)
212 .filter(|value| !value.is_empty())
213 .map(str::to_string)
214 .or_else(|| local_env.get("CLOUDFLARE_API_TOKEN").cloned())
215 .or_else(resolve_cloudflare_api_token)
216 .ok_or_else(|| {
217 "No Cloudflare API token found. Use `--token`, `CLOUDFLARE_API_TOKEN`, or `xbp config cloudflare set-key`.".to_string()
218 })?;
219 let account_id = account_id_override
220 .map(str::trim)
221 .filter(|value| !value.is_empty())
222 .map(str::to_string)
223 .or_else(|| local_env.get("CLOUDFLARE_ACCOUNT_ID").cloned())
224 .or_else(resolve_cloudflare_account_id)
225 .ok_or_else(|| {
226 "No Cloudflare account ID found. Use `--account-id`, `CLOUDFLARE_ACCOUNT_ID`, or `xbp config cloudflare set-account-id`.".to_string()
227 })?;
228 CloudflareClient::new(token, account_id)
229}
230
231fn load_local_env(worker_root: &Path) -> Result<std::collections::HashMap<String, String>, String> {
232 let env_local_path = worker_root.join(".env.local");
233 if env_local_path.exists() {
234 project::parse_multiline_env_file(&env_local_path)
235 } else {
236 Ok(std::collections::HashMap::new())
237 }
238}
239
240fn resolve_secret_value(cmd: &WorkersSecretsPutCmd) -> Result<String, String> {
241 match (cmd.from_stdin, cmd.value.as_deref()) {
242 (true, Some(_)) => Err("Use either `--value` or `--from-stdin`, not both.".to_string()),
243 (true, None) => {
244 let mut buffer = String::new();
245 io::stdin()
246 .read_to_string(&mut buffer)
247 .map_err(|error| format!("Failed to read stdin: {}", error))?;
248 let value = buffer.trim_end_matches(['\r', '\n']).to_string();
249 if value.is_empty() {
250 return Err("No secret value was provided on stdin.".to_string());
251 }
252 Ok(value)
253 }
254 (false, Some(value)) if !value.trim().is_empty() => Ok(value.to_string()),
255 _ => Err("Provide `--value <secret>` or `--from-stdin`.".to_string()),
256 }
257}
258
259fn read_bulk_secret_patch(cmd: &WorkersSecretsBulkCmd) -> Result<Value, String> {
260 let content = fs::read_to_string(&cmd.file)
261 .map_err(|error| format!("Failed to read {}: {}", cmd.file.display(), error))?;
262
263 if cmd.format.eq_ignore_ascii_case("json") {
264 let parsed = serde_json::from_str::<Value>(&content)
265 .map_err(|error| format!("Failed to parse JSON {}: {}", cmd.file.display(), error))?;
266 return normalize_secret_patch_value(parsed);
267 }
268
269 if !cmd.format.eq_ignore_ascii_case("env") {
270 return Err(format!(
271 "Unsupported bulk secret format `{}`. Use `env` or `json`.",
272 cmd.format
273 ));
274 }
275
276 let parsed = project::parse_multiline_env_content(&content);
277 let patch = parsed
278 .into_iter()
279 .map(|(key, value)| {
280 (
281 key,
282 json!({
283 "type": "secret_text",
284 "text": value,
285 }),
286 )
287 })
288 .collect::<Map<String, Value>>();
289 Ok(Value::Object(patch))
290}
291
292fn normalize_secret_patch_value(value: Value) -> Result<Value, String> {
293 let Value::Object(entries) = value else {
294 return Err("Bulk JSON secrets input must be an object.".to_string());
295 };
296
297 let mut normalized = Map::new();
298 for (key, value) in entries {
299 let normalized_value = match value {
300 Value::Null => Value::Null,
301 Value::String(text) => json!({
302 "type": "secret_text",
303 "text": text,
304 }),
305 Value::Object(mut object) => {
306 object
307 .entry("type".to_string())
308 .or_insert_with(|| Value::String("secret_text".to_string()));
309 Value::Object(object)
310 }
311 other => {
312 return Err(format!(
313 "Invalid bulk secret value for `{}`. Use a string, object, or null, not {}.",
314 key, other
315 ));
316 }
317 };
318 normalized.insert(key, normalized_value);
319 }
320 Ok(Value::Object(normalized))
321}
322
323fn print_json(value: &impl serde::Serialize) -> Result<(), String> {
324 println!(
325 "{}",
326 serde_json::to_string_pretty(value)
327 .map_err(|error| format!("Failed to encode JSON output: {}", error))?
328 );
329 Ok(())
330}