Skip to content

Commit

Permalink
Don't send writeOnly properties if they're also createOnly (#1448)
Browse files Browse the repository at this point in the history
Fixes #1435

Ideally we'd have an integration test around this case to avoid future
regression but currently the VPC-based test is very unstable, so has
been disabled, but included for documentation of the issues.

- Deletion of the `aws-native:ec2:IpamPoolCidr` regularly fails with
`"GeneralServiceException": Error occurred during operation 'The CIDR
has one or more allocations.'` even after the VPC is deleted for
serveral minutes.
- Sometimes Update of the `aws-native:ec2:IpamPool` fails with
`"NotStabilized": IpamPoolCidrFailureReason(Message=The CIDR has one or
more allocations.`
- The refresh of the `aws-native:ec2:IpamPool` fails due to a diff in
the `provisionedCidrs` property.
- It's also not ideal that we have to create the IPAM manually to avoid
the test failing if run in parallel as only 1 IPAM is allowed to be
created in each region.

Extracted the patch generation code instead and wrote a unit around it.
Also replicated the fix into the untyped ExtensionResource
implementation and tested using the same test suite.
danielrbradley authored Mar 22, 2024
1 parent 5bb7e7c commit 210092f
Showing 16 changed files with 402 additions and 105 deletions.
21 changes: 21 additions & 0 deletions examples/examples_py_test.go
Original file line number Diff line number Diff line change
@@ -75,6 +75,27 @@ func TestDefaultTagsPython(t *testing.T) {
integration.ProgramTest(t, &test)
}

func TestVpcPython(t *testing.T) {
// This test is not stable for several reasons so we can't run it in CI.
// Deletion of the aws-native:ec2:IpamPoolCidr regularly fails with "GeneralServiceException: Error occurred during operation 'The CIDR has one or more allocations.'" even after the VPC is deleted.
// Sometimes Update of the aws-native:ec2:IpamPool fails with "NotStabilized: IpamPoolCidrFailureReason(Message=The CIDR has one or more allocations."
// The refresh of the aws-native:ec2:IpamPool fails due to a diff in the provisionedCidrs property.
// It's also not ideal that we have to create the IPAM manually to avoid the test failing if run in parallel.
t.SkipNow()
test := getPythonBaseOptions(t).
With(integration.ProgramTestOptions{
Dir: filepath.Join(getCwd(t), "vpc-py", "step1"),
EditDirs: []integration.EditDir{
{
Dir: filepath.Join(getCwd(t), "vpc-py", "step2"),
Additive: true,
},
},
})

integration.ProgramTest(t, &test)
}

func getPythonBaseOptions(t *testing.T) integration.ProgramTestOptions {
base := getBaseOptions(t)
basePy := base.With(integration.ProgramTestOptions{
2 changes: 2 additions & 0 deletions examples/vpc-py/step1/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.pyc
venv/
3 changes: 3 additions & 0 deletions examples/vpc-py/step1/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: simple-py
runtime: python
description: Test updating a VPC which has properties which are both create-only and write-only
26 changes: 26 additions & 0 deletions examples/vpc-py/step1/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright 2021, Pulumi Corporation. All rights reserved.

import pulumi
from pulumi_aws_native import ec2
from pulumi_command import local

aws_native_config = pulumi.Config("aws-native")

# Only 1 IPAM can exist per region, so this must be created manually as a pre-requisite and is therefore looked up here.
ipamId = local.run(
command="aws ec2 describe-ipams --query 'Ipams[0].IpamId' --output text").stdout

ipam = ec2.get_ipam(ipam_id=ipamId)

ipamPool = ec2.IpamPool("ipamPool", address_family="ipv4",
ipam_scope_id=ipam.private_default_scope_id,
locale=aws_native_config.require("region"))

ipamPoolCidr = ec2.IpamPoolCidr(
"ipamPoolCidr", ipam_pool_id=ipamPool.id, cidr="10.0.0.0/16")

vpc = ec2.Vpc("vpc",
# enable_dns_hostnames=True,
ipv4_ipam_pool_id=ipamPool.id,
ipv4_netmask_length=20,
opts=pulumi.ResourceOptions(depends_on=[ipamPoolCidr]))
2 changes: 2 additions & 0 deletions examples/vpc-py/step1/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pulumi>=3.5.1,<4.0.0
pulumi-command>=0.9.2,<2.0.0
25 changes: 25 additions & 0 deletions examples/vpc-py/step2/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2021, Pulumi Corporation. All rights reserved.

import pulumi
from pulumi_aws_native import ec2
from pulumi_command import local

aws_native_config = pulumi.Config("aws-native")

ipamId = local.run(
command="aws ec2 describe-ipams --query 'Ipams[0].IpamId' --output text").stdout

ipam = ec2.get_ipam(ipam_id=ipamId)

ipamPool = ec2.IpamPool("ipamPool", address_family="ipv4",
ipam_scope_id=ipam.private_default_scope_id,
locale=aws_native_config.require("region"))

ipamPoolCidr = ec2.IpamPoolCidr(
"ipamPoolCidr", ipam_pool_id=ipamPool.id, cidr="10.0.0.0/16")

vpc = ec2.Vpc("vpc",
enable_dns_hostnames=True,
ipv4_ipam_pool_id=ipamPool.id,
ipv4_netmask_length=20,
opts=pulumi.ResourceOptions(depends_on=[ipamPoolCidr]))
7 changes: 7 additions & 0 deletions provider/cmd/pulumi-resource-aws-native/schema.json
Original file line number Diff line number Diff line change
@@ -173204,6 +173204,13 @@
"outputs"
],
"inputProperties": {
"createOnly": {
"type": "array",
"items": {
"type": "string"
},
"description": "Property names as defined by `createOnlyProperties` in the CloudFormation schema. Create-only properties can't be set during updates, so will not be included in patches even if they are also marked as write-only, and will cause an error if attempted to be updated. Therefore any property here should also be included in the `replaceOnChanges` resource option too.\nIn the CloudFormation schema these are fully qualified property paths (e.g. `/properties/AccessToken`) whereas here we only include the top-level property name (e.g. `AccessToken`)."
},
"properties": {
"type": "object",
"additionalProperties": {
10 changes: 9 additions & 1 deletion provider/pkg/naming/convert.go
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ package naming
import (
"fmt"
"reflect"
"slices"
"strings"

"github.com/mattbaird/jsonpatch"
@@ -179,7 +180,14 @@ func (c *sdkToCfnConverter) sdkObjectValueToCfn(typeName string, spec metadata.C

func (c *sdkToCfnConverter) diffToPatch(diff *resource.ObjectDiff) ([]jsonpatch.JsonPatchOperation, error) {
var ops []jsonpatch.JsonPatchOperation
for sdkName, prop := range c.spec.Inputs {
// Sort keys to ensure deterministic ordering of patch operations.
sortedKeys := make([]string, 0, len(c.spec.Inputs))
for sdkName := range c.spec.Inputs {
sortedKeys = append(sortedKeys, string(sdkName))
}
slices.Sort(sortedKeys)
for _, sdkName := range sortedKeys {
prop := c.spec.Inputs[sdkName]
cfnName := ToCfnName(sdkName, c.spec.IrreversibleNames)
key := resource.PropertyKey(sdkName)
if v, ok := diff.Updates[key]; ok {
14 changes: 1 addition & 13 deletions provider/pkg/provider/provider.go
Original file line number Diff line number Diff line change
@@ -1074,19 +1074,7 @@ func (p *cfnProvider) Update(ctx context.Context, req *pulumirpc.UpdateRequest)
return nil, errors.Errorf("Resource type %s not found", resourceToken)
}

diff := oldInputs.Diff(newInputs)

// Write-only properties can't even be read internally within the CloudControl service so they must be included in
// patch requests as adds to ensure the updated model validates.
for _, writeOnlyPropName := range spec.WriteOnly {
propKey := resource.PropertyKey(writeOnlyPropName)
if _, ok := diff.Sames[propKey]; ok {
delete(diff.Sames, propKey)
diff.Adds[propKey] = newInputs[propKey]
}
}

ops, err := naming.DiffToPatch(&spec, p.resourceMap.Types, diff)
ops, err := resources.CalcPatch(oldInputs, newInputs, spec, p.resourceMap.Types)
if err != nil {
return nil, err
}
150 changes: 59 additions & 91 deletions provider/pkg/resources/extension_resource.go
Original file line number Diff line number Diff line change
@@ -6,18 +6,17 @@ import (
"context"
"fmt"

"github.com/mattbaird/jsonpatch"
"github.com/pulumi/pulumi-aws-native/provider/pkg/client"
"github.com/pulumi/pulumi-aws-native/provider/pkg/default_tags"
"github.com/pulumi/pulumi-go-provider/resourcex"
pschema "github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/wI2L/jsondiff"
)

type ExtensionResourceInputs struct {
Type string
Properties map[string]any
CreateOnly []string
WriteOnly []string
TagsProperty string
TagsStyle default_tags.TagsStyle
@@ -163,41 +162,9 @@ func (r *extensionResource) Update(ctx context.Context, urn resource.URN, id str
return nil, fmt.Errorf("changing the type of an extension resource is not supported")
}

jsonDiffPatch, err := jsondiff.Compare(typedOldInputs.Properties, typedInputs.Properties)
jsonPatch, err := CalculateUntypedPatch(typedOldInputs, typedInputs)
if err != nil {
return nil, fmt.Errorf("failed to compare properties: %w", err)
}

// Write-only properties can't even be read internally within the CloudControl service so they must be included in
// patch requests as adds to ensure the updated model validates.
for _, writeOnlyPropName := range typedInputs.WriteOnly {
newValue, ok := typedInputs.Properties[writeOnlyPropName]
if !ok {
continue
}
hasPatch := false
for _, op := range jsonDiffPatch {
if op.Path == writeOnlyPropName {
hasPatch = true
break
}
}
if !hasPatch {
jsonDiffPatch = append(jsonDiffPatch, jsondiff.Operation{
Type: "add",
Path: writeOnlyPropName,
Value: newValue,
})
}
}

jsonPatch := make([]jsonpatch.JsonPatchOperation, 0, len(jsonDiffPatch))
for _, op := range jsonDiffPatch {
jsonPatch = append(jsonPatch, jsonpatch.JsonPatchOperation{
Operation: op.Type,
Path: op.Path,
Value: op.Value,
})
return nil, fmt.Errorf("failed to calculate patch: %w", err)
}

resourceState, err := r.client.Update(ctx, typedInputs.Type, id, jsonPatch)
@@ -223,72 +190,73 @@ func (r *extensionResource) Delete(ctx context.Context, urn resource.URN, id str
return nil
}

func extensionResourceInputProperties() map[string]pschema.PropertySpec {
return map[string]pschema.PropertySpec{
"type": {
Description: "CloudFormation type name. This has three parts, each separated by two colons. For AWS resources this starts with `AWS::` e.g. `AWS::Logs::LogGroup`. Third party resources should use a namespace prefix e.g. `MyCompany::MyService::MyResource`.",
TypeSpec: pschema.TypeSpec{
Type: "string",
},
ReplaceOnChanges: true,
},
"properties": {
Description: "Property bag containing the properties for the resource. These should be defined using the casing expected by the CloudControl API as these values are sent exact as provided.",
TypeSpec: pschema.TypeSpec{
Type: "object",
AdditionalProperties: &pschema.TypeSpec{
Ref: "pulumi.json#/Any",
func ExtensionResourceSpec() pschema.ResourceSpec {
return pschema.ResourceSpec{
ObjectTypeSpec: pschema.ObjectTypeSpec{
Description: "A special resource that enables deploying CloudFormation Extensions (third-party resources). An extension has to be pre-registered in your AWS account in order to use this resource.",
Properties: map[string]pschema.PropertySpec{
"outputs": {
Description: "Dictionary of the extension resource attributes.",
TypeSpec: pschema.TypeSpec{
Type: "object",
AdditionalProperties: &pschema.TypeSpec{
Ref: "pulumi.json#/Any",
},
},
},
},
Required: []string{"outputs"},
},
"writeOnly": {
Description: "Property names as defined by `writeOnlyProperties` in the CloudFormation schema. Write-only properties are not returned during read operations and have to be included in all update operations as CloudControl itself can't read their previous values.\nIn the CloudFormation schema these are fully qualified property paths (e.g. `/properties/AccessToken`) whereas here we only include the top-level property name (e.g. `AccessToken`).",
TypeSpec: pschema.TypeSpec{
Type: "array",
Items: &pschema.TypeSpec{
InputProperties: map[string]pschema.PropertySpec{
"type": {
Description: "CloudFormation type name. This has three parts, each separated by two colons. For AWS resources this starts with `AWS::` e.g. `AWS::Logs::LogGroup`. Third party resources should use a namespace prefix e.g. `MyCompany::MyService::MyResource`.",
TypeSpec: pschema.TypeSpec{
Type: "string",
},
ReplaceOnChanges: true,
},
},
"tagsProperty": {
Description: "Optional name of the property containing the tags. Defaults to \"Tags\" if the `tagsStyle` is set to either \"stringMap\" or \"keyValueArray\". This is used to apply default tags to the resource and can be ignored if not using default tags.",
TypeSpec: pschema.TypeSpec{
Type: "string",
"properties": {
Description: "Property bag containing the properties for the resource. These should be defined using the casing expected by the CloudControl API as these values are sent exact as provided.",
TypeSpec: pschema.TypeSpec{
Type: "object",
AdditionalProperties: &pschema.TypeSpec{
Ref: "pulumi.json#/Any",
},
},
},
},
"tagsStyle": {
Description: "Optional style of tags this resource uses. Valid values are \"stringMap\", \"keyValueArray\" or \"none\". Defaults to `keyValueArray` if `tagsProperty` is set. This is used to apply default tags to the resource and can be ignored if not using default tags.",
TypeSpec: pschema.TypeSpec{
Type: "string",
"createOnly": {
Description: "Property names as defined by `createOnlyProperties` in the CloudFormation schema. Create-only properties can't be set during updates, so will not be included in patches even if they are also marked as write-only, and will cause an error if attempted to be updated. Therefore any property here should also be included in the `replaceOnChanges` resource option too.\nIn the CloudFormation schema these are fully qualified property paths (e.g. `/properties/AccessToken`) whereas here we only include the top-level property name (e.g. `AccessToken`).",
TypeSpec: pschema.TypeSpec{
Type: "array",
Items: &pschema.TypeSpec{
Type: "string",
},
},
},
},
}
}

func ExtensionResourceSpec() pschema.ResourceSpec {
return pschema.ResourceSpec{
ObjectTypeSpec: pschema.ObjectTypeSpec{
Description: "A special resource that enables deploying CloudFormation Extensions (third-party resources). An extension has to be pre-registered in your AWS account in order to use this resource.",
Properties: extensionResourceOutputProperties(),
Required: []string{"outputs"},
},
InputProperties: extensionResourceInputProperties(),
RequiredInputs: []string{"type", "properties"},
}
}

func extensionResourceOutputProperties() map[string]pschema.PropertySpec {
properties := map[string]pschema.PropertySpec{}
properties["outputs"] = pschema.PropertySpec{
Description: "Dictionary of the extension resource attributes.",
TypeSpec: pschema.TypeSpec{
Type: "object",
AdditionalProperties: &pschema.TypeSpec{
Ref: "pulumi.json#/Any",
"writeOnly": {
Description: "Property names as defined by `writeOnlyProperties` in the CloudFormation schema. Write-only properties are not returned during read operations and have to be included in all update operations as CloudControl itself can't read their previous values.\nIn the CloudFormation schema these are fully qualified property paths (e.g. `/properties/AccessToken`) whereas here we only include the top-level property name (e.g. `AccessToken`).",
TypeSpec: pschema.TypeSpec{
Type: "array",
Items: &pschema.TypeSpec{
Type: "string",
},
},
},
"tagsProperty": {
Description: "Optional name of the property containing the tags. Defaults to \"Tags\" if the `tagsStyle` is set to either \"stringMap\" or \"keyValueArray\". This is used to apply default tags to the resource and can be ignored if not using default tags.",
TypeSpec: pschema.TypeSpec{
Type: "string",
},
},
"tagsStyle": {
Description: "Optional style of tags this resource uses. Valid values are \"stringMap\", \"keyValueArray\" or \"none\". Defaults to `keyValueArray` if `tagsProperty` is set. This is used to apply default tags to the resource and can be ignored if not using default tags.",
TypeSpec: pschema.TypeSpec{
Type: "string",
},
},
},
RequiredInputs: []string{"type", "properties"},
}
return properties
}

func (r *ExtensionResourceInputs) toOutputs(resourceState map[string]interface{}) map[string]any {
85 changes: 85 additions & 0 deletions provider/pkg/resources/patching.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package resources

import (
"fmt"
"slices"
"strings"

"github.com/mattbaird/jsonpatch"
"github.com/pulumi/pulumi-aws-native/provider/pkg/metadata"
"github.com/pulumi/pulumi-aws-native/provider/pkg/naming"
"github.com/pulumi/pulumi/pkg/v3/codegen"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/wI2L/jsondiff"
)

func CalcPatch(oldInputs resource.PropertyMap, newInputs resource.PropertyMap, spec metadata.CloudAPIResource, types map[string]metadata.CloudAPIType) ([]jsonpatch.JsonPatchOperation, error) {
diff := oldInputs.Diff(newInputs)

// Write-only properties can't even be read internally within the CloudControl service so they must be included in
// patch requests as adds to ensure the updated model validates.
// If a property is both write-only and create-only, we should not include it in the patch request
// because create-only properties can't be updated and even doing an add of the same value is rejected.
createOnlyProps := codegen.NewStringSet(spec.CreateOnly...)
writeOnlyProps := codegen.NewStringSet(spec.WriteOnly...)
mustSendProps := writeOnlyProps.Subtract(createOnlyProps)
for _, writeOnlyPropName := range mustSendProps.SortedValues() {
propKey := resource.PropertyKey(writeOnlyPropName)
if _, ok := diff.Sames[propKey]; ok {
delete(diff.Sames, propKey)
diff.Adds[propKey] = newInputs[propKey]
}
}

return naming.DiffToPatch(&spec, types, diff)
}

func CalculateUntypedPatch(typedOldInputs ExtensionResourceInputs, typedInputs ExtensionResourceInputs) ([]jsonpatch.JsonPatchOperation, error) {
jsonDiffPatch, err := jsondiff.Compare(typedOldInputs.Properties, typedInputs.Properties)
if err != nil {
return nil, fmt.Errorf("failed to compare properties: %w", err)
}

// Write-only properties can't even be read internally within the CloudControl service so they must be included in
// patch requests as adds to ensure the updated model validates.
// If a property is both write-only and create-only, we should not include it in the patch request
// because create-only properties can't be updated and even doing an add of the same value is rejected.
createOnlyProps := codegen.NewStringSet(typedInputs.CreateOnly...)
for _, writeOnlyPropName := range typedInputs.WriteOnly {
if createOnlyProps.Has(writeOnlyPropName) {
continue
}
newValue, ok := typedInputs.Properties[writeOnlyPropName]
if !ok {
continue
}
propPath := "/" + writeOnlyPropName
hasPatch := false
for _, op := range jsonDiffPatch {
if op.Path == propPath {
hasPatch = true
break
}
}
if !hasPatch {
jsonDiffPatch = append(jsonDiffPatch, jsondiff.Operation{
Type: "add",
Path: propPath,
Value: newValue,
})
}
}

jsonPatch := make([]jsonpatch.JsonPatchOperation, 0, len(jsonDiffPatch))
for _, op := range jsonDiffPatch {
jsonPatch = append(jsonPatch, jsonpatch.JsonPatchOperation{
Operation: op.Type,
Path: op.Path,
Value: op.Value,
})
}
slices.SortStableFunc(jsonPatch, func(a, b jsonpatch.JsonPatchOperation) int {
return strings.Compare(a.Path, b.Path)
})
return jsonPatch, nil
}
114 changes: 114 additions & 0 deletions provider/pkg/resources/patching_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package resources

import (
"testing"

"github.com/mattbaird/jsonpatch"
"github.com/pulumi/pulumi-aws-native/provider/pkg/metadata"
"github.com/pulumi/pulumi-aws-native/provider/pkg/naming"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
"github.com/stretchr/testify/assert"
)

func TestCalcPatch(t *testing.T) {
type args struct {
oldInputs resource.PropertyMap
newInputs resource.PropertyMap
spec metadata.CloudAPIResource
types map[string]metadata.CloudAPIType
}
type implementation func(t *testing.T, args args) []jsonpatch.JsonPatchOperation
typed := func(t *testing.T, args args) []jsonpatch.JsonPatchOperation {
t.Helper()
patch, err := CalcPatch(args.oldInputs, args.newInputs, args.spec, args.types)
assert.NoError(t, err)
return patch
}
untyped := func(t *testing.T, args args) []jsonpatch.JsonPatchOperation {
t.Helper()
// Convert all keys to upper camel case to match the behavior of the SdkToCfn function.
keysToUpperCamel := func(s string) (string, bool) {
return naming.ToUpperCamel(s), true
}
patch, err := CalculateUntypedPatch(ExtensionResourceInputs{
Properties: args.oldInputs.MapRepl(keysToUpperCamel, nil),
}, ExtensionResourceInputs{
Properties: args.newInputs.MapRepl(keysToUpperCamel, nil),
CreateOnly: slice.Map(args.spec.CreateOnly, naming.ToUpperCamel),
WriteOnly: slice.Map(args.spec.WriteOnly, naming.ToUpperCamel),
})
assert.NoError(t, err)
return patch
}

for name, run := range map[string]implementation{
"Typed": typed,
"Untyped": untyped,
} {
t.Run(name, func(t *testing.T) {
t.Run("no diff", func(t *testing.T) {
patch := run(t, args{
oldInputs: resource.PropertyMap{
"prop1": resource.NewStringProperty("value1"),
},
newInputs: resource.PropertyMap{
"prop1": resource.NewStringProperty("value1"),
},
spec: metadata.CloudAPIResource{
Inputs: map[string]schema.PropertySpec{
"prop1": {TypeSpec: schema.TypeSpec{Type: "string"}},
},
}})
assert.Empty(t, patch)
})
t.Run("always sends write-only properties", func(t *testing.T) {
expected := []jsonpatch.JsonPatchOperation{
{Operation: "add", Path: "/Prop1", Value: "1"},
{Operation: "replace", Path: "/Prop2", Value: "2b"},
}
patch := run(t, args{
oldInputs: resource.PropertyMap{
"prop1": resource.NewStringProperty("1"),
"prop2": resource.NewStringProperty("2a"),
},
newInputs: resource.PropertyMap{
"prop1": resource.NewStringProperty("1"),
"prop2": resource.NewStringProperty("2b"),
},
spec: metadata.CloudAPIResource{
Inputs: map[string]schema.PropertySpec{
"prop1": {TypeSpec: schema.TypeSpec{Type: "string"}},
"prop2": {TypeSpec: schema.TypeSpec{Type: "string"}},
},
WriteOnly: []string{"prop1"},
}})
assert.Equal(t, expected, patch)
})
t.Run("don't send write-only, create-only properties", func(t *testing.T) {
expected := []jsonpatch.JsonPatchOperation{
{Operation: "replace", Path: "/Prop2", Value: "2b"},
}
patch := run(t, args{
oldInputs: resource.PropertyMap{
"prop1": resource.NewStringProperty("1"),
"prop2": resource.NewStringProperty("2a"),
},
newInputs: resource.PropertyMap{
"prop1": resource.NewStringProperty("1"),
"prop2": resource.NewStringProperty("2b"),
},
spec: metadata.CloudAPIResource{
Inputs: map[string]schema.PropertySpec{
"prop1": {TypeSpec: schema.TypeSpec{Type: "string"}},
"prop2": {TypeSpec: schema.TypeSpec{Type: "string"}},
},
WriteOnly: []string{"prop1"},
CreateOnly: []string{"prop1"},
}})
assert.Equal(t, expected, patch)
})
})
}
}
13 changes: 13 additions & 0 deletions sdk/dotnet/ExtensionResource.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions sdk/go/aws/extensionResource.go
6 changes: 6 additions & 0 deletions sdk/nodejs/extensionResource.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions sdk/python/pulumi_aws_native/extension_resource.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 210092f

Please sign in to comment.