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}