diff --git a/examples/cfn-custom-resource/Pulumi.yaml b/examples/cfn-custom-resource/Pulumi.yaml new file mode 100644 index 0000000000..13bd0b2885 --- /dev/null +++ b/examples/cfn-custom-resource/Pulumi.yaml @@ -0,0 +1,3 @@ +name: cfn-custom-resource +runtime: nodejs +description: A TypeScript Pulumi program with AWS Cloud Control provider diff --git a/examples/cfn-custom-resource/ami-lookup.js b/examples/cfn-custom-resource/ami-lookup.js new file mode 100644 index 0000000000..318f3bcc63 --- /dev/null +++ b/examples/cfn-custom-resource/ami-lookup.js @@ -0,0 +1,103 @@ +/** +* A sample Lambda function that looks up the latest AMI ID for a given region and architecture. +**/ + +// Map instance architectures to an AMI name pattern +var archToAMINamePattern = { + "PV64": "amzn-ami-pv*x86_64-ebs", + "HVM64": "al2023-ami-2023.*-kernel-*-x86_64", + "HVMG2": "amzn-ami-graphics-hvm*x86_64-ebs*" +}; +const { EC2Client, DescribeImagesCommand } = require("@aws-sdk/client-ec2"); + +exports.handler = async function(event, context) { + const redactedEvent = { ...event, ResponseURL: "REDACTED" }; + console.log("REQUEST RECEIVED:\n" + JSON.stringify(redactedEvent)); + + // For Delete requests, immediately send a SUCCESS response. + if (event.RequestType == "Delete") { + await sendResponse(event, context, "SUCCESS"); + return; + } + + var responseStatus = "FAILED"; + var responseData = {}; + + const ec2Client = new EC2Client({ region: event.ResourceProperties.Region }); + const describeImagesParams = { + Filters: [{ Name: "name", Values: [archToAMINamePattern[event.ResourceProperties.Architecture]]}], + Owners: [event.ResourceProperties.Architecture == "HVMG2" ? "679593333241" : "amazon"] + }; + + try { + const describeImagesResult = await ec2Client.send(new DescribeImagesCommand(describeImagesParams)); + var images = describeImagesResult.Images; + // Sort images by name in descending order. The names contain the AMI version, formatted as YYYY.MM.Ver. + images.sort((x, y) => y.Name.localeCompare(x.Name)); + for (var j = 0; j < images.length; j++) { + if (isBeta(images[j].Name)) continue; + responseStatus = "SUCCESS"; + responseData["Id"] = images[j].ImageId; + break; + } + } catch (err) { + responseData = { Error: "DescribeImages call failed" }; + console.log(responseData.Error + ":\n", err); + } + + await sendResponse(event, context, responseStatus, responseData); +}; + +// Check if the image is a beta or rc image. The Lambda function won't return any of those images. +function isBeta(imageName) { + return imageName.toLowerCase().indexOf("beta") > -1 || imageName.toLowerCase().indexOf(".rc") > -1; +} + +// Send response to the pre-signed S3 URL +async function sendResponse(event, context, responseStatus, responseData) { + var responseBody = JSON.stringify({ + Status: responseStatus, + Reason: "See the details in CloudWatch Log Stream: " + context.logStreamName, + PhysicalResourceId: context.logStreamName, + StackId: event.StackId, + RequestId: event.RequestId, + LogicalResourceId: event.LogicalResourceId, + Data: responseData + }); + + console.log("RESPONSE BODY:\n", responseBody); + + var https = require("https"); + var url = require("url"); + + var parsedUrl = url.parse(event.ResponseURL); + var options = { + hostname: parsedUrl.hostname, + port: 443, + path: parsedUrl.path, + method: "PUT", + headers: { + "content-type": "", + "content-length": responseBody.length + } + }; + + console.log("SENDING RESPONSE...\n"); + + await new Promise((resolve, reject) => { + var request = https.request(options, function(response) { + console.log("STATUS: " + response.statusCode); + console.log("HEADERS: " + JSON.stringify(response.headers)); + resolve(); + }); + + request.on("error", function(error) { + console.log("sendResponse Error:" + error); + reject(error); + }); + + // write data to request body + request.write(responseBody); + request.end(); + }); +} diff --git a/examples/cfn-custom-resource/index.ts b/examples/cfn-custom-resource/index.ts new file mode 100644 index 0000000000..c3da73c853 --- /dev/null +++ b/examples/cfn-custom-resource/index.ts @@ -0,0 +1,98 @@ +// Copyright 2016-2024, Pulumi Corporation. + +import * as pulumi from '@pulumi/pulumi'; +import * as aws from "@pulumi/aws-native"; +import * as awsClassic from "@pulumi/aws"; + +const amiRegion = new pulumi.Config().require("amiRegion"); + +// Create an IAM role for the Lambda function +const lambdaRole = new awsClassic.iam.Role("lambdaRole", { + assumeRolePolicy: awsClassic.iam.assumeRolePolicyForPrincipal({ Service: "lambda.amazonaws.com" }), +}); + +const policy = new awsClassic.iam.Policy("lambdaPolicy", { + policy: { + Version: "2012-10-17", + Statement: [{ + Action: "ec2:DescribeImages", + Effect: "Allow", + Resource: "*", + }], + }, +}); + +const rpa1 = new awsClassic.iam.RolePolicyAttachment("lambdaRolePolicyAttachment1", { + role: lambdaRole.name, + policyArn: policy.arn, +}); + +const rpa2 = new awsClassic.iam.RolePolicyAttachment("lambdaRolePolicyAttachment2", { + role: lambdaRole.name, + policyArn: awsClassic.iam.ManagedPolicies.AWSLambdaBasicExecutionRole, +}); + +const bucket = new awsClassic.s3.BucketV2('custom-resource-emulator', { + forceDestroy: true, +}); + +const handlerCode = new awsClassic.s3.BucketObjectv2("handler-code", { + bucket: bucket.bucket, + key: "handlerCode", + source: new pulumi.asset.AssetArchive({ + "index.js": new pulumi.asset.FileAsset("ami-lookup.js"), + }) +}) + +// Create the Lambda function for the custom resource +const lambdaFunction = new awsClassic.lambda.Function("ami-lookup-custom-resource", { + runtime: awsClassic.types.enums.lambda.Runtime.NodeJS20dX, + s3Bucket: bucket.bucket, + s3Key: handlerCode.key, + handler: "index.handler", + role: lambdaRole.arn, + memorySize: 128, + timeout: 30, +}, { dependsOn: [rpa1, rpa2] }); + +const cfnCustomResource = new aws.cloudformation.CustomResourceEmulator('emulator', { + bucketName: bucket.id, + bucketKeyPrefix: 'custom-resource-emulator', + customResourceProperties: { + Region: amiRegion, + Architecture: 'HVM64', + }, + serviceToken: lambdaFunction.arn, + resourceType: 'Custom::MyResource', +}, { customTimeouts: { create: '5m', update: '5m', delete: '5m' } }); + +const cloudformationStack = new awsClassic.cloudformation.Stack('stack', { + templateBody: pulumi.interpolate`{ + "AWSTemplateFormatVersion" : "2010-09-09", + + "Description" : "AWS CloudFormation AMI Look Up Sample Template: Demonstrates how to dynamically specify an AMI ID. This template provisions an EC2 instance with an AMI ID that is based on the instance's type and region. **WARNING** This template creates an Amazon EC2 instance. You will be billed for the AWS resources used if you create a stack from this template.", + + "Resources" : { + "AMIInfo": { + "Type": "Custom::AMIInfo", + "Properties": { + "ServiceToken": "${lambdaFunction.arn}", + "ServiceTimeout": 300, + "Region": "${amiRegion}", + "Architecture": "HVM64" + } + } + }, + + "Outputs" : { + "AMIID" : { + "Description": "The Amazon EC2 instance AMI ID.", + "Value" : { "Fn::GetAtt": [ "AMIInfo", "Id" ] } + } + } +} +` +}); + +export const cloudformationAmiId = cloudformationStack.outputs['AMIID']; +export const emulatorAmiId = cfnCustomResource.data['Id']; diff --git a/examples/cfn-custom-resource/package.json b/examples/cfn-custom-resource/package.json new file mode 100644 index 0000000000..5ac520f4e6 --- /dev/null +++ b/examples/cfn-custom-resource/package.json @@ -0,0 +1,13 @@ +{ + "name": "cfn-custom-resource", + "devDependencies": { + "@types/node": "^8.0.0" + }, + "dependencies": { + "@pulumi/pulumi": "^3.136.0", + "@pulumi/aws": "^6.57.0" + }, + "peerDependencies": { + "@pulumi/aws-native": "dev" + } +} diff --git a/examples/cfn-custom-resource/tsconfig.json b/examples/cfn-custom-resource/tsconfig.json new file mode 100644 index 0000000000..ab65afa613 --- /dev/null +++ b/examples/cfn-custom-resource/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "strict": true, + "outDir": "bin", + "target": "es2016", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true, + "experimentalDecorators": true, + "pretty": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "forceConsistentCasingInFileNames": true + }, + "files": [ + "index.ts" + ] +} diff --git a/examples/examples_nodejs_test.go b/examples/examples_nodejs_test.go index fc353dc088..457af00889 100644 --- a/examples/examples_nodejs_test.go +++ b/examples/examples_nodejs_test.go @@ -8,7 +8,13 @@ import ( "path/filepath" "testing" + "github.com/pulumi/providertest/pulumitest" + "github.com/pulumi/providertest/pulumitest/assertpreview" + "github.com/pulumi/providertest/pulumitest/opttest" "github.com/pulumi/pulumi/pkg/v3/testing/integration" + "github.com/pulumi/pulumi/sdk/v3/go/auto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestSimpleTs(t *testing.T) { @@ -29,6 +35,46 @@ func TestGetTs(t *testing.T) { integration.ProgramTest(t, &test) } +func TestCustomResourceEmulator(t *testing.T) { + crossTest := func(t *testing.T, outputs auto.OutputMap) { + require.Contains(t, outputs, "cloudformationAmiId") + cloudformationAmiId := outputs["cloudformationAmiId"].Value.(string) + require.NotEmpty(t, cloudformationAmiId) + + require.Contains(t, outputs, "emulatorAmiId") + emulatorAmiId := outputs["emulatorAmiId"].Value.(string) + assert.Equal(t, cloudformationAmiId, emulatorAmiId) + } + + cwd := getCwd(t) + options := []opttest.Option{ + opttest.LocalProviderPath("aws-native", filepath.Join(cwd, "..", "bin")), + opttest.YarnLink("@pulumi/aws-native"), + } + test := pulumitest.NewPulumiTest(t, filepath.Join(cwd, "cfn-custom-resource"), options...) + test.SetConfig(t, "amiRegion", "us-west-2") + + previewResult := test.Preview(t) + t.Logf("#%v", previewResult.ChangeSummary) + + upResult := test.Up(t) + t.Logf("#%v", upResult.Summary) + crossTest(t, upResult.Outputs) + + previewResult = test.Preview(t) + assertpreview.HasNoChanges(t, previewResult) + + test.SetConfig(t, "amiRegion", "us-east-1") + upResult = test.Up(t) + t.Logf("#%v", upResult.Summary) + crossTest(t, upResult.Outputs) + + previewResult = test.Preview(t) + assertpreview.HasNoChanges(t, previewResult) + + test.Destroy(t) +} + func TestVpcCidrs(t *testing.T) { test := getJSBaseOptions(t). With(integration.ProgramTestOptions{ diff --git a/examples/go.mod b/examples/go.mod index ef615c28d3..a91a985f72 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -3,7 +3,9 @@ module github.com/pulumi/pulumi-aws-native/examples go 1.21 require ( + github.com/pulumi/providertest v0.1.3 github.com/pulumi/pulumi/pkg/v3 v3.138.0 + github.com/pulumi/pulumi/sdk/v3 v3.138.0 github.com/stretchr/testify v1.9.0 ) @@ -61,6 +63,7 @@ require ( github.com/edsrzf/mmap-go v1.1.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-git/go-git/v5 v5.12.0 // indirect @@ -115,6 +118,7 @@ require ( github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/natefinch/atomic v1.0.1 // indirect + github.com/nxadm/tail v1.4.11 // indirect github.com/opentracing/basictracer-go v1.1.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pgavlin/fx v0.1.6 // indirect @@ -126,7 +130,6 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 // indirect github.com/pulumi/esc v0.10.0 // indirect - github.com/pulumi/pulumi/sdk/v3 v3.138.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect @@ -173,6 +176,7 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/grpc v1.67.1 // indirect google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/frand v1.4.2 // indirect diff --git a/examples/go.sum b/examples/go.sum index f7e7a8403c..0d4e45dd57 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -143,6 +143,15 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gkampitakis/ciinfo v0.3.0 h1:gWZlOC2+RYYttL0hBqcoQhM7h1qNkVqvRCV1fOvpAv8= +github.com/gkampitakis/ciinfo v0.3.0/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.4.9 h1:x6+GEQeYWC+cnLNsHK5uXXgEQADmlH/1EqMrjfXjzk8= +github.com/gkampitakis/go-snaps v0.4.9/go.mod h1:8HW4KX3JKV8M0GSw69CvT+Jqhd1AlBPMPpBfjBI3bdY= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -313,6 +322,8 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/opentracing/basictracer-go v1.1.0 h1:Oa1fTSBvAl8pa3U+IJYqrKm0NALwH9OsgwOqDv4xJW0= @@ -339,6 +350,8 @@ github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 h1:vkHw5I/plNdTr435 github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231/go.mod h1:murToZ2N9hNJzewjHBgfFdXhZKjY3z5cYC1VXk+lbFE= github.com/pulumi/esc v0.10.0 h1:jzBKzkLVW0mePeanDRfqSQoCJ5yrkux0jIwAkUxpRKE= github.com/pulumi/esc v0.10.0/go.mod h1:2Bfa+FWj/xl8CKqRTWbWgDX0SOD4opdQgvYSURTGK2c= +github.com/pulumi/providertest v0.1.3 h1:GpNKRy/haNjRHiUA9bi4diU4Op2zf3axYXbga5AepHg= +github.com/pulumi/providertest v0.1.3/go.mod h1:GcsqEGgSngwaNOD+kICJPIUQlnA911fGBU8HDlJvVL0= github.com/pulumi/pulumi/pkg/v3 v3.138.0 h1:a+MMvCrvsju4YFVYEwPBtcW7XqsEIV3B+FMblisxEkM= github.com/pulumi/pulumi/pkg/v3 v3.138.0/go.mod h1:xpaeNeKmM2KLafWwm8TlvJGbWtwEwlrK88U6FvXucpY= github.com/pulumi/pulumi/sdk/v3 v3.138.0 h1:1feN0YU1dHnbNw+cHaenmx3AgU0DEiKQbvjxaGQuShk= @@ -366,6 +379,8 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -388,6 +403,14 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U= github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8= +github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= +github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= @@ -506,6 +529,7 @@ golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -599,6 +623,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/provider/cmd/pulumi-resource-aws-native/schema.json b/provider/cmd/pulumi-resource-aws-native/schema.json index 2f102f0fad..46d0141770 100644 --- a/provider/cmd/pulumi-resource-aws-native/schema.json +++ b/provider/cmd/pulumi-resource-aws-native/schema.json @@ -181221,6 +181221,87 @@ "trainingData" ] }, + "aws-native:cloudformation:CustomResourceEmulator": { + "description": "The Custom Resource Emulator allows you to use AWS CloudFormation Custom Resources directly in your Pulumi programs. It provides a way to invoke AWS Lambda functions that implement custom provisioning logic following the CloudFormation Custom Resource protocol.\n\n\u003e **Note**: Currently, only Lambda-backed Custom Resources are supported. SNS-backed Custom Resources are not supported at this time.\n\n## Example Usage\n\n```typescript\nimport * as aws from \"@pulumi/aws-native\";\n\nconst bucket = new aws.s3.Bucket('custom-resource-emulator');\n\n// Create a Custom Resource that invokes a Lambda function\nconst cr = new aws.cloudformation.CustomResourceEmulator('cr', {\n bucketName: bucket.id,\n bucketKeyPrefix: 'custom-resource-emulator',\n customResourceProperties: {\n hello: \"world\"\n },\n serviceToken: \"arn:aws:lambda:us-west-2:123456789012:function:my-custom-resource\",\n resourceType: 'Custom::MyResource',\n}, { customTimeouts: { create: '5m', update: '5m', delete: '5m' } });\n\n// Access the response data\nexport const customResourceData = customResource.data;\n```\n\n## About CloudFormation Custom Resources\n\nCloudFormation Custom Resources allow you to write custom provisioning logic for resources that aren't directly available as AWS CloudFormation resource types. Common use cases include:\n\n- Implementing complex provisioning logic\n- Performing custom validations or transformations\n- Integrating with third-party services\n- Implementing organization-specific infrastructure patterns\n\nFor more information about CloudFormation Custom Resources, see [Custom Resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html) in the AWS CloudFormation User Guide.\n\n## Permissions\n\nThe IAM principal used by your Pulumi program must have the following permissions:\n\n1. `lambda:InvokeFunction` on the Lambda function specified in `serviceToken`\n2. S3 permissions on the bucket specified in `bucketName`:\n - `s3:PutObject`\n - `s3:GetObject`\n - `s3:HeadObject`\n\n## Lambda Function Requirements\n\nThe Lambda function specified in `serviceToken` must implement the CloudFormation Custom Resource lifecycle.\nFor detailed information about implementing Lambda-backed Custom Resources, see [AWS Lambda-backed Custom Resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html) in the AWS CloudFormation User Guide.\n\n## Timeouts\n\nCustom Resources have a default timeout of 60 minutes, matching the CloudFormation timeout for custom resource operations. You can customize it using the [`customTimeouts`](https://www.pulumi.com/docs/iac/concepts/options/customtimeouts/) resource option.\n", + "properties": { + "bucket": { + "type": "string", + "description": "The name of the S3 bucket to use for storing the response from the Custom Resource." + }, + "data": { + "type": "object", + "additionalProperties": { + "$ref": "pulumi.json#/Any" + }, + "description": "The response data returned by invoking the Custom Resource." + }, + "noEcho": { + "type": "boolean", + "description": "Whether the response data contains sensitive information that should be marked as secret and not logged." + }, + "physicalResourceId": { + "type": "string", + "description": "The name or unique identifier that corresponds to the `PhysicalResourceId` included in the Custom Resource response. If no `PhysicalResourceId` is provided in the response, a random ID will be generated." + }, + "resourceType": { + "type": "string", + "description": "The CloudFormation type of the Custom Resource provider. For example, `Custom::MyCustomResource`." + }, + "serviceToken": { + "type": "string", + "description": "The service token, such as a Lambda function ARN, that is invoked when the resource is created, updated, or deleted." + }, + "stackId": { + "type": "string", + "description": "A stand-in value for the CloudFormation stack ID." + } + }, + "required": [ + "physicalResourceId", + "data", + "stackId", + "serviceToken", + "bucket", + "resourceType", + "noEcho" + ], + "inputProperties": { + "bucketKeyPrefix": { + "type": "string", + "description": "The prefix to use for the bucket key when storing the response from the Custom Resource provider." + }, + "bucketName": { + "type": "string", + "description": "The name of the S3 bucket to use for storing the response from the Custom Resource.\n\nThe IAM principal configured for the provider must have `s3:PutObject`, `s3:HeadObject` and `s3:GetObject` permissions on this bucket." + }, + "customResourceProperties": { + "type": "object", + "additionalProperties": { + "$ref": "pulumi.json#/Any" + }, + "description": "The properties to pass as an input to the Custom Resource.\nThe properties are passed as a map of key-value pairs whereas all primitive values (number, boolean) are converted to strings for CloudFormation interoperability." + }, + "resourceType": { + "type": "string", + "description": "The CloudFormation type of the Custom Resource. For example, `Custom::MyCustomResource`.\nThis is required for CloudFormation interoperability." + }, + "serviceToken": { + "type": "string", + "description": "The service token to use for the Custom Resource. The service token is invoked when the resource is created, updated, or deleted.\nThis can be a Lambda Function ARN with optional version or alias identifiers.\n\nThe IAM principal configured for the provider must have `lambda:InvokeFunction` permissions on this service token." + }, + "stackId": { + "type": "string", + "description": "A stand-in value for the CloudFormation stack ID. This is required for CloudFormation interoperability.\nIf not provided, the Pulumi Stack ID is used." + } + }, + "requiredInputs": [ + "bucketName", + "bucketKeyPrefix", + "serviceToken", + "customResourceProperties", + "resourceType" + ] + }, "aws-native:cloudformation:HookDefaultVersion": { "description": "Set a version as default version for a hook in CloudFormation Registry.\n\n{{% examples %}}\n## Example Usage\n{{% example %}}\n### Example\n```csharp\nusing System.Collections.Generic;\nusing System.Linq;\nusing Pulumi;\nusing AwsNative = Pulumi.AwsNative;\n\nreturn await Deployment.RunAsync(() =\u003e \n{\n var hookDefaultVersion = new AwsNative.CloudFormation.HookDefaultVersion(\"hookDefaultVersion\", new()\n {\n TypeVersionArn = \"arn:aws:cloudformation:us-west-2:123456789012:type/hook/My-Sample-Hook/00000001\",\n });\n\n});\n\n\n```\n\n```go\npackage main\n\nimport (\n\t\"github.com/pulumi/pulumi-aws-native/sdk/go/aws/cloudformation\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"\n)\n\nfunc main() {\n\tpulumi.Run(func(ctx *pulumi.Context) error {\n\t\t_, err := cloudformation.NewHookDefaultVersion(ctx, \"hookDefaultVersion\", \u0026cloudformation.HookDefaultVersionArgs{\n\t\t\tTypeVersionArn: pulumi.String(\"arn:aws:cloudformation:us-west-2:123456789012:type/hook/My-Sample-Hook/00000001\"),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n\n```\n\n```typescript\nimport * as pulumi from \"@pulumi/pulumi\";\nimport * as aws_native from \"@pulumi/aws-native\";\n\nconst hookDefaultVersion = new aws_native.cloudformation.HookDefaultVersion(\"hookDefaultVersion\", {typeVersionArn: \"arn:aws:cloudformation:us-west-2:123456789012:type/hook/My-Sample-Hook/00000001\"});\n\n```\n\n```python\nimport pulumi\nimport pulumi_aws_native as aws_native\n\nhook_default_version = aws_native.cloudformation.HookDefaultVersion(\"hookDefaultVersion\", type_version_arn=\"arn:aws:cloudformation:us-west-2:123456789012:type/hook/My-Sample-Hook/00000001\")\n\n```\n\n{{% /example %}}\n{{% example %}}\n### Example\n```csharp\nusing System.Collections.Generic;\nusing System.Linq;\nusing Pulumi;\nusing AwsNative = Pulumi.AwsNative;\n\nreturn await Deployment.RunAsync(() =\u003e \n{\n var hookDefaultVersion = new AwsNative.CloudFormation.HookDefaultVersion(\"hookDefaultVersion\", new()\n {\n TypeVersionArn = \"arn:aws:cloudformation:us-west-2:123456789012:type/hook/My-Sample-Hook/00000001\",\n });\n\n});\n\n\n```\n\n```go\npackage main\n\nimport (\n\t\"github.com/pulumi/pulumi-aws-native/sdk/go/aws/cloudformation\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"\n)\n\nfunc main() {\n\tpulumi.Run(func(ctx *pulumi.Context) error {\n\t\t_, err := cloudformation.NewHookDefaultVersion(ctx, \"hookDefaultVersion\", \u0026cloudformation.HookDefaultVersionArgs{\n\t\t\tTypeVersionArn: pulumi.String(\"arn:aws:cloudformation:us-west-2:123456789012:type/hook/My-Sample-Hook/00000001\"),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n\n```\n\n```typescript\nimport * as pulumi from \"@pulumi/pulumi\";\nimport * as aws_native from \"@pulumi/aws-native\";\n\nconst hookDefaultVersion = new aws_native.cloudformation.HookDefaultVersion(\"hookDefaultVersion\", {typeVersionArn: \"arn:aws:cloudformation:us-west-2:123456789012:type/hook/My-Sample-Hook/00000001\"});\n\n```\n\n```python\nimport pulumi\nimport pulumi_aws_native as aws_native\n\nhook_default_version = aws_native.cloudformation.HookDefaultVersion(\"hookDefaultVersion\", type_version_arn=\"arn:aws:cloudformation:us-west-2:123456789012:type/hook/My-Sample-Hook/00000001\")\n\n```\n\n{{% /example %}}\n{{% example %}}\n### Example\n```csharp\nusing System.Collections.Generic;\nusing System.Linq;\nusing Pulumi;\nusing AwsNative = Pulumi.AwsNative;\n\nreturn await Deployment.RunAsync(() =\u003e \n{\n var hookDefaultVersion = new AwsNative.CloudFormation.HookDefaultVersion(\"hookDefaultVersion\", new()\n {\n TypeName = \"My::Sample::Hook\",\n VersionId = \"1\",\n });\n\n});\n\n\n```\n\n```go\npackage main\n\nimport (\n\t\"github.com/pulumi/pulumi-aws-native/sdk/go/aws/cloudformation\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"\n)\n\nfunc main() {\n\tpulumi.Run(func(ctx *pulumi.Context) error {\n\t\t_, err := cloudformation.NewHookDefaultVersion(ctx, \"hookDefaultVersion\", \u0026cloudformation.HookDefaultVersionArgs{\n\t\t\tTypeName: pulumi.String(\"My::Sample::Hook\"),\n\t\t\tVersionId: pulumi.String(\"1\"),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n\n```\n\n```typescript\nimport * as pulumi from \"@pulumi/pulumi\";\nimport * as aws_native from \"@pulumi/aws-native\";\n\nconst hookDefaultVersion = new aws_native.cloudformation.HookDefaultVersion(\"hookDefaultVersion\", {\n typeName: \"My::Sample::Hook\",\n versionId: \"1\",\n});\n\n```\n\n```python\nimport pulumi\nimport pulumi_aws_native as aws_native\n\nhook_default_version = aws_native.cloudformation.HookDefaultVersion(\"hookDefaultVersion\",\n type_name=\"My::Sample::Hook\",\n version_id=\"1\")\n\n```\n\n{{% /example %}}\n{{% example %}}\n### Example\n```csharp\nusing System.Collections.Generic;\nusing System.Linq;\nusing Pulumi;\nusing AwsNative = Pulumi.AwsNative;\n\nreturn await Deployment.RunAsync(() =\u003e \n{\n var hookDefaultVersion = new AwsNative.CloudFormation.HookDefaultVersion(\"hookDefaultVersion\", new()\n {\n TypeName = \"My::Sample::Hook\",\n VersionId = \"1\",\n });\n\n});\n\n\n```\n\n```go\npackage main\n\nimport (\n\t\"github.com/pulumi/pulumi-aws-native/sdk/go/aws/cloudformation\"\n\t\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"\n)\n\nfunc main() {\n\tpulumi.Run(func(ctx *pulumi.Context) error {\n\t\t_, err := cloudformation.NewHookDefaultVersion(ctx, \"hookDefaultVersion\", \u0026cloudformation.HookDefaultVersionArgs{\n\t\t\tTypeName: pulumi.String(\"My::Sample::Hook\"),\n\t\t\tVersionId: pulumi.String(\"1\"),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n\n```\n\n```typescript\nimport * as pulumi from \"@pulumi/pulumi\";\nimport * as aws_native from \"@pulumi/aws-native\";\n\nconst hookDefaultVersion = new aws_native.cloudformation.HookDefaultVersion(\"hookDefaultVersion\", {\n typeName: \"My::Sample::Hook\",\n versionId: \"1\",\n});\n\n```\n\n```python\nimport pulumi\nimport pulumi_aws_native as aws_native\n\nhook_default_version = aws_native.cloudformation.HookDefaultVersion(\"hookDefaultVersion\",\n type_name=\"My::Sample::Hook\",\n version_id=\"1\")\n\n```\n\n{{% /example %}}\n{{% /examples %}}\n", "properties": { diff --git a/provider/pkg/provider/provider.go b/provider/pkg/provider/provider.go index 1146176b63..c058c243f7 100644 --- a/provider/pkg/provider/provider.go +++ b/provider/pkg/provider/provider.go @@ -39,6 +39,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudcontrol" "github.com/aws/aws-sdk-go-v2/service/cloudformation" "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/lambda" + "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/ssm" "github.com/aws/aws-sdk-go-v2/service/sts" ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" @@ -111,11 +113,13 @@ type cfnProvider struct { pulumiSchema []byte - cfn *cloudformation.Client - ccc client.CloudControlClient - ec2 *ec2.Client - ssm *ssm.Client - sts *sts.Client + cfn *cloudformation.Client + ccc client.CloudControlClient + ec2 *ec2.Client + ssm *ssm.Client + sts *sts.Client + s3 *s3.Client + lambda *lambda.Client customResources map[string]resources.CustomResource } @@ -488,6 +492,8 @@ func (p *cfnProvider) Configure(ctx context.Context, req *pulumirpc.ConfigureReq p.ec2 = ec2.NewFromConfig(cfg) p.ssm = ssm.NewFromConfig(cfg) p.sts = sts.NewFromConfig(cfg) + p.s3 = s3.NewFromConfig(cfg) + p.lambda = lambda.NewFromConfig(cfg) if !skipCredentialsValidation { callerIdentityResp, err := p.sts.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) @@ -505,8 +511,12 @@ func (p *cfnProvider) Configure(ctx context.Context, req *pulumirpc.ConfigureReq } } + lambdaClient := client.NewLambdaClient(p.lambda) + s3Client := client.NewS3Client(p.s3, s3.NewPresignClient(p.s3)) + p.customResources = map[string]resources.CustomResource{ metadata.ExtensionResourceToken: resources.NewExtensionResource(p.ccc), + metadata.CfnCustomResourceToken: resources.NewCfnCustomResource(p.name, s3Client, lambdaClient), } p.configured = true @@ -1125,7 +1135,7 @@ func (p *cfnProvider) Delete(ctx context.Context, req *pulumirpc.DeleteRequest) KeepSecrets: true, }) if err != nil { - return nil, errors.Wrapf(err, "failed to parse inputs for update") + return nil, errors.Wrapf(err, "failed to parse inputs for delete") } // Retrieve the state. diff --git a/provider/pkg/resources/cfn_custom_resource.go b/provider/pkg/resources/cfn_custom_resource.go index eb178e8b71..cafa606b27 100644 --- a/provider/pkg/resources/cfn_custom_resource.go +++ b/provider/pkg/resources/cfn_custom_resource.go @@ -60,21 +60,21 @@ var _ CustomResource = (*cfnCustomResource)(nil) // - Implementing organization-specific infrastructure patterns // // The Custom Resource implementation (i.e. the Lambda function) is responsible for: -// - Processing the request based on the RequestType -// - Sending a response to the pre-signed URL. The response should include: -// - success/failure status and a reason -// - a PhysicalResourceId for the resource -// - optionally return data that can be referenced by other resources +// - Processing the request based on the RequestType +// - Sending a response to the pre-signed URL. The response should include: +// - success/failure status and a reason +// - a PhysicalResourceId for the resource +// - optionally return data that can be referenced by other resources // // Example CloudFormation Custom Resource: -// Resources: -// MyCustomResource: -// Type: Custom::MyResource -// Properties: -// ServiceToken: arn:aws:lambda:region:account:function:name -// Property1: value1 -// Property2: value2 // +// Resources: +// MyCustomResource: +// Type: Custom::MyResource +// Properties: +// ServiceToken: arn:aws:lambda:region:account:function:name +// Property1: value1 +// Property2: value2 // // This emulator implements this lifecycle in Pulumi by: // - Translating Pulumi resource operations to CloudFormation custom resource requests @@ -82,18 +82,18 @@ var _ CustomResource = (*cfnCustomResource)(nil) // - Handling timeout and error scenarios according to CloudFormation specifications // // Architecture: -// ┌──────────────┐ ┌─────────────┐ ┌──────────────┐ -// │ Pulumi │ CRUD │ Custom │ AWS │ Lambda │ -// │ Engine ├────────►│ Resource ├────────►│ Function │ -// │ │ │ Emulator │ │ │ -// └──────────────┘ └─────────────┘ └──────────────┘ -// │ │ -// │ │ -// │ ┌──────────┐ │ -// └─────►│ S3 │◄──────┘ -// Poll for │ Bucket │ Response -// Response └──────────┘ // +// ┌──────────────┐ ┌─────────────┐ ┌──────────────┐ +// │ Pulumi │ CRUD │ Custom │ AWS │ Lambda │ +// │ Engine ├────────►│ Resource ├────────►│ Function │ +// │ │ │ Emulator │ │ │ +// └──────────────┘ └─────────────┘ └──────────────┘ +// │ │ +// │ │ +// │ ┌──────────┐ │ +// └─────►│ S3 │◄──────┘ +// Poll for │ Bucket │ Response +// Response └──────────┘ func NewCfnCustomResource(providerName string, s3Client client.S3Client, lambdaClient client.LambdaClient) *cfnCustomResource { return &cfnCustomResource{ providerName: providerName, @@ -139,10 +139,10 @@ func (s CfnCustomResourceState) ToPropertyMap() resource.PropertyMap { return resource.NewPropertyMap(s) } -func CfnCustomResourceSpec() pschema.ResourceSpec { +func CfnCustomResourceSpec(description string) pschema.ResourceSpec { return pschema.ResourceSpec{ ObjectTypeSpec: pschema.ObjectTypeSpec{ - Description: "TODO: pulumi/pulumi-cdk#109", + Description: description, Properties: map[string]pschema.PropertySpec{ "physicalResourceId": { Description: "The name or unique identifier that corresponds to the `PhysicalResourceId` included in the Custom Resource response. If no `PhysicalResourceId` is provided in the response, a random ID will be generated.", @@ -241,23 +241,20 @@ func (c *cfnCustomResource) Check(ctx context.Context, urn urn.URN, randomSeed [ var failures []ValidationFailure - if !lambdaFunctionArnRegex.MatchString(typedInputs.ServiceToken) { + serviceTokenInput, hasServiceToken := inputs[resource.PropertyKey("serviceToken")] + if (!hasServiceToken || !serviceTokenInput.ContainsUnknowns()) && !lambdaFunctionArnRegex.MatchString(typedInputs.ServiceToken) { failures = append(failures, ValidationFailure{ Path: "serviceToken", Reason: "serviceToken must be a valid Lambda function ARN.", }) } - if typedInputs.StackID == nil { + _, hasStackID := inputs[resource.PropertyKey("stackId")] + if !hasStackID && typedInputs.StackID == nil { // if the stack ID is not provided, we use the pulumi stack ID as the stack ID inputs[resource.PropertyKey("stackId")] = resource.NewStringProperty(urn.Stack().String()) } - if typedInputs.CustomResourceProperties != nil { - stringifiedCustomResourceProperties := naming.ToStringifiedMap(typedInputs.CustomResourceProperties) - inputs[resource.PropertyKey("customResourceProperties")] = resource.PropertyValue{V: resource.NewPropertyMapFromMap(stringifiedCustomResourceProperties)} - } - return inputs, failures, nil } @@ -266,32 +263,33 @@ func (c *cfnCustomResource) Check(ctx context.Context, urn urn.URN, randomSeed [ // // Creation Flow: // -// ┌─────────┐ ┌─────────────┐ ┌───────────────┐ ┌──────────┐ -// │ Create │ │Generate S3 │ │ Invoke Lambda │ │ Wait for │ -// │ Request ├──►│Presigned URL├──►│ with CREATE ├──►│ Response │ -// └─────────┘ └─────────────┘ │ RequestType │ └────┬─────┘ -// └───────────────┘ │ -// ▼ -// ┌───────────────┐ -// │Return Physical│ -// │Resource ID & │ -// │Outputs │ -// └───────────────┘ +// ┌─────────┐ ┌─────────────┐ ┌───────────────┐ ┌──────────┐ +// │ Create │ │Generate S3 │ │ Invoke Lambda │ │ Wait for │ +// │ Request ├──►│Presigned URL├──►│ with CREATE ├──►│ Response │ +// └─────────┘ └─────────────┘ │ RequestType │ └────┬─────┘ +// └───────────────┘ │ +// ▼ +// ┌───────────────┐ +// │Return Physical│ +// │Resource ID & │ +// │Outputs │ +// └───────────────┘ // // The Create operation: // 1. Generates a presigned S3 URL for response collection // 2. Constructs a CloudFormation CREATE event with: -// - Unique RequestID (UUID) -// - ResponseURL (presigned S3 URL) -// - ResourceType from input -// - LogicalResourceId from Pulumi URN -// - Custom properties from input +// - Unique RequestID (UUID) +// - ResponseURL (presigned S3 URL) +// - ResourceType from input +// - LogicalResourceId from Pulumi URN +// - Custom properties from input +// // 3. Invokes the Lambda function asynchronously // 4. Waits for response in S3 bucket // 5. Processes response: -// - On success: Returns PhysicalResourceId and properties -// - On failure: Returns error with reason -// - Handles `NoEcho` for sensitive data +// - On success: Returns PhysicalResourceId and properties +// - On failure: Returns error with reason +// - Handles `NoEcho` for sensitive data func (c *cfnCustomResource) Create(ctx context.Context, urn urn.URN, inputs resource.PropertyMap, timeout time.Duration) (*string, resource.PropertyMap, error) { var typedInputs CfnCustomResourceInputs _, err := resourcex.Unmarshal(&typedInputs, inputs, resourcex.UnmarshalOptions{}) @@ -306,7 +304,7 @@ func (c *cfnCustomResource) Create(ctx context.Context, urn urn.URN, inputs reso ResourceType: typedInputs.ResourceType, LogicalResourceID: urn.Name(), StackID: *typedInputs.StackID, - ResourceProperties: typedInputs.CustomResourceProperties, + ResourceProperties: naming.ToStringifiedMap(typedInputs.CustomResourceProperties), } response, err := c.invokeCustomResource(ctx, customResourceInvokeData{ @@ -348,27 +346,28 @@ func (c *cfnCustomResource) Create(ctx context.Context, urn urn.URN, inputs reso // // Delete Flow: // -// ┌─────────┐ ┌─────────────┐ ┌───────────────┐ ┌──────────┐ -// │ Delete │ │Generate S3 │ │ Invoke Lambda │ │ Wait for │ -// │ Request ├──►│Presigned URL├──►│ with DELETE ├──►│Response │ -// └─────────┘ └─────────────┘ │ RequestType │ └────┬─────┘ -// └───────────────┘ │ -// ▼ -// ┌───────────────┐ -// │Verify Delete │ -// │Success │ -// └───────────────┘ +// ┌─────────┐ ┌─────────────┐ ┌───────────────┐ ┌──────────┐ +// │ Delete │ │Generate S3 │ │ Invoke Lambda │ │ Wait for │ +// │ Request ├──►│Presigned URL├──►│ with DELETE ├──►│Response │ +// └─────────┘ └─────────────┘ │ RequestType │ └────┬─────┘ +// └───────────────┘ │ +// ▼ +// ┌───────────────┐ +// │Verify Delete │ +// │Success │ +// └───────────────┘ // // The Delete operation: // 1. Generates a presigned S3 URL for response collection // 2. Constructs CloudFormation DELETE event with: -// - Existing PhysicalResourceId -// - Current ResourceProperties -// - All standard CloudFormation fields +// - Existing PhysicalResourceId +// - Current ResourceProperties +// - All standard CloudFormation fields +// // 3. Invokes Lambda and waits for response // 4. Handles response: -// - Success: Completes deletion -// - Failure: Returns error with reason from Lambda +// - Success: Completes deletion +// - Failure: Returns error with reason from Lambda func (c *cfnCustomResource) Delete(ctx context.Context, urn urn.URN, id string, inputs, state resource.PropertyMap, timeout time.Duration) error { var typedInputs CfnCustomResourceInputs _, err := resourcex.Unmarshal(&typedInputs, inputs, resourcex.UnmarshalOptions{}) @@ -390,7 +389,7 @@ func (c *cfnCustomResource) Delete(ctx context.Context, urn urn.URN, id string, ResourceType: typedInputs.ResourceType, LogicalResourceID: urn.Name(), StackID: *typedInputs.StackID, - ResourceProperties: typedInputs.CustomResourceProperties, + ResourceProperties: naming.ToStringifiedMap(typedInputs.CustomResourceProperties), } response, err := c.invokeCustomResource(ctx, customResourceInvokeData{ @@ -421,42 +420,44 @@ func (c *cfnCustomResource) Delete(ctx context.Context, urn urn.URN, id string, // // Update Flow: // -// ┌─────────┐ ┌─────────────┐ ┌───────────────┐ ┌──────────┐ -// │ Update │ │Generate S3 │ │ Invoke Lambda │ │ Wait for │ -// │ Request ├──►│Presigned URL├──►│ with UPDATE ├──►│ Response │ -// └─────────┘ └─────────────┘ │ RequestType │ └────┬─────┘ -// └───────────────┘ │ -// ▼ -// ┌───────────────┐ -// │Check Physical │ -// │Resource ID │ -// └───────┬───────┘ -// │ -// ▼ -// ┌───────────────┐ -// │Delete Old │ -// │Resource │ -// │(if ID changed)│ -// └───────┬───────┘ -// │ -// ▼ -// ┌───────────────┐ -// │Return updated │ -// │Physical │ -// │Resource ID & │ -// │Outputs │ -// └───────────────┘ +// ┌─────────┐ ┌─────────────┐ ┌───────────────┐ ┌──────────┐ +// │ Update │ │Generate S3 │ │ Invoke Lambda │ │ Wait for │ +// │ Request ├──►│Presigned URL├──►│ with UPDATE ├──►│ Response │ +// └─────────┘ └─────────────┘ │ RequestType │ └────┬─────┘ +// └───────────────┘ │ +// ▼ +// ┌───────────────┐ +// │Check Physical │ +// │Resource ID │ +// └───────┬───────┘ +// │ +// ▼ +// ┌───────────────┐ +// │Delete Old │ +// │Resource │ +// │(if ID changed)│ +// └───────┬───────┘ +// │ +// ▼ +// ┌───────────────┐ +// │Return updated │ +// │Physical │ +// │Resource ID & │ +// │Outputs │ +// └───────────────┘ // // The Update operation: // 1. Generates a presigned S3 URL for response collection // 2. Constructs a CloudFormation UPDATE event with: -// - Existing PhysicalResourceId -// - Old and new ResourceProperties -// - All standard CloudFormation fields +// - Existing PhysicalResourceId +// - Old and new ResourceProperties +// - All standard CloudFormation fields +// // 3. Invokes Lambda and collects response // 4. If PhysicalResourceId changes: -// - Initiates cleanup of old resource -// - Sends DELETE event for old PhysicalResourceId +// - Initiates cleanup of old resource +// - Sends DELETE event for old PhysicalResourceId +// // 5. Returns updated properties and new PhysicalResourceId func (c *cfnCustomResource) Update(ctx context.Context, urn urn.URN, id string, inputs, oldInputs, state resource.PropertyMap, timeout time.Duration) (resource.PropertyMap, error) { var oldTypedInputs CfnCustomResourceInputs @@ -485,8 +486,8 @@ func (c *cfnCustomResource) Update(ctx context.Context, urn urn.URN, id string, ResourceType: newTypedInputs.ResourceType, LogicalResourceID: urn.Name(), StackID: *newTypedInputs.StackID, - ResourceProperties: newTypedInputs.CustomResourceProperties, - OldResourceProperties: oldTypedInputs.CustomResourceProperties, + ResourceProperties: naming.ToStringifiedMap(newTypedInputs.CustomResourceProperties), + OldResourceProperties: naming.ToStringifiedMap(oldTypedInputs.CustomResourceProperties), } startTime := c.clock.Now() @@ -522,7 +523,7 @@ func (c *cfnCustomResource) Update(ctx context.Context, urn urn.URN, id string, ResourceType: typedState.ResourceType, LogicalResourceID: urn.Name(), StackID: typedState.StackID, - ResourceProperties: oldTypedInputs.CustomResourceProperties, + ResourceProperties: naming.ToStringifiedMap(oldTypedInputs.CustomResourceProperties), } deleteTimeout := DefaultCustomResourceTimeout diff --git a/provider/pkg/resources/cfn_custom_resource_test.go b/provider/pkg/resources/cfn_custom_resource_test.go index a06ab6d2c5..6fa247b733 100644 --- a/provider/pkg/resources/cfn_custom_resource_test.go +++ b/provider/pkg/resources/cfn_custom_resource_test.go @@ -12,6 +12,7 @@ import ( "github.com/aws/aws-lambda-go/cfn" "github.com/pulumi/pulumi-aws-native/provider/pkg/client" + "github.com/pulumi/pulumi-aws-native/provider/pkg/naming" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/pulumi/pulumi/sdk/v3/go/common/resource/urn" "github.com/stretchr/testify/assert" @@ -79,56 +80,32 @@ func TestCfnCustomResource_Check(t *testing.T) { inputs: resource.PropertyMap{ "serviceToken": resource.NewStringProperty("arn:aws:lambda:us-west-2:123456789012:function:my-function"), "stackId": resource.NewStringProperty("testProject"), - "customResourceProperties": resource.NewObjectProperty(resource.NewPropertyMapFromMap(map[string]interface{}{ - "level1": map[string]interface{}{ - "level2": []interface{}{ - map[string]interface{}{ - "key1": "value1", - "key2": 2, - }, - 3.14, - "string", - }, - "anotherKey": true, - "arrayOfMaps": []interface{}{ - map[string]interface{}{ - "key1": "value1", - "key2": 2, - }, - map[string]interface{}{ - "key3": "value3", - "key4": 4, - }, - }, - }, - })), }, expectedInputs: resource.PropertyMap{ "serviceToken": resource.NewStringProperty("arn:aws:lambda:us-west-2:123456789012:function:my-function"), "stackId": resource.NewStringProperty("testProject"), - "customResourceProperties": resource.NewObjectProperty(resource.NewPropertyMapFromMap(map[string]interface{}{ - "level1": map[string]interface{}{ - "level2": []interface{}{ - map[string]interface{}{ - "key1": "value1", - "key2": "2", - }, - "3.14", - "string", - }, - "anotherKey": "true", - "arrayOfMaps": []interface{}{ - map[string]interface{}{ - "key1": "value1", - "key2": "2", - }, - map[string]interface{}{ - "key3": "value3", - "key4": "4", - }, - }, - }, - })), + }, + }, + { + name: "Unknown inputs", + inputs: resource.PropertyMap{ + "serviceToken": resource.MakeComputed(resource.NewStringProperty("")), + "stackId": resource.MakeComputed(resource.NewStringProperty("")), + }, + expectedInputs: resource.PropertyMap{ + "serviceToken": resource.MakeComputed(resource.NewStringProperty("")), + "stackId": resource.MakeComputed(resource.NewStringProperty("")), + }, + }, + { + name: "Preserves Secrets", + inputs: resource.PropertyMap{ + "serviceToken": resource.MakeSecret(resource.NewStringProperty("arn:aws:lambda:us-west-2:123456789012:function:my-function")), + "stackId": resource.MakeSecret(resource.NewStringProperty("testProject")), + }, + expectedInputs: resource.PropertyMap{ + "serviceToken": resource.MakeSecret(resource.NewStringProperty("arn:aws:lambda:us-west-2:123456789012:function:my-function")), + "stackId": resource.MakeSecret(resource.NewStringProperty("testProject")), }, }, } @@ -234,6 +211,27 @@ func TestCfnCustomResource_Create(t *testing.T) { "key": "value", }, }, + { + name: "Stringify CustomResourceInputs", + customResourceData: map[string]interface{}{ + "prop1": "value1", + "prop2": true, + "prop3": []interface{}{"a", "b", "c"}, + "prop4": map[string]interface{}{ + "nestedProp1": "nestedValue1", + "nestedProp2": 42, + }, + }, + customResourceInputs: map[string]interface{}{ + "key1": "value1", + "key2": 42, + "key3": true, + "key4": map[string]interface{}{ + "nestedKey1": "nestedValue1", + "nestedKey2": 100, + }, + }, + }, } for _, tt := range tests { @@ -266,7 +264,7 @@ func TestCfnCustomResource_Create(t *testing.T) { ResourceType: resourceType, LogicalResourceID: urn.Name(), StackID: stackID, - ResourceProperties: tt.customResourceInputs, + ResourceProperties: naming.ToStringifiedMap(tt.customResourceInputs), } mockLambdaClient.EXPECT().InvokeAsync( @@ -639,9 +637,11 @@ func TestCfnCustomResource_Update(t *testing.T) { oldResourceProperties := map[string]interface{}{ "inputs": "old", + "key": 42, } newResourceProperties := map[string]interface{}{ "inputs": "new", + "key": 42, } responseUrl := "https://example.com" @@ -652,8 +652,8 @@ func TestCfnCustomResource_Update(t *testing.T) { PhysicalResourceID: physicalResourceID, LogicalResourceID: urn.Name(), StackID: stackID, - ResourceProperties: newResourceProperties, - OldResourceProperties: oldResourceProperties, + ResourceProperties: naming.ToStringifiedMap(newResourceProperties), + OldResourceProperties: naming.ToStringifiedMap(oldResourceProperties), } mockLambdaClient.EXPECT().InvokeAsync( @@ -1251,6 +1251,18 @@ func TestCfnCustomResource_Delete(t *testing.T) { "key": "value", }, }, + { + name: "Stringify CustomResourceInputs", + customResourceInputs: map[string]interface{}{ + "key1": "value1", + "key2": 42, + "key3": true, + "key4": map[string]interface{}{ + "nestedKey1": "nestedValue1", + "nestedKey2": 100, + }, + }, + }, } for _, tt := range tests { @@ -1284,7 +1296,7 @@ func TestCfnCustomResource_Delete(t *testing.T) { LogicalResourceID: urn.Name(), StackID: stackID, PhysicalResourceID: physicalResourceID, - ResourceProperties: tt.customResourceInputs, + ResourceProperties: naming.ToStringifiedMap(tt.customResourceInputs), } mockLambdaClient.EXPECT().InvokeAsync( diff --git a/provider/pkg/schema/docs/content/cfn-custom-resource.md b/provider/pkg/schema/docs/content/cfn-custom-resource.md new file mode 100644 index 0000000000..2bbeac6342 --- /dev/null +++ b/provider/pkg/schema/docs/content/cfn-custom-resource.md @@ -0,0 +1,55 @@ +The Custom Resource Emulator allows you to use AWS CloudFormation Custom Resources directly in your Pulumi programs. It provides a way to invoke AWS Lambda functions that implement custom provisioning logic following the CloudFormation Custom Resource protocol. + +> **Note**: Currently, only Lambda-backed Custom Resources are supported. SNS-backed Custom Resources are not supported at this time. + +## Example Usage + +```typescript +import * as aws from "@pulumi/aws-native"; + +const bucket = new aws.s3.Bucket('custom-resource-emulator'); + +// Create a Custom Resource that invokes a Lambda function +const cr = new aws.cloudformation.CustomResourceEmulator('cr', { + bucketName: bucket.id, + bucketKeyPrefix: 'custom-resource-emulator', + customResourceProperties: { + hello: "world" + }, + serviceToken: "arn:aws:lambda:us-west-2:123456789012:function:my-custom-resource", + resourceType: 'Custom::MyResource', +}, { customTimeouts: { create: '5m', update: '5m', delete: '5m' } }); + +// Access the response data +export const customResourceData = customResource.data; +``` + +## About CloudFormation Custom Resources + +CloudFormation Custom Resources allow you to write custom provisioning logic for resources that aren't directly available as AWS CloudFormation resource types. Common use cases include: + +- Implementing complex provisioning logic +- Performing custom validations or transformations +- Integrating with third-party services +- Implementing organization-specific infrastructure patterns + +For more information about CloudFormation Custom Resources, see [Custom Resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html) in the AWS CloudFormation User Guide. + +## Permissions + +The IAM principal used by your Pulumi program must have the following permissions: + +1. `lambda:InvokeFunction` on the Lambda function specified in `serviceToken` +2. S3 permissions on the bucket specified in `bucketName`: + - `s3:PutObject` + - `s3:GetObject` + - `s3:HeadObject` + +## Lambda Function Requirements + +The Lambda function specified in `serviceToken` must implement the CloudFormation Custom Resource lifecycle. +For detailed information about implementing Lambda-backed Custom Resources, see [AWS Lambda-backed Custom Resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html) in the AWS CloudFormation User Guide. + +## Timeouts + +Custom Resources have a default timeout of 60 minutes, matching the CloudFormation timeout for custom resource operations. You can customize it using the [`customTimeouts`](https://www.pulumi.com/docs/iac/concepts/options/customtimeouts/) resource option. diff --git a/provider/pkg/schema/docs/custom.go b/provider/pkg/schema/docs/custom.go new file mode 100644 index 0000000000..fcd5e5711c --- /dev/null +++ b/provider/pkg/schema/docs/custom.go @@ -0,0 +1,11 @@ +// Copyright 2016-2024, Pulumi Corporation. + +package docs + +import ( + _ "embed" +) + +// TODO[pulumi/pulumi-cdk#109] Add examples for the other languages. +//go:embed content/cfn-custom-resource.md +var CfnCustomResourceEmulatorDocs string diff --git a/provider/pkg/schema/gen.go b/provider/pkg/schema/gen.go index 193abd3f52..6fddcd3b4c 100644 --- a/provider/pkg/schema/gen.go +++ b/provider/pkg/schema/gen.go @@ -17,6 +17,7 @@ import ( "github.com/pulumi/pulumi-aws-native/provider/pkg/metadata" "github.com/pulumi/pulumi-aws-native/provider/pkg/naming" "github.com/pulumi/pulumi-aws-native/provider/pkg/resources" + "github.com/pulumi/pulumi-aws-native/provider/pkg/schema/docs" "github.com/pulumi/pulumi/pkg/v3/codegen" dotnetgen "github.com/pulumi/pulumi/pkg/v3/codegen/dotnet" pschema "github.com/pulumi/pulumi/pkg/v3/codegen/schema" @@ -391,6 +392,7 @@ func GatherPackage(supportedResourceTypes []string, jsonSchemas []*jsschema.Sche }, Resources: map[string]pschema.ResourceSpec{ metadata.ExtensionResourceToken: resources.ExtensionResourceSpec(), + metadata.CfnCustomResourceToken: resources.CfnCustomResourceSpec(docs.CfnCustomResourceEmulatorDocs), }, Functions: map[string]pschema.FunctionSpec{}, Language: map[string]pschema.RawMessage{}, diff --git a/sdk/dotnet/CloudFormation/CustomResourceEmulator.cs b/sdk/dotnet/CloudFormation/CustomResourceEmulator.cs new file mode 100644 index 0000000000..423f1e3db2 --- /dev/null +++ b/sdk/dotnet/CloudFormation/CustomResourceEmulator.cs @@ -0,0 +1,194 @@ +// *** WARNING: this file was generated by pulumi. *** +// *** Do not edit by hand unless you're certain you know what you are doing! *** + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Pulumi.Serialization; + +namespace Pulumi.AwsNative.CloudFormation +{ + /// + /// The Custom Resource Emulator allows you to use AWS CloudFormation Custom Resources directly in your Pulumi programs. It provides a way to invoke AWS Lambda functions that implement custom provisioning logic following the CloudFormation Custom Resource protocol. + /// + /// > **Note**: Currently, only Lambda-backed Custom Resources are supported. SNS-backed Custom Resources are not supported at this time. + /// + /// ## Example Usage + /// + /// ## About CloudFormation Custom Resources + /// + /// CloudFormation Custom Resources allow you to write custom provisioning logic for resources that aren't directly available as AWS CloudFormation resource types. Common use cases include: + /// + /// - Implementing complex provisioning logic + /// - Performing custom validations or transformations + /// - Integrating with third-party services + /// - Implementing organization-specific infrastructure patterns + /// + /// For more information about CloudFormation Custom Resources, see [Custom Resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html) in the AWS CloudFormation User Guide. + /// + /// ## Permissions + /// + /// The IAM principal used by your Pulumi program must have the following permissions: + /// + /// 1. `lambda:InvokeFunction` on the Lambda function specified in `serviceToken` + /// 2. S3 permissions on the bucket specified in `bucketName`: + /// - `s3:PutObject` + /// - `s3:GetObject` + /// - `s3:HeadObject` + /// + /// ## Lambda Function Requirements + /// + /// The Lambda function specified in `serviceToken` must implement the CloudFormation Custom Resource lifecycle. + /// For detailed information about implementing Lambda-backed Custom Resources, see [AWS Lambda-backed Custom Resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html) in the AWS CloudFormation User Guide. + /// + /// ## Timeouts + /// + /// Custom Resources have a default timeout of 60 minutes, matching the CloudFormation timeout for custom resource operations. You can customize it using the [`customTimeouts`](https://www.pulumi.com/docs/iac/concepts/options/customtimeouts/) resource option. + /// + [AwsNativeResourceType("aws-native:cloudformation:CustomResourceEmulator")] + public partial class CustomResourceEmulator : global::Pulumi.CustomResource + { + /// + /// The name of the S3 bucket to use for storing the response from the Custom Resource. + /// + [Output("bucket")] + public Output Bucket { get; private set; } = null!; + + /// + /// The response data returned by invoking the Custom Resource. + /// + [Output("data")] + public Output> Data { get; private set; } = null!; + + /// + /// Whether the response data contains sensitive information that should be marked as secret and not logged. + /// + [Output("noEcho")] + public Output NoEcho { get; private set; } = null!; + + /// + /// The name or unique identifier that corresponds to the `PhysicalResourceId` included in the Custom Resource response. If no `PhysicalResourceId` is provided in the response, a random ID will be generated. + /// + [Output("physicalResourceId")] + public Output PhysicalResourceId { get; private set; } = null!; + + /// + /// The CloudFormation type of the Custom Resource provider. For example, `Custom::MyCustomResource`. + /// + [Output("resourceType")] + public Output ResourceType { get; private set; } = null!; + + /// + /// The service token, such as a Lambda function ARN, that is invoked when the resource is created, updated, or deleted. + /// + [Output("serviceToken")] + public Output ServiceToken { get; private set; } = null!; + + /// + /// A stand-in value for the CloudFormation stack ID. + /// + [Output("stackId")] + public Output StackId { get; private set; } = null!; + + + /// + /// Create a CustomResourceEmulator resource with the given unique name, arguments, and options. + /// + /// + /// The unique name of the resource + /// The arguments used to populate this resource's properties + /// A bag of options that control this resource's behavior + public CustomResourceEmulator(string name, CustomResourceEmulatorArgs args, CustomResourceOptions? options = null) + : base("aws-native:cloudformation:CustomResourceEmulator", name, args ?? new CustomResourceEmulatorArgs(), MakeResourceOptions(options, "")) + { + } + + private CustomResourceEmulator(string name, Input id, CustomResourceOptions? options = null) + : base("aws-native:cloudformation:CustomResourceEmulator", name, null, MakeResourceOptions(options, id)) + { + } + + private static CustomResourceOptions MakeResourceOptions(CustomResourceOptions? options, Input? id) + { + var defaultOptions = new CustomResourceOptions + { + Version = Utilities.Version, + }; + var merged = CustomResourceOptions.Merge(defaultOptions, options); + // Override the ID if one was specified for consistency with other language SDKs. + merged.Id = id ?? merged.Id; + return merged; + } + /// + /// Get an existing CustomResourceEmulator resource's state with the given name, ID, and optional extra + /// properties used to qualify the lookup. + /// + /// + /// The unique name of the resulting resource. + /// The unique provider ID of the resource to lookup. + /// A bag of options that control this resource's behavior + public static CustomResourceEmulator Get(string name, Input id, CustomResourceOptions? options = null) + { + return new CustomResourceEmulator(name, id, options); + } + } + + public sealed class CustomResourceEmulatorArgs : global::Pulumi.ResourceArgs + { + /// + /// The prefix to use for the bucket key when storing the response from the Custom Resource provider. + /// + [Input("bucketKeyPrefix", required: true)] + public Input BucketKeyPrefix { get; set; } = null!; + + /// + /// The name of the S3 bucket to use for storing the response from the Custom Resource. + /// + /// The IAM principal configured for the provider must have `s3:PutObject`, `s3:HeadObject` and `s3:GetObject` permissions on this bucket. + /// + [Input("bucketName", required: true)] + public Input BucketName { get; set; } = null!; + + [Input("customResourceProperties", required: true)] + private InputMap? _customResourceProperties; + + /// + /// The properties to pass as an input to the Custom Resource. + /// The properties are passed as a map of key-value pairs whereas all primitive values (number, boolean) are converted to strings for CloudFormation interoperability. + /// + public InputMap CustomResourceProperties + { + get => _customResourceProperties ?? (_customResourceProperties = new InputMap()); + set => _customResourceProperties = value; + } + + /// + /// The CloudFormation type of the Custom Resource. For example, `Custom::MyCustomResource`. + /// This is required for CloudFormation interoperability. + /// + [Input("resourceType", required: true)] + public Input ResourceType { get; set; } = null!; + + /// + /// The service token to use for the Custom Resource. The service token is invoked when the resource is created, updated, or deleted. + /// This can be a Lambda Function ARN with optional version or alias identifiers. + /// + /// The IAM principal configured for the provider must have `lambda:InvokeFunction` permissions on this service token. + /// + [Input("serviceToken", required: true)] + public Input ServiceToken { get; set; } = null!; + + /// + /// A stand-in value for the CloudFormation stack ID. This is required for CloudFormation interoperability. + /// If not provided, the Pulumi Stack ID is used. + /// + [Input("stackId")] + public Input? StackId { get; set; } + + public CustomResourceEmulatorArgs() + { + } + public static new CustomResourceEmulatorArgs Empty => new CustomResourceEmulatorArgs(); + } +} diff --git a/sdk/go/aws/cloudformation/customResourceEmulator.go b/sdk/go/aws/cloudformation/customResourceEmulator.go new file mode 100644 index 0000000000..1e3e266d87 --- /dev/null +++ b/sdk/go/aws/cloudformation/customResourceEmulator.go @@ -0,0 +1,245 @@ +// Code generated by pulumi-language-go DO NOT EDIT. +// *** WARNING: Do not edit by hand unless you're certain you know what you are doing! *** + +package cloudformation + +import ( + "context" + "reflect" + + "errors" + "github.com/pulumi/pulumi-aws-native/sdk/go/aws/internal" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +// The Custom Resource Emulator allows you to use AWS CloudFormation Custom Resources directly in your Pulumi programs. It provides a way to invoke AWS Lambda functions that implement custom provisioning logic following the CloudFormation Custom Resource protocol. +// +// > **Note**: Currently, only Lambda-backed Custom Resources are supported. SNS-backed Custom Resources are not supported at this time. +// +// ## Example Usage +// +// ## About CloudFormation Custom Resources +// +// CloudFormation Custom Resources allow you to write custom provisioning logic for resources that aren't directly available as AWS CloudFormation resource types. Common use cases include: +// +// - Implementing complex provisioning logic +// - Performing custom validations or transformations +// - Integrating with third-party services +// - Implementing organization-specific infrastructure patterns +// +// For more information about CloudFormation Custom Resources, see [Custom Resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html) in the AWS CloudFormation User Guide. +// +// ## Permissions +// +// The IAM principal used by your Pulumi program must have the following permissions: +// +// 1. `lambda:InvokeFunction` on the Lambda function specified in `serviceToken` +// 2. S3 permissions on the bucket specified in `bucketName`: +// - `s3:PutObject` +// - `s3:GetObject` +// - `s3:HeadObject` +// +// ## Lambda Function Requirements +// +// The Lambda function specified in `serviceToken` must implement the CloudFormation Custom Resource lifecycle. +// For detailed information about implementing Lambda-backed Custom Resources, see [AWS Lambda-backed Custom Resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html) in the AWS CloudFormation User Guide. +// +// ## Timeouts +// +// Custom Resources have a default timeout of 60 minutes, matching the CloudFormation timeout for custom resource operations. You can customize it using the [`customTimeouts`](https://www.pulumi.com/docs/iac/concepts/options/customtimeouts/) resource option. +type CustomResourceEmulator struct { + pulumi.CustomResourceState + + // The name of the S3 bucket to use for storing the response from the Custom Resource. + Bucket pulumi.StringOutput `pulumi:"bucket"` + // The response data returned by invoking the Custom Resource. + Data pulumi.MapOutput `pulumi:"data"` + // Whether the response data contains sensitive information that should be marked as secret and not logged. + NoEcho pulumi.BoolOutput `pulumi:"noEcho"` + // The name or unique identifier that corresponds to the `PhysicalResourceId` included in the Custom Resource response. If no `PhysicalResourceId` is provided in the response, a random ID will be generated. + PhysicalResourceId pulumi.StringOutput `pulumi:"physicalResourceId"` + // The CloudFormation type of the Custom Resource provider. For example, `Custom::MyCustomResource`. + ResourceType pulumi.StringOutput `pulumi:"resourceType"` + // The service token, such as a Lambda function ARN, that is invoked when the resource is created, updated, or deleted. + ServiceToken pulumi.StringOutput `pulumi:"serviceToken"` + // A stand-in value for the CloudFormation stack ID. + StackId pulumi.StringOutput `pulumi:"stackId"` +} + +// NewCustomResourceEmulator registers a new resource with the given unique name, arguments, and options. +func NewCustomResourceEmulator(ctx *pulumi.Context, + name string, args *CustomResourceEmulatorArgs, opts ...pulumi.ResourceOption) (*CustomResourceEmulator, error) { + if args == nil { + return nil, errors.New("missing one or more required arguments") + } + + if args.BucketKeyPrefix == nil { + return nil, errors.New("invalid value for required argument 'BucketKeyPrefix'") + } + if args.BucketName == nil { + return nil, errors.New("invalid value for required argument 'BucketName'") + } + if args.CustomResourceProperties == nil { + return nil, errors.New("invalid value for required argument 'CustomResourceProperties'") + } + if args.ResourceType == nil { + return nil, errors.New("invalid value for required argument 'ResourceType'") + } + if args.ServiceToken == nil { + return nil, errors.New("invalid value for required argument 'ServiceToken'") + } + opts = internal.PkgResourceDefaultOpts(opts) + var resource CustomResourceEmulator + err := ctx.RegisterResource("aws-native:cloudformation:CustomResourceEmulator", name, args, &resource, opts...) + if err != nil { + return nil, err + } + return &resource, nil +} + +// GetCustomResourceEmulator gets an existing CustomResourceEmulator resource's state with the given name, ID, and optional +// state properties that are used to uniquely qualify the lookup (nil if not required). +func GetCustomResourceEmulator(ctx *pulumi.Context, + name string, id pulumi.IDInput, state *CustomResourceEmulatorState, opts ...pulumi.ResourceOption) (*CustomResourceEmulator, error) { + var resource CustomResourceEmulator + err := ctx.ReadResource("aws-native:cloudformation:CustomResourceEmulator", name, id, state, &resource, opts...) + if err != nil { + return nil, err + } + return &resource, nil +} + +// Input properties used for looking up and filtering CustomResourceEmulator resources. +type customResourceEmulatorState struct { +} + +type CustomResourceEmulatorState struct { +} + +func (CustomResourceEmulatorState) ElementType() reflect.Type { + return reflect.TypeOf((*customResourceEmulatorState)(nil)).Elem() +} + +type customResourceEmulatorArgs struct { + // The prefix to use for the bucket key when storing the response from the Custom Resource provider. + BucketKeyPrefix string `pulumi:"bucketKeyPrefix"` + // The name of the S3 bucket to use for storing the response from the Custom Resource. + // + // The IAM principal configured for the provider must have `s3:PutObject`, `s3:HeadObject` and `s3:GetObject` permissions on this bucket. + BucketName string `pulumi:"bucketName"` + // The properties to pass as an input to the Custom Resource. + // The properties are passed as a map of key-value pairs whereas all primitive values (number, boolean) are converted to strings for CloudFormation interoperability. + CustomResourceProperties map[string]interface{} `pulumi:"customResourceProperties"` + // The CloudFormation type of the Custom Resource. For example, `Custom::MyCustomResource`. + // This is required for CloudFormation interoperability. + ResourceType string `pulumi:"resourceType"` + // The service token to use for the Custom Resource. The service token is invoked when the resource is created, updated, or deleted. + // This can be a Lambda Function ARN with optional version or alias identifiers. + // + // The IAM principal configured for the provider must have `lambda:InvokeFunction` permissions on this service token. + ServiceToken string `pulumi:"serviceToken"` + // A stand-in value for the CloudFormation stack ID. This is required for CloudFormation interoperability. + // If not provided, the Pulumi Stack ID is used. + StackId *string `pulumi:"stackId"` +} + +// The set of arguments for constructing a CustomResourceEmulator resource. +type CustomResourceEmulatorArgs struct { + // The prefix to use for the bucket key when storing the response from the Custom Resource provider. + BucketKeyPrefix pulumi.StringInput + // The name of the S3 bucket to use for storing the response from the Custom Resource. + // + // The IAM principal configured for the provider must have `s3:PutObject`, `s3:HeadObject` and `s3:GetObject` permissions on this bucket. + BucketName pulumi.StringInput + // The properties to pass as an input to the Custom Resource. + // The properties are passed as a map of key-value pairs whereas all primitive values (number, boolean) are converted to strings for CloudFormation interoperability. + CustomResourceProperties pulumi.MapInput + // The CloudFormation type of the Custom Resource. For example, `Custom::MyCustomResource`. + // This is required for CloudFormation interoperability. + ResourceType pulumi.StringInput + // The service token to use for the Custom Resource. The service token is invoked when the resource is created, updated, or deleted. + // This can be a Lambda Function ARN with optional version or alias identifiers. + // + // The IAM principal configured for the provider must have `lambda:InvokeFunction` permissions on this service token. + ServiceToken pulumi.StringInput + // A stand-in value for the CloudFormation stack ID. This is required for CloudFormation interoperability. + // If not provided, the Pulumi Stack ID is used. + StackId pulumi.StringPtrInput +} + +func (CustomResourceEmulatorArgs) ElementType() reflect.Type { + return reflect.TypeOf((*customResourceEmulatorArgs)(nil)).Elem() +} + +type CustomResourceEmulatorInput interface { + pulumi.Input + + ToCustomResourceEmulatorOutput() CustomResourceEmulatorOutput + ToCustomResourceEmulatorOutputWithContext(ctx context.Context) CustomResourceEmulatorOutput +} + +func (*CustomResourceEmulator) ElementType() reflect.Type { + return reflect.TypeOf((**CustomResourceEmulator)(nil)).Elem() +} + +func (i *CustomResourceEmulator) ToCustomResourceEmulatorOutput() CustomResourceEmulatorOutput { + return i.ToCustomResourceEmulatorOutputWithContext(context.Background()) +} + +func (i *CustomResourceEmulator) ToCustomResourceEmulatorOutputWithContext(ctx context.Context) CustomResourceEmulatorOutput { + return pulumi.ToOutputWithContext(ctx, i).(CustomResourceEmulatorOutput) +} + +type CustomResourceEmulatorOutput struct{ *pulumi.OutputState } + +func (CustomResourceEmulatorOutput) ElementType() reflect.Type { + return reflect.TypeOf((**CustomResourceEmulator)(nil)).Elem() +} + +func (o CustomResourceEmulatorOutput) ToCustomResourceEmulatorOutput() CustomResourceEmulatorOutput { + return o +} + +func (o CustomResourceEmulatorOutput) ToCustomResourceEmulatorOutputWithContext(ctx context.Context) CustomResourceEmulatorOutput { + return o +} + +// The name of the S3 bucket to use for storing the response from the Custom Resource. +func (o CustomResourceEmulatorOutput) Bucket() pulumi.StringOutput { + return o.ApplyT(func(v *CustomResourceEmulator) pulumi.StringOutput { return v.Bucket }).(pulumi.StringOutput) +} + +// The response data returned by invoking the Custom Resource. +func (o CustomResourceEmulatorOutput) Data() pulumi.MapOutput { + return o.ApplyT(func(v *CustomResourceEmulator) pulumi.MapOutput { return v.Data }).(pulumi.MapOutput) +} + +// Whether the response data contains sensitive information that should be marked as secret and not logged. +func (o CustomResourceEmulatorOutput) NoEcho() pulumi.BoolOutput { + return o.ApplyT(func(v *CustomResourceEmulator) pulumi.BoolOutput { return v.NoEcho }).(pulumi.BoolOutput) +} + +// The name or unique identifier that corresponds to the `PhysicalResourceId` included in the Custom Resource response. If no `PhysicalResourceId` is provided in the response, a random ID will be generated. +func (o CustomResourceEmulatorOutput) PhysicalResourceId() pulumi.StringOutput { + return o.ApplyT(func(v *CustomResourceEmulator) pulumi.StringOutput { return v.PhysicalResourceId }).(pulumi.StringOutput) +} + +// The CloudFormation type of the Custom Resource provider. For example, `Custom::MyCustomResource`. +func (o CustomResourceEmulatorOutput) ResourceType() pulumi.StringOutput { + return o.ApplyT(func(v *CustomResourceEmulator) pulumi.StringOutput { return v.ResourceType }).(pulumi.StringOutput) +} + +// The service token, such as a Lambda function ARN, that is invoked when the resource is created, updated, or deleted. +func (o CustomResourceEmulatorOutput) ServiceToken() pulumi.StringOutput { + return o.ApplyT(func(v *CustomResourceEmulator) pulumi.StringOutput { return v.ServiceToken }).(pulumi.StringOutput) +} + +// A stand-in value for the CloudFormation stack ID. +func (o CustomResourceEmulatorOutput) StackId() pulumi.StringOutput { + return o.ApplyT(func(v *CustomResourceEmulator) pulumi.StringOutput { return v.StackId }).(pulumi.StringOutput) +} + +func init() { + pulumi.RegisterInputType(reflect.TypeOf((*CustomResourceEmulatorInput)(nil)).Elem(), &CustomResourceEmulator{}) + pulumi.RegisterOutputType(CustomResourceEmulatorOutput{}) +} diff --git a/sdk/go/aws/cloudformation/init.go b/sdk/go/aws/cloudformation/init.go index 56cb49ce57..68f5024fc2 100644 --- a/sdk/go/aws/cloudformation/init.go +++ b/sdk/go/aws/cloudformation/init.go @@ -21,6 +21,8 @@ func (m *module) Version() semver.Version { func (m *module) Construct(ctx *pulumi.Context, name, typ, urn string) (r pulumi.Resource, err error) { switch typ { + case "aws-native:cloudformation:CustomResourceEmulator": + r = &CustomResourceEmulator{} case "aws-native:cloudformation:HookDefaultVersion": r = &HookDefaultVersion{} case "aws-native:cloudformation:HookTypeConfig": diff --git a/sdk/nodejs/cloudformation/customResourceEmulator.ts b/sdk/nodejs/cloudformation/customResourceEmulator.ts new file mode 100644 index 0000000000..0c2a04c5f9 --- /dev/null +++ b/sdk/nodejs/cloudformation/customResourceEmulator.ts @@ -0,0 +1,206 @@ +// *** WARNING: this file was generated by pulumi-language-nodejs. *** +// *** Do not edit by hand unless you're certain you know what you are doing! *** + +import * as pulumi from "@pulumi/pulumi"; +import * as utilities from "../utilities"; + +/** + * The Custom Resource Emulator allows you to use AWS CloudFormation Custom Resources directly in your Pulumi programs. It provides a way to invoke AWS Lambda functions that implement custom provisioning logic following the CloudFormation Custom Resource protocol. + * + * > **Note**: Currently, only Lambda-backed Custom Resources are supported. SNS-backed Custom Resources are not supported at this time. + * + * ## Example Usage + * + * ```typescript + * import * as aws from "@pulumi/aws-native"; + * + * const bucket = new aws.s3.Bucket('custom-resource-emulator'); + * + * // Create a Custom Resource that invokes a Lambda function + * const cr = new aws.cloudformation.CustomResourceEmulator('cr', { + * bucketName: bucket.id, + * bucketKeyPrefix: 'custom-resource-emulator', + * customResourceProperties: { + * hello: "world" + * }, + * serviceToken: "arn:aws:lambda:us-west-2:123456789012:function:my-custom-resource", + * resourceType: 'Custom::MyResource', + * }, { customTimeouts: { create: '5m', update: '5m', delete: '5m' } }); + * + * // Access the response data + * export const customResourceData = customResource.data; + * ``` + * + * ## About CloudFormation Custom Resources + * + * CloudFormation Custom Resources allow you to write custom provisioning logic for resources that aren't directly available as AWS CloudFormation resource types. Common use cases include: + * + * - Implementing complex provisioning logic + * - Performing custom validations or transformations + * - Integrating with third-party services + * - Implementing organization-specific infrastructure patterns + * + * For more information about CloudFormation Custom Resources, see [Custom Resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html) in the AWS CloudFormation User Guide. + * + * ## Permissions + * + * The IAM principal used by your Pulumi program must have the following permissions: + * + * 1. `lambda:InvokeFunction` on the Lambda function specified in `serviceToken` + * 2. S3 permissions on the bucket specified in `bucketName`: + * - `s3:PutObject` + * - `s3:GetObject` + * - `s3:HeadObject` + * + * ## Lambda Function Requirements + * + * The Lambda function specified in `serviceToken` must implement the CloudFormation Custom Resource lifecycle. + * For detailed information about implementing Lambda-backed Custom Resources, see [AWS Lambda-backed Custom Resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html) in the AWS CloudFormation User Guide. + * + * ## Timeouts + * + * Custom Resources have a default timeout of 60 minutes, matching the CloudFormation timeout for custom resource operations. You can customize it using the [`customTimeouts`](https://www.pulumi.com/docs/iac/concepts/options/customtimeouts/) resource option. + */ +export class CustomResourceEmulator extends pulumi.CustomResource { + /** + * Get an existing CustomResourceEmulator resource's state with the given name, ID, and optional extra + * properties used to qualify the lookup. + * + * @param name The _unique_ name of the resulting resource. + * @param id The _unique_ provider ID of the resource to lookup. + * @param opts Optional settings to control the behavior of the CustomResource. + */ + public static get(name: string, id: pulumi.Input, opts?: pulumi.CustomResourceOptions): CustomResourceEmulator { + return new CustomResourceEmulator(name, undefined as any, { ...opts, id: id }); + } + + /** @internal */ + public static readonly __pulumiType = 'aws-native:cloudformation:CustomResourceEmulator'; + + /** + * Returns true if the given object is an instance of CustomResourceEmulator. This is designed to work even + * when multiple copies of the Pulumi SDK have been loaded into the same process. + */ + public static isInstance(obj: any): obj is CustomResourceEmulator { + if (obj === undefined || obj === null) { + return false; + } + return obj['__pulumiType'] === CustomResourceEmulator.__pulumiType; + } + + /** + * The name of the S3 bucket to use for storing the response from the Custom Resource. + */ + public /*out*/ readonly bucket!: pulumi.Output; + /** + * The response data returned by invoking the Custom Resource. + */ + public /*out*/ readonly data!: pulumi.Output<{[key: string]: any}>; + /** + * Whether the response data contains sensitive information that should be marked as secret and not logged. + */ + public /*out*/ readonly noEcho!: pulumi.Output; + /** + * The name or unique identifier that corresponds to the `PhysicalResourceId` included in the Custom Resource response. If no `PhysicalResourceId` is provided in the response, a random ID will be generated. + */ + public /*out*/ readonly physicalResourceId!: pulumi.Output; + /** + * The CloudFormation type of the Custom Resource provider. For example, `Custom::MyCustomResource`. + */ + public readonly resourceType!: pulumi.Output; + /** + * The service token, such as a Lambda function ARN, that is invoked when the resource is created, updated, or deleted. + */ + public readonly serviceToken!: pulumi.Output; + /** + * A stand-in value for the CloudFormation stack ID. + */ + public readonly stackId!: pulumi.Output; + + /** + * Create a CustomResourceEmulator resource with the given unique name, arguments, and options. + * + * @param name The _unique_ name of the resource. + * @param args The arguments to use to populate this resource's properties. + * @param opts A bag of options that control this resource's behavior. + */ + constructor(name: string, args: CustomResourceEmulatorArgs, opts?: pulumi.CustomResourceOptions) { + let resourceInputs: pulumi.Inputs = {}; + opts = opts || {}; + if (!opts.id) { + if ((!args || args.bucketKeyPrefix === undefined) && !opts.urn) { + throw new Error("Missing required property 'bucketKeyPrefix'"); + } + if ((!args || args.bucketName === undefined) && !opts.urn) { + throw new Error("Missing required property 'bucketName'"); + } + if ((!args || args.customResourceProperties === undefined) && !opts.urn) { + throw new Error("Missing required property 'customResourceProperties'"); + } + if ((!args || args.resourceType === undefined) && !opts.urn) { + throw new Error("Missing required property 'resourceType'"); + } + if ((!args || args.serviceToken === undefined) && !opts.urn) { + throw new Error("Missing required property 'serviceToken'"); + } + resourceInputs["bucketKeyPrefix"] = args ? args.bucketKeyPrefix : undefined; + resourceInputs["bucketName"] = args ? args.bucketName : undefined; + resourceInputs["customResourceProperties"] = args ? args.customResourceProperties : undefined; + resourceInputs["resourceType"] = args ? args.resourceType : undefined; + resourceInputs["serviceToken"] = args ? args.serviceToken : undefined; + resourceInputs["stackId"] = args ? args.stackId : undefined; + resourceInputs["bucket"] = undefined /*out*/; + resourceInputs["data"] = undefined /*out*/; + resourceInputs["noEcho"] = undefined /*out*/; + resourceInputs["physicalResourceId"] = undefined /*out*/; + } else { + resourceInputs["bucket"] = undefined /*out*/; + resourceInputs["data"] = undefined /*out*/; + resourceInputs["noEcho"] = undefined /*out*/; + resourceInputs["physicalResourceId"] = undefined /*out*/; + resourceInputs["resourceType"] = undefined /*out*/; + resourceInputs["serviceToken"] = undefined /*out*/; + resourceInputs["stackId"] = undefined /*out*/; + } + opts = pulumi.mergeOptions(utilities.resourceOptsDefaults(), opts); + super(CustomResourceEmulator.__pulumiType, name, resourceInputs, opts); + } +} + +/** + * The set of arguments for constructing a CustomResourceEmulator resource. + */ +export interface CustomResourceEmulatorArgs { + /** + * The prefix to use for the bucket key when storing the response from the Custom Resource provider. + */ + bucketKeyPrefix: pulumi.Input; + /** + * The name of the S3 bucket to use for storing the response from the Custom Resource. + * + * The IAM principal configured for the provider must have `s3:PutObject`, `s3:HeadObject` and `s3:GetObject` permissions on this bucket. + */ + bucketName: pulumi.Input; + /** + * The properties to pass as an input to the Custom Resource. + * The properties are passed as a map of key-value pairs whereas all primitive values (number, boolean) are converted to strings for CloudFormation interoperability. + */ + customResourceProperties: pulumi.Input<{[key: string]: any}>; + /** + * The CloudFormation type of the Custom Resource. For example, `Custom::MyCustomResource`. + * This is required for CloudFormation interoperability. + */ + resourceType: pulumi.Input; + /** + * The service token to use for the Custom Resource. The service token is invoked when the resource is created, updated, or deleted. + * This can be a Lambda Function ARN with optional version or alias identifiers. + * + * The IAM principal configured for the provider must have `lambda:InvokeFunction` permissions on this service token. + */ + serviceToken: pulumi.Input; + /** + * A stand-in value for the CloudFormation stack ID. This is required for CloudFormation interoperability. + * If not provided, the Pulumi Stack ID is used. + */ + stackId?: pulumi.Input; +} diff --git a/sdk/nodejs/cloudformation/index.ts b/sdk/nodejs/cloudformation/index.ts index fdcfe29190..20c88e7a54 100644 --- a/sdk/nodejs/cloudformation/index.ts +++ b/sdk/nodejs/cloudformation/index.ts @@ -5,6 +5,11 @@ import * as pulumi from "@pulumi/pulumi"; import * as utilities from "../utilities"; // Export members: +export { CustomResourceEmulatorArgs } from "./customResourceEmulator"; +export type CustomResourceEmulator = import("./customResourceEmulator").CustomResourceEmulator; +export const CustomResourceEmulator: typeof import("./customResourceEmulator").CustomResourceEmulator = null as any; +utilities.lazyLoad(exports, ["CustomResourceEmulator"], () => require("./customResourceEmulator")); + export { GetHookDefaultVersionArgs, GetHookDefaultVersionResult, GetHookDefaultVersionOutputArgs } from "./getHookDefaultVersion"; export const getHookDefaultVersion: typeof import("./getHookDefaultVersion").getHookDefaultVersion = null as any; export const getHookDefaultVersionOutput: typeof import("./getHookDefaultVersion").getHookDefaultVersionOutput = null as any; @@ -128,6 +133,8 @@ const _module = { version: utilities.getVersion(), construct: (name: string, type: string, urn: string): pulumi.Resource => { switch (type) { + case "aws-native:cloudformation:CustomResourceEmulator": + return new CustomResourceEmulator(name, undefined, { urn }) case "aws-native:cloudformation:HookDefaultVersion": return new HookDefaultVersion(name, undefined, { urn }) case "aws-native:cloudformation:HookTypeConfig": diff --git a/sdk/nodejs/tsconfig.json b/sdk/nodejs/tsconfig.json index 500d58084d..947b91e9d0 100644 --- a/sdk/nodejs/tsconfig.json +++ b/sdk/nodejs/tsconfig.json @@ -333,6 +333,7 @@ "cleanroomsml/getTrainingDataset.ts", "cleanroomsml/index.ts", "cleanroomsml/trainingDataset.ts", + "cloudformation/customResourceEmulator.ts", "cloudformation/getHookDefaultVersion.ts", "cloudformation/getHookTypeConfig.ts", "cloudformation/getHookVersion.ts", diff --git a/sdk/python/pulumi_aws_native/__init__.py b/sdk/python/pulumi_aws_native/__init__.py index 6d18114b4b..ba5fc6dac0 100644 --- a/sdk/python/pulumi_aws_native/__init__.py +++ b/sdk/python/pulumi_aws_native/__init__.py @@ -1059,6 +1059,7 @@ "mod": "cloudformation", "fqn": "pulumi_aws_native.cloudformation", "classes": { + "aws-native:cloudformation:CustomResourceEmulator": "CustomResourceEmulator", "aws-native:cloudformation:HookDefaultVersion": "HookDefaultVersion", "aws-native:cloudformation:HookTypeConfig": "HookTypeConfig", "aws-native:cloudformation:HookVersion": "HookVersion", diff --git a/sdk/python/pulumi_aws_native/cloudformation/__init__.py b/sdk/python/pulumi_aws_native/cloudformation/__init__.py index 2859a6ec08..eb0dcf4098 100644 --- a/sdk/python/pulumi_aws_native/cloudformation/__init__.py +++ b/sdk/python/pulumi_aws_native/cloudformation/__init__.py @@ -6,6 +6,7 @@ import typing # Export this package's modules as members: from ._enums import * +from .custom_resource_emulator import * from .get_hook_default_version import * from .get_hook_type_config import * from .get_hook_version import *