Skip to main content

pulumi_gestalt_rust/
native.rs

1use crate::PulumiAny;
2use anyhow::{Context as anyhowContext, Result, bail};
3use bon::Builder;
4use pulumi_gestalt_model::{
5    FromPulumiValue, Output, PulumiValue, PulumiValueContent, ToPulumiValue,
6};
7use pulumi_gestalt_rust_integration as integration;
8use pulumi_gestalt_rust_integration::{ConfigValue, FieldName};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::sync::Arc;
12use tokio::runtime::Runtime;
13
14pub trait Provider {
15    fn get_provider_id(&self) -> Output<String>;
16}
17
18#[derive(Default, Builder, Clone)]
19pub struct CustomResourceOptions {
20    #[builder(with = |p: &impl Provider| { p.get_provider_id() })]
21    pub provider: Option<Output<String>>,
22}
23
24pub struct CompositeOutput {
25    inner: integration::RegisterResourceOutput,
26    runtime: Arc<Runtime>,
27}
28
29impl CompositeOutput {
30    fn from_internal(inner: integration::RegisterResourceOutput, runtime: Arc<Runtime>) -> Self {
31        Self { inner, runtime }
32    }
33
34    pub fn get_field<T>(&self, key: &str) -> Output<T>
35    where
36        T: FromPulumiValue + Send + Sync + 'static,
37    {
38        let res = self
39            .runtime
40            .block_on(self.inner.get_field(FieldName::from(key.to_string())));
41        Context::integration_to_model_output(res)
42    }
43
44    pub fn get_field_any(&self, key: &str) -> Output<PulumiAny> {
45        let res = self
46            .runtime
47            .block_on(self.inner.get_field(FieldName::from(key.to_string())));
48        Context::integration_to_pulumi_any_output(res)
49    }
50
51    pub fn get_urn(&self) -> Output<String> {
52        let res = self.runtime.block_on(self.inner.get_urn());
53        Context::integration_to_model_output(res)
54    }
55
56    pub fn get_id(&self) -> Output<String> {
57        let res = self.runtime.block_on(self.inner.get_id());
58        Context::integration_to_model_output(res)
59    }
60
61    pub fn get_provider_id(&self) -> Output<String> {
62        let res = self.runtime.block_on(self.inner.get_provider_id());
63        Context::integration_to_model_output(res)
64    }
65}
66
67pub struct Context {
68    inner: integration::Context,
69    runtime: Arc<Runtime>,
70}
71
72impl Default for Context {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78impl Context {
79    pub fn new() -> Self {
80        let runtime = Runtime::new().unwrap();
81        let ctx = runtime.block_on(integration::Context::new());
82        Self {
83            inner: ctx,
84            runtime: Arc::new(runtime),
85        }
86    }
87
88    pub fn finish(&self) {
89        self.runtime.block_on(self.inner.finish())
90    }
91
92    pub fn new_output<T>(&self, value: &T) -> Output<T>
93    where
94        T: Clone + Send + Sync + 'static,
95    {
96        let value = value.clone();
97        Output::from_resolved_future(async move {
98            pulumi_gestalt_model::ResolvedOutput {
99                value: Some(value),
100                secret: false,
101                dependencies: Default::default(),
102            }
103        })
104    }
105
106    pub fn new_secret<T>(&self, value: &T) -> Output<T>
107    where
108        T: Clone + Send + Sync + 'static,
109    {
110        self.new_output(value).secret()
111    }
112
113    pub fn add_export(&self, key: &str, output: &impl ToPulumiValue) {
114        let key = FieldName::from(key.to_string());
115        let output = self.runtime.block_on(output.to_pulumi_value());
116        let internal_output = self
117            .inner
118            .create_output_from_future(async move { output.clone() });
119        self.runtime
120            .block_on(self.inner.add_output(key, internal_output));
121    }
122
123    pub fn register_resource(&self, request: RegisterResourceRequest) -> CompositeOutput {
124        let mut inputs = HashMap::new();
125        for object in request.object {
126            inputs.insert(
127                FieldName::from(object.name.clone()),
128                object.value.as_internal_output(self),
129            );
130        }
131        let provider = request
132            .options
133            .as_ref()
134            .and_then(|o| o.provider.as_ref())
135            .map(|p| self.model_to_integration_output(p.clone()));
136
137        let result = self.runtime.block_on(self.inner.register_resource(
138            integration::RegisterResourceRequest {
139                r#type: request.type_.clone(),
140                name: request.name.clone(),
141                version: request.version.clone(),
142                inputs,
143                provider,
144            },
145        ));
146
147        CompositeOutput::from_internal(result, self.runtime.clone())
148    }
149
150    pub fn invoke_resource(&self, request: InvokeResourceRequest) -> CompositeOutput {
151        let mut inputs = HashMap::new();
152        for object in request.object {
153            inputs.insert(
154                FieldName::from(object.name.clone()),
155                object.value.as_internal_output(self),
156            );
157        }
158        let result = self.runtime.block_on(self.inner.invoke_resource(
159            integration::InvokeResourceRequest {
160                token: request.token.clone(),
161                version: request.version.clone(),
162                inputs,
163            },
164        ));
165        CompositeOutput::from_internal(result, self.runtime.clone())
166    }
167
168    fn model_to_integration_output<T>(&self, output: Output<T>) -> integration::Output
169    where
170        T: ToPulumiValue + Clone + Send + Sync + 'static,
171    {
172        self.inner.create_output_from_future(async move {
173            let resolved = output.resolve().await;
174            match resolved.value {
175                None => PulumiValue {
176                    content: PulumiValueContent::Nothing,
177                    secret: resolved.secret,
178                    dependencies: resolved.dependencies,
179                },
180                Some(value) => {
181                    let mut pulumi_value = value.to_pulumi_value().await;
182                    pulumi_value.secret |= resolved.secret;
183                    pulumi_value.dependencies.extend(resolved.dependencies);
184                    pulumi_value
185                }
186            }
187        })
188    }
189
190    fn integration_to_model_output<T>(output: integration::Output) -> Output<T>
191    where
192        T: FromPulumiValue + Send + Sync + 'static,
193    {
194        Output::from_resolved_future(async move {
195            let resolved = output.resolve().await;
196            let Some(pulumi_value) = resolved.value else {
197                return pulumi_gestalt_model::ResolvedOutput {
198                    value: None,
199                    secret: false,
200                    dependencies: Default::default(),
201                };
202            };
203
204            if matches!(pulumi_value.content, PulumiValueContent::Nothing) {
205                pulumi_gestalt_model::ResolvedOutput {
206                    value: None,
207                    secret: false,
208                    dependencies: Default::default(),
209                }
210            } else {
211                pulumi_gestalt_model::ResolvedOutput {
212                    value: Some(T::from_pulumi_value(&pulumi_value).unwrap_or_else(|err| {
213                        panic!(
214                            "Failed to convert PulumiValue into {}: {err}",
215                            std::any::type_name::<T>()
216                        )
217                    })),
218                    secret: pulumi_value.secret,
219                    dependencies: pulumi_value.dependencies,
220                }
221            }
222        })
223    }
224
225    fn integration_to_pulumi_any_output(output: integration::Output) -> Output<PulumiAny> {
226        Output::from_resolved_future(async move {
227            let resolved = output.resolve().await;
228            let Some(pulumi_value) = resolved.value else {
229                return pulumi_gestalt_model::ResolvedOutput {
230                    value: None,
231                    secret: false,
232                    dependencies: Default::default(),
233                };
234            };
235
236            if matches!(pulumi_value.content, PulumiValueContent::Nothing) {
237                pulumi_gestalt_model::ResolvedOutput {
238                    value: None,
239                    secret: false,
240                    dependencies: Default::default(),
241                }
242            } else {
243                pulumi_gestalt_model::ResolvedOutput {
244                    value: Some(pulumi_gestalt_model::PulumiValueMiddleware::ready(
245                        pulumi_value.clone(),
246                    )),
247                    secret: pulumi_value.secret,
248                    dependencies: pulumi_value.dependencies,
249                }
250            }
251        })
252    }
253
254    fn config_full_key(name: Option<&str>, key: &str) -> String {
255        let namespace = name
256            .map(|value| value.to_string())
257            .or_else(|| std::env::var("PULUMI_PROJECT").ok())
258            .unwrap_or_else(|| "<unknown-project>".to_string());
259        format!("{namespace}:{key}")
260    }
261
262    pub fn require_config(&self, name: Option<&str>, key: &str) -> Result<String> {
263        let full_key = Self::config_full_key(name, key);
264        match self
265            .runtime
266            .block_on(self.inner.get_config_value(name, key))
267        {
268            Some(ConfigValue::PlainText(value)) => Ok(value),
269            Some(ConfigValue::Secret(_)) => {
270                bail!("Config `{full_key}` is secret and cannot be read as plaintext")
271            }
272            None => {
273                bail!("Config `{full_key}` does not exist")
274            }
275        }
276    }
277
278    pub fn require_config_deserialize<T>(&self, name: Option<&str>, key: &str) -> Result<T>
279    where
280        T: for<'de> Deserialize<'de>,
281    {
282        let val = self
283            .require_config(name, key)
284            .context("Failed to obtain config value")?;
285
286        serde_json::from_str(&val)
287            .with_context(|| format!("Failed to deserialize config value for key `{key}`"))
288    }
289
290    pub fn require_config_secret(&self, name: Option<&str>, key: &str) -> Result<Output<String>> {
291        let full_key = Self::config_full_key(name, key);
292        match self
293            .runtime
294            .block_on(self.inner.get_config_value(name, key))
295        {
296            Some(ConfigValue::Secret(sec)) => Ok(Self::integration_to_model_output(sec)),
297            Some(ConfigValue::PlainText(_)) => {
298                bail!("Config `{full_key}` is plaintext and cannot be read as secret")
299            }
300            None => {
301                bail!("Config `{full_key}` does not exist")
302            }
303        }
304    }
305
306    pub fn require_config_secret_deserialize<T>(
307        &self,
308        name: Option<&str>,
309        key: &str,
310    ) -> Result<Output<T>>
311    where
312        T: for<'de> Deserialize<'de> + Serialize + Clone + Send + Sync + 'static,
313    {
314        let secret_output = self
315            .require_config_secret(name, key)
316            .context("Failed to obtain secret config value")?;
317
318        let key = key.to_string();
319        Ok(secret_output.map(move |s| {
320            serde_json::from_str(&s)
321                .with_context(|| {
322                    format!("Failed to deserialize secret config value for key `{key}`")
323                })
324                .unwrap()
325        }))
326    }
327
328    pub fn get_organization(&self) -> &str {
329        self.inner.get_organization()
330    }
331
332    pub fn get_project(&self) -> &str {
333        self.inner.get_project()
334    }
335
336    pub fn get_stack(&self) -> &str {
337        self.inner.get_stack()
338    }
339
340    pub fn get_root_directory(&self) -> &str {
341        self.inner.get_root_directory()
342    }
343
344    pub fn require_pulumi_version(&self, version_range: &str) -> Result<()> {
345        self.runtime
346            .block_on(self.inner.require_pulumi_version(version_range))
347            .context("Failed to require Pulumi version")
348    }
349}
350
351pub trait IntoInternalOutput {
352    fn as_internal_output(&self, ctx: &Context) -> integration::Output;
353}
354
355impl<T> IntoInternalOutput for Output<T>
356where
357    T: ToPulumiValue + Clone + Send + Sync + 'static,
358{
359    fn as_internal_output(&self, ctx: &Context) -> integration::Output {
360        ctx.model_to_integration_output(self.clone())
361    }
362}
363
364pub struct RegisterResourceRequest<'a> {
365    pub type_: String,
366    pub name: String,
367    pub version: String,
368    pub object: &'a [ResourceRequestObjectField<'a>],
369    pub options: Option<CustomResourceOptions>,
370}
371
372pub struct InvokeResourceRequest<'a> {
373    pub token: String,
374    pub version: String,
375    pub object: &'a [ResourceRequestObjectField<'a>],
376}
377
378pub struct ResourceRequestObjectField<'a> {
379    pub name: String,
380    pub value: &'a dyn IntoInternalOutput,
381}