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 resource_name = ctx.resource_name();
65 let add_data = add_catalog_resource(stripe, catalog, config, &resource_name).await?;
66 let raw = resolve_env_blob(
67 stripe,
68 &add_data,
69 ctx.instance,
70 &resource_name,
71 C::REFERENCE,
72 env_keys,
73 )
74 .await?;
75 Ok(ProvisionedCredentials { resource_name, raw })
76}
77
78#[derive(Debug)]
81pub struct ProvisionedEnv {
82 pub resource_name: String,
83 pub values: BTreeMap<String, String>,
84}
85
86pub async fn provision_with_env<C, R>(
91 stripe: &StripeProjects<R>,
92 catalog: &Catalog,
93 ctx: &ProvisionContext<'_>,
94 config: &C,
95 env_keys: &[&str],
96) -> Result<ProvisionedEnv, ProjectsError>
97where
98 C: CatalogService,
99 R: CommandRunner,
100{
101 if !ctx.skip_instance_context {
102 project::ensure_project(stripe, ctx.def, ctx.definition_dir).await?;
103 project::ensure_environment(stripe, ctx.instance).await?;
104 }
105 let resource_name = ctx.resource_name();
106 let add_data = add_catalog_resource(stripe, catalog, config, &resource_name).await?;
107 let mut values = BTreeMap::new();
108 let mut missing: Vec<&str> = Vec::new();
109 for key in env_keys {
110 match find_env_value(&add_data, key) {
111 Some(value) => {
112 values.insert((*key).to_owned(), value);
113 }
114 None => missing.push(key),
115 }
116 }
117 if !missing.is_empty() {
119 let pulled = project::pull_env_values(stripe, ctx.instance, &missing).await?;
120 for (idx, key) in missing.iter().enumerate() {
121 if let Some(value) = pulled.get(idx).cloned().flatten() {
122 values.insert((*key).to_owned(), value);
123 }
124 }
125 }
126 Ok(ProvisionedEnv {
127 resource_name,
128 values,
129 })
130}
131
132pub async fn provision_outputs<C, R>(
139 stripe: &StripeProjects<R>,
140 catalog: &Catalog,
141 ctx: &ProvisionContext<'_>,
142 config: &C,
143 provider_prefix: &str,
144 fields: &[(&str, &str, bool)],
145) -> Result<(String, BTreeMap<String, String>), ProjectsError>
146where
147 C: CatalogService,
148 R: CommandRunner,
149{
150 let resource_prefix = ctx.resource_name().to_ascii_uppercase().replace('-', "_");
151 let candidates: Vec<String> = fields
152 .iter()
153 .flat_map(|(suffix, _, _)| {
154 [
155 format!("{resource_prefix}_{suffix}"),
156 format!("{provider_prefix}_{suffix}"),
157 ]
158 })
159 .collect();
160 let candidate_refs: Vec<&str> = candidates.iter().map(String::as_str).collect();
161 let ProvisionedEnv {
162 resource_name,
163 values,
164 } = provision_with_env(stripe, catalog, ctx, config, &candidate_refs).await?;
165 let mut outputs = BTreeMap::new();
166 for (suffix, output, required) in fields {
167 let value = values
168 .get(&format!("{resource_prefix}_{suffix}"))
169 .or_else(|| values.get(&format!("{provider_prefix}_{suffix}")));
170 match value {
171 Some(value) => {
172 outputs.insert((*output).to_owned(), value.clone());
173 }
174 None if *required => {
175 return Err(ProjectsError::ProvisionFailed {
176 resource: resource_name.clone(),
177 detail: format!("provider did not return {suffix} for {resource_name}"),
178 });
179 }
180 None => {}
181 }
182 }
183 Ok((resource_name, outputs))
184}
185
186async fn resolve_env_blob<R>(
187 stripe: &StripeProjects<R>,
188 add_data: &Value,
189 instance: &str,
190 resource: &str,
191 reference: &str,
192 env_keys: &[&str],
193) -> Result<String, ProjectsError>
194where
195 R: CommandRunner,
196{
197 for key in env_keys {
198 if let Some(value) = find_env_value(add_data, key) {
199 return Ok(value);
200 }
201 }
202 for key in env_keys {
203 if let Some(value) = project::refreshed_env_value(stripe, reference, key).await? {
204 return Ok(value);
205 }
206 }
207 for key in env_keys {
208 if let Some(value) = project::pull_env_value(stripe, instance, key).await? {
209 return Ok(value);
210 }
211 }
212 Err(ProjectsError::ProvisionFailed {
213 resource: resource.to_owned(),
214 detail: format!("none of {env_keys:?} was returned or pulled from Stripe Projects"),
215 })
216}