Skip to main content

systemprompt_cli/commands/cloud/deploy/pipeline/
request.rs

1//! Typed inputs and outputs for [`super::DeployOrchestrator`].
2//!
3//! [`DeploySecretsSource`] decides what a deploy is allowed to push. With a
4//! Vault profile the container fetches its own secrets at boot, so the deploy
5//! pushes only the credentials Vault itself needs to answer — never the
6//! contents of `secrets.json`, and never the JWT signing key.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11use std::path::PathBuf;
12
13use systemprompt_cloud::CloudCredentials;
14use systemprompt_identifiers::TenantId;
15use systemprompt_models::env::contains_placeholder;
16use systemprompt_models::profile::{SecretsConfig, SecretsSource, VaultAuth, VaultSecretsConfig};
17
18pub const VAULT_ADDR_ENV: &str = "VAULT_ADDR";
19pub const VAULT_NAMESPACE_ENV: &str = "VAULT_NAMESPACE";
20
21#[derive(Debug)]
22pub struct DeployRequest {
23    pub tenant_id: TenantId,
24    pub tenant_name: String,
25    pub profile_name: String,
26    pub project_root: PathBuf,
27    pub credentials: CloudCredentials,
28    pub secrets: DeploySecretsSource,
29    pub signing_key_path: PathBuf,
30    pub options: DeployOptions,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum DeploySecretsSource {
35    EnvFromFile { path: PathBuf },
36    Vault { bootstrap_env: Vec<String> },
37}
38
39impl DeploySecretsSource {
40    #[must_use]
41    pub fn from_profile(secrets: Option<&SecretsConfig>, secrets_path: PathBuf) -> Self {
42        match secrets {
43            Some(config) if matches!(config.source, SecretsSource::Vault) => {
44                let bootstrap_env = config
45                    .vault
46                    .as_ref()
47                    .map(bootstrap_env_names)
48                    .unwrap_or_default();
49                Self::Vault { bootstrap_env }
50            },
51            Some(_) | None => Self::EnvFromFile { path: secrets_path },
52        }
53    }
54}
55
56#[must_use]
57pub fn bootstrap_env_names(vault: &VaultSecretsConfig) -> Vec<String> {
58    let mut names = match &vault.auth {
59        VaultAuth::Token {
60            token_env,
61            token_file,
62        } => {
63            if token_file.is_some() {
64                Vec::new()
65            } else {
66                vec![token_env.clone()]
67            }
68        },
69        VaultAuth::AppRole {
70            role_id_env,
71            secret_id_env,
72            ..
73        } => vec![role_id_env.clone(), secret_id_env.clone()],
74        VaultAuth::Kubernetes { .. } => Vec::new(),
75    };
76
77    if contains_placeholder(&vault.address) {
78        names.push(placeholder_var(&vault.address, VAULT_ADDR_ENV));
79    }
80    if let Some(namespace) = vault.namespace.as_deref()
81        && contains_placeholder(namespace)
82    {
83        names.push(placeholder_var(namespace, VAULT_NAMESPACE_ENV));
84    }
85
86    names.sort();
87    names.dedup();
88    names
89}
90
91fn placeholder_var(value: &str, fallback: &str) -> String {
92    value
93        .split_once("${")
94        .and_then(|(_, rest)| rest.split_once('}'))
95        .map(|(name, _)| name.split(":-").next().unwrap_or(name).to_owned())
96        .filter(|name| !name.is_empty())
97        .unwrap_or_else(|| fallback.to_owned())
98}
99
100#[derive(Debug, Clone, Copy)]
101pub struct DeployOptions {
102    pub skip_push: bool,
103}
104
105#[derive(Debug)]
106pub struct DeployReport {
107    pub image: String,
108    pub status: String,
109    pub app_url: Option<String>,
110}