Skip to main content

xbp_cli/commands/workers/
mod.rs

1mod 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}