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