Skip to main content

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

1//! Deploy pipeline sequencing.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::collections::HashMap;
7
8use anyhow::Result;
9use systemprompt_cloud::constants::{container, paths};
10use systemprompt_cloud::deploy::{find_services_config, validate_profile_dockerfile};
11use systemprompt_cloud::{CloudApiClient, CloudCredentials, DockerCli, secrets_env};
12use systemprompt_loader::ConfigLoader;
13
14use super::artifacts::DeployArtifacts;
15use super::progress::{DeployEvent, DeployProgress};
16use super::request::{DeployReport, DeployRequest};
17
18const SIGNING_KEY_ENV: &str = "SIGNING_KEY_PEM";
19const PROFILE_ENV: &str = "SYSTEMPROMPT_PROFILE";
20
21#[derive(Debug, Default)]
22pub struct DeployOrchestrator {
23    docker: DockerCli,
24}
25
26impl DeployOrchestrator {
27    #[must_use]
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    #[must_use]
33    pub fn with_docker(mut self, docker: DockerCli) -> Self {
34        self.docker = docker;
35        self
36    }
37
38    pub async fn deploy(
39        &self,
40        request: &DeployRequest,
41        progress: &dyn DeployProgress,
42    ) -> Result<DeployReport> {
43        let artifacts = DeployArtifacts::resolve(&request.project_root, &request.profile_name)?;
44        progress.event(&DeployEvent::ArtifactsResolved {
45            tenant_name: &request.tenant_name,
46            binary: &artifacts.binary,
47            dockerfile: &artifacts.dockerfile,
48        });
49
50        let services_config_path = find_services_config(&request.project_root)?;
51        let services_config = ConfigLoader::load_from_path(&services_config_path)?;
52        validate_profile_dockerfile(
53            &artifacts.dockerfile,
54            &request.project_root,
55            &services_config,
56        )?;
57
58        let api_client = CloudApiClient::new(
59            &request.credentials.api_url,
60            request.credentials.api_token.as_str(),
61        )?;
62
63        let image = self
64            .build_and_push(&api_client, request, &artifacts, progress)
65            .await?;
66        provision_secrets(&api_client, request, progress).await?;
67
68        progress.event(&DeployEvent::DeployStarted);
69        let response = api_client.deploy(&request.tenant_id, &image).await?;
70        progress.event(&DeployEvent::Deployed {
71            status: &response.status,
72            app_url: response.app_url.as_deref(),
73        });
74
75        Ok(DeployReport {
76            image,
77            status: response.status,
78            app_url: response.app_url,
79        })
80    }
81
82    async fn build_and_push(
83        &self,
84        api_client: &CloudApiClient,
85        request: &DeployRequest,
86        artifacts: &DeployArtifacts,
87        progress: &dyn DeployProgress,
88    ) -> Result<String> {
89        progress.event(&DeployEvent::RegistryAuthStarted);
90        let registry_token = api_client.get_registry_token(&request.tenant_id).await?;
91        progress.event(&DeployEvent::RegistryAuthFinished);
92
93        let image = format!(
94            "{}/{}:{}",
95            registry_token.registry, registry_token.repository, registry_token.tag
96        );
97        progress.event(&DeployEvent::ImageResolved { image: &image });
98
99        progress.event(&DeployEvent::BuildStarted);
100        self.docker
101            .build_image(&request.project_root, &artifacts.dockerfile, &image)?;
102        progress.event(&DeployEvent::BuildFinished);
103
104        if request.options.skip_push {
105            progress.event(&DeployEvent::PushSkipped);
106        } else {
107            progress.event(&DeployEvent::PushStarted);
108            self.docker.login(
109                &registry_token.registry,
110                &registry_token.username,
111                &registry_token.token,
112            )?;
113            self.docker.push(&image)?;
114            progress.event(&DeployEvent::PushFinished);
115        }
116
117        Ok(image)
118    }
119}
120
121async fn provision_secrets(
122    api_client: &CloudApiClient,
123    request: &DeployRequest,
124    progress: &dyn DeployProgress,
125) -> Result<()> {
126    progress.event(&DeployEvent::SecretsPhaseStarted);
127
128    let mut env_secrets = if request.secrets_path.exists() {
129        secrets_env::map_secrets_to_env_vars(secrets_env::load_secrets_json(&request.secrets_path)?)
130    } else {
131        progress.event(&DeployEvent::SecretsFileMissing);
132        HashMap::new()
133    };
134
135    if !env_secrets.contains_key(SIGNING_KEY_ENV)
136        && let Some(pem) = secrets_env::read_signing_key_pem(&request.signing_key_path)?
137    {
138        env_secrets.insert(SIGNING_KEY_ENV.to_owned(), pem);
139    }
140
141    if !env_secrets.is_empty() {
142        progress.event(&DeployEvent::SecretsSyncStarted);
143        let keys = api_client
144            .set_secrets(&request.tenant_id, env_secrets)
145            .await?;
146        progress.event(&DeployEvent::SecretsSynced { count: keys.len() });
147    }
148
149    progress.event(&DeployEvent::CredentialsSyncStarted);
150    let keys = api_client
151        .set_secrets(&request.tenant_id, credentials_env(&request.credentials))
152        .await?;
153    progress.event(&DeployEvent::CredentialsSynced { count: keys.len() });
154
155    let profile_env_path = format!(
156        "{}/{}/{}",
157        container::PROFILES,
158        request.profile_name,
159        paths::PROFILE_CONFIG
160    );
161    let mut profile_secret = HashMap::new();
162    profile_secret.insert(PROFILE_ENV.to_owned(), profile_env_path);
163    api_client
164        .set_secrets(&request.tenant_id, profile_secret)
165        .await?;
166    progress.event(&DeployEvent::ProfilePathConfigured);
167
168    Ok(())
169}
170
171fn credentials_env(creds: &CloudCredentials) -> HashMap<String, String> {
172    HashMap::from([
173        (
174            "SYSTEMPROMPT_API_TOKEN".to_owned(),
175            creds.api_token.as_str().to_owned(),
176        ),
177        (
178            "SYSTEMPROMPT_USER_EMAIL".to_owned(),
179            creds.user_email.as_str().to_owned(),
180        ),
181        ("SYSTEMPROMPT_CLI_REMOTE".to_owned(), "true".to_owned()),
182    ])
183}