systemprompt_cli/commands/cloud/deploy/pipeline/
orchestrator.rs1use 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 ®istry_token.registry,
110 ®istry_token.username,
111 ®istry_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}