stackless_stripe_projects/
provision.rs1use std::collections::BTreeMap;
8use std::path::Path;
9
10use serde_json::Value;
11use stackless_core::def::StackDef;
12
13use crate::catalog::Catalog;
14use crate::catalog::verify::{CatalogService, add_catalog_resource};
15use crate::error::ProjectsError;
16use crate::project::{self, find_env_value};
17use crate::stripe::{CommandRunner, StripeProjects};
18
19#[derive(Debug)]
21pub struct ProvisionContext<'a> {
22 pub def: &'a StackDef,
23 pub instance: &'a str,
24 pub logical_name: &'a str,
25 pub definition_dir: &'a Path,
26 pub substrate: &'a str,
27 pub skip_instance_context: bool,
29}
30
31impl ProvisionContext<'_> {
32 pub fn resource_name(&self) -> String {
34 format!("{}-{}", self.instance, self.logical_name)
35 }
36}
37
38#[derive(Debug)]
42pub struct ProvisionedCredentials {
43 pub resource_name: String,
44 pub raw: String,
45}
46
47pub async fn provision_with_credentials<C, R>(
50 stripe: &StripeProjects<R>,
51 catalog: &Catalog,
52 ctx: &ProvisionContext<'_>,
53 config: &C,
54 env_keys: &[&str],
55) -> Result<ProvisionedCredentials, ProjectsError>
56where
57 C: CatalogService,
58 R: CommandRunner,
59{
60 if !ctx.skip_instance_context {
61 project::ensure_project(stripe, ctx.def, ctx.definition_dir).await?;
62 project::ensure_environment(stripe, ctx.instance).await?;
63 }
64 let requested = ctx.resource_name();
65 let added = add_catalog_resource(stripe, catalog, config, &requested).await?;
66 let resource_name = added.name;
67 let raw = resolve_env_blob(
68 stripe,
69 &added.data,
70 ctx.instance,
71 &resource_name,
72 C::REFERENCE,
73 env_keys,
74 )
75 .await?;
76 Ok(ProvisionedCredentials { resource_name, raw })
77}
78
79#[derive(Debug)]
82pub struct ProvisionedEnv {
83 pub resource_name: String,
84 pub values: BTreeMap<String, String>,
85}
86
87pub async fn provision_with_env<C, R>(
92 stripe: &StripeProjects<R>,
93 catalog: &Catalog,
94 ctx: &ProvisionContext<'_>,
95 config: &C,
96 env_keys: &[&str],
97) -> Result<ProvisionedEnv, ProjectsError>
98where
99 C: CatalogService,
100 R: CommandRunner,
101{
102 if !ctx.skip_instance_context {
103 project::ensure_project(stripe, ctx.def, ctx.definition_dir).await?;
104 project::ensure_environment(stripe, ctx.instance).await?;
105 }
106 let requested = ctx.resource_name();
107 let added = add_catalog_resource(stripe, catalog, config, &requested).await?;
108 let resource_name = added.name;
109 let mut values = BTreeMap::new();
110 let mut missing: Vec<&str> = Vec::new();
111 for key in env_keys {
112 match find_env_value(&added.data, key) {
113 Some(value) => {
114 values.insert((*key).to_owned(), value);
115 }
116 None => missing.push(key),
117 }
118 }
119 if !missing.is_empty() {
121 let pulled = project::pull_env_values(stripe, ctx.instance, &missing).await?;
122 for (idx, key) in missing.iter().enumerate() {
123 if let Some(value) = pulled.get(idx).cloned().flatten() {
124 values.insert((*key).to_owned(), value);
125 }
126 }
127 }
128 Ok(ProvisionedEnv {
129 resource_name,
130 values,
131 })
132}
133
134pub async fn provision_outputs<C, R>(
144 stripe: &StripeProjects<R>,
145 catalog: &Catalog,
146 ctx: &ProvisionContext<'_>,
147 config: &C,
148 provider_prefix: &str,
149 fields: &[(&str, &str, bool)],
150) -> Result<(String, BTreeMap<String, String>), ProjectsError>
151where
152 C: CatalogService,
153 R: CommandRunner,
154{
155 if !ctx.skip_instance_context {
156 project::ensure_project(stripe, ctx.def, ctx.definition_dir).await?;
157 project::ensure_environment(stripe, ctx.instance).await?;
158 }
159 let requested = ctx.resource_name();
160 let added = add_catalog_resource(stripe, catalog, config, &requested).await?;
161 let resource_name = added.name;
162 let resource_prefix = resource_name.to_ascii_uppercase().replace('-', "_");
163 let candidates: Vec<String> = fields
164 .iter()
165 .flat_map(|(suffix, _, _)| {
166 [
167 format!("{resource_prefix}_{suffix}"),
168 format!("{provider_prefix}_{suffix}"),
169 ]
170 })
171 .collect();
172 let mut values = BTreeMap::new();
173 let mut missing: Vec<&str> = Vec::new();
174 for key in &candidates {
175 match find_env_value(&added.data, key) {
176 Some(value) => {
177 values.insert(key.clone(), value);
178 }
179 None => missing.push(key.as_str()),
180 }
181 }
182 if !missing.is_empty() {
183 let pulled = project::pull_env_values(stripe, ctx.instance, &missing).await?;
184 for (idx, key) in missing.iter().enumerate() {
185 if let Some(value) = pulled.get(idx).cloned().flatten() {
186 values.insert((*key).to_owned(), value);
187 }
188 }
189 }
190 let mut outputs = BTreeMap::new();
191 for (suffix, output, required) in fields {
192 let value = values
193 .get(&format!("{resource_prefix}_{suffix}"))
194 .or_else(|| values.get(&format!("{provider_prefix}_{suffix}")));
195 match value {
196 Some(value) => {
197 outputs.insert((*output).to_owned(), value.clone());
198 }
199 None if *required => {
200 return Err(ProjectsError::ProvisionFailed {
201 resource: resource_name.clone(),
202 detail: format!("provider did not return {suffix} for {resource_name}"),
203 });
204 }
205 None => {}
206 }
207 }
208 Ok((resource_name, outputs))
209}
210
211async fn resolve_env_blob<R>(
212 stripe: &StripeProjects<R>,
213 add_data: &Value,
214 instance: &str,
215 resource: &str,
216 reference: &str,
217 env_keys: &[&str],
218) -> Result<String, ProjectsError>
219where
220 R: CommandRunner,
221{
222 for key in env_keys {
223 if let Some(value) = find_env_value(add_data, key) {
224 return Ok(value);
225 }
226 }
227 for key in env_keys {
228 if let Some(value) = project::refreshed_env_value(stripe, reference, key).await? {
229 return Ok(value);
230 }
231 }
232 for key in env_keys {
233 if let Some(value) = project::pull_env_value(stripe, instance, key).await? {
234 return Ok(value);
235 }
236 }
237 Err(ProjectsError::ProvisionFailed {
238 resource: resource.to_owned(),
239 detail: format!("none of {env_keys:?} was returned or pulled from Stripe Projects"),
240 })
241}