From 2962615c932c7ea3d791f8500acb2bf285d69251 Mon Sep 17 00:00:00 2001 From: Florian Stadler Date: Tue, 5 Nov 2024 11:46:30 +0100 Subject: [PATCH] Refactor Custom Resources to allow configuring timeouts and secrets (#1795) Previously it was assumed that the custom resources do not concern themselves with secretness or handle timeouts. But this is necessary to support CloudFormation based Custom Resources. Those can contain secret outputs and support configuring timeouts. This change modifies the `CustomResource` interface to accommodate those requirements. In detail this means that the Create/Update/Read lifecycle methods now returns a `PropertyMap` instead of a generic `map[string]interface{}` and take a timeout parameter. For reviewing this change, I'd first recommend having a look at the changes to the `CustomResource` interface in `provider/pkg/resources/custom.go` and then double check the refactoring changes resulting in that. This change also introduces tests for the provider's CRUD lifecycle. As part of doing that, I added mocks using `uber/gomock`. Relates to https://github.com/pulumi/pulumi-cdk/issues/109 --- provider/go.mod | 16 +- provider/go.sum | 33 +- provider/pkg/client/client.go | 1 + provider/pkg/client/mock_client.go | 103 +++ provider/pkg/provider/provider.go | 75 +-- provider/pkg/provider/provider_test.go | 605 ++++++++++++++++++ provider/pkg/resources/checkpoint.go | 23 + provider/pkg/resources/checkpoint_test.go | 90 +++ provider/pkg/resources/custom.go | 10 +- provider/pkg/resources/extension_resource.go | 18 +- .../pkg/resources/mock_custom_resource.go | 121 ++++ 11 files changed, 1019 insertions(+), 76 deletions(-) create mode 100644 provider/pkg/client/mock_client.go create mode 100644 provider/pkg/provider/provider_test.go create mode 100644 provider/pkg/resources/checkpoint.go create mode 100644 provider/pkg/resources/checkpoint_test.go create mode 100644 provider/pkg/resources/mock_custom_resource.go diff --git a/provider/go.mod b/provider/go.mod index a1598bc737..e76974c8f9 100644 --- a/provider/go.mod +++ b/provider/go.mod @@ -30,6 +30,7 @@ require ( github.com/stretchr/testify v1.9.0 github.com/wI2L/jsondiff v0.5.1 github.com/zclconf/go-cty v1.13.2 + go.uber.org/mock v0.4.0 google.golang.org/grpc v1.63.2 google.golang.org/protobuf v1.33.0 ) @@ -75,12 +76,12 @@ require ( github.com/cloudflare/circl v1.3.7 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deckarep/golang-set/v2 v2.5.0 // indirect github.com/djherbis/times v1.5.0 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/evanphx/json-patch v0.5.2 // indirect + github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect @@ -90,7 +91,7 @@ require ( github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/gofrs/uuid v4.2.0+incompatible // indirect + github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -111,7 +112,7 @@ require ( github.com/hashicorp/go-sockaddr v1.0.6 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/vault/api v1.12.0 // indirect - github.com/iancoleman/strcase v0.2.0 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -149,7 +150,7 @@ require ( github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/term v1.1.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 // indirect github.com/pulumi/esc v0.10.0 // indirect github.com/pulumi/inflector v0.1.1 // indirect @@ -162,7 +163,8 @@ require ( github.com/segmentio/encoding v0.3.5 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect - github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/afero v1.10.0 // indirect + github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cobra v1.8.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/texttheater/golang-levenshtein v1.0.1 // indirect @@ -182,7 +184,7 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - go.uber.org/atomic v1.10.0 // indirect + go.uber.org/atomic v1.11.0 // indirect gocloud.dev v0.37.0 // indirect gocloud.dev/secrets/hashivault v0.37.0 // indirect golang.org/x/crypto v0.25.0 // indirect diff --git a/provider/go.sum b/provider/go.sum index e25dd76690..24b5ad0214 100644 --- a/provider/go.sum +++ b/provider/go.sum @@ -175,8 +175,9 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deckarep/golang-set/v2 v2.5.0 h1:hn6cEZtQ0h3J8kFrHR/NrzyOoTnjgW1+FmNJzQ7y/sA= github.com/deckarep/golang-set/v2 v2.5.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/djherbis/times v1.5.0 h1:79myA211VwPhFTqUk8xehWrsEO+zcIZj0zT8mXPVARU= @@ -193,8 +194,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= -github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= @@ -238,8 +239,8 @@ github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/goccy/go-yaml v1.9.5 h1:Eh/+3uk9kLxG4koCX6lRMAPS1OaMSAi+FJcya0INdB0= github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= -github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= -github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -364,15 +365,14 @@ github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZ github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= github.com/hashicorp/vault/api v1.12.0 h1:meCpJSesvzQyao8FCOgk2fGdoADAnbDu2WPJN1lDLJ4= github.com/hashicorp/vault/api v1.12.0/go.mod h1:si+lJCYO7oGkIoNPAN8j3azBLTn9SjMGS+jFaHd1Cck= -github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -480,8 +480,9 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 h1:vkHw5I/plNdTr435cARxCW6q9gc0S/Yxz7Mkd38pOb0= github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231/go.mod h1:murToZ2N9hNJzewjHBgfFdXhZKjY3z5cYC1VXk+lbFE= @@ -522,10 +523,10 @@ 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/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= -github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= -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/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= +github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= 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= @@ -599,8 +600,10 @@ go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= gocloud.dev v0.37.0 h1:XF1rN6R0qZI/9DYjN16Uy0durAmSlf58DHOcb28GPro= gocloud.dev v0.37.0/go.mod h1:7/O4kqdInCNsc6LqgmuFnS0GRew4XNNYWpA44yQnwco= gocloud.dev/secrets/hashivault v0.37.0 h1:5ehGtUBP29DFAgAs6bPw7fVSgqQ3TxaoK2xVcLp1x+c= diff --git a/provider/pkg/client/client.go b/provider/pkg/client/client.go index 81e3952b90..27022d864d 100644 --- a/provider/pkg/client/client.go +++ b/provider/pkg/client/client.go @@ -18,6 +18,7 @@ import ( // CloudControlApiClient providers CRUD operations around Cloud Control API, with the mechanics of API calls abstracted away. // For instance, it serializes and deserializes wire data and follows the protocol of long-running operations. +//go:generate mockgen -package client -source client.go -destination mock_client.go CloudControlApiClient type CloudControlClient interface { // Create creates a resource of the specified type with the desired state. // It awaits the operation until completion and returns a map of output property values. diff --git a/provider/pkg/client/mock_client.go b/provider/pkg/client/mock_client.go new file mode 100644 index 0000000000..0eb7996820 --- /dev/null +++ b/provider/pkg/client/mock_client.go @@ -0,0 +1,103 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: client.go +// +// Generated by this command: +// +// mockgen -package client -source client.go -destination mock_client.go CloudControlApiClient +// + +// Package client is a generated GoMock package. +package client + +import ( + context "context" + reflect "reflect" + + jsonpatch "github.com/mattbaird/jsonpatch" + gomock "go.uber.org/mock/gomock" +) + +// MockCloudControlClient is a mock of CloudControlClient interface. +type MockCloudControlClient struct { + ctrl *gomock.Controller + recorder *MockCloudControlClientMockRecorder + isgomock struct{} +} + +// MockCloudControlClientMockRecorder is the mock recorder for MockCloudControlClient. +type MockCloudControlClientMockRecorder struct { + mock *MockCloudControlClient +} + +// NewMockCloudControlClient creates a new mock instance. +func NewMockCloudControlClient(ctrl *gomock.Controller) *MockCloudControlClient { + mock := &MockCloudControlClient{ctrl: ctrl} + mock.recorder = &MockCloudControlClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCloudControlClient) EXPECT() *MockCloudControlClientMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockCloudControlClient) Create(ctx context.Context, typeName string, desiredState map[string]any) (*string, map[string]any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, typeName, desiredState) + ret0, _ := ret[0].(*string) + ret1, _ := ret[1].(map[string]any) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Create indicates an expected call of Create. +func (mr *MockCloudControlClientMockRecorder) Create(ctx, typeName, desiredState any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockCloudControlClient)(nil).Create), ctx, typeName, desiredState) +} + +// Delete mocks base method. +func (m *MockCloudControlClient) Delete(ctx context.Context, typeName, identifier string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, typeName, identifier) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockCloudControlClientMockRecorder) Delete(ctx, typeName, identifier any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockCloudControlClient)(nil).Delete), ctx, typeName, identifier) +} + +// Read mocks base method. +func (m *MockCloudControlClient) Read(ctx context.Context, typeName, identifier string) (map[string]any, bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", ctx, typeName, identifier) + ret0, _ := ret[0].(map[string]any) + ret1, _ := ret[1].(bool) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Read indicates an expected call of Read. +func (mr *MockCloudControlClientMockRecorder) Read(ctx, typeName, identifier any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockCloudControlClient)(nil).Read), ctx, typeName, identifier) +} + +// Update mocks base method. +func (m *MockCloudControlClient) Update(ctx context.Context, typeName, identifier string, patches []jsonpatch.JsonPatchOperation) (map[string]any, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, typeName, identifier, patches) + ret0, _ := ret[0].(map[string]any) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockCloudControlClientMockRecorder) Update(ctx, typeName, identifier, patches any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockCloudControlClient)(nil).Update), ctx, typeName, identifier, patches) +} diff --git a/provider/pkg/provider/provider.go b/provider/pkg/provider/provider.go index ce70e43baa..f69fb49d8d 100644 --- a/provider/pkg/provider/provider.go +++ b/provider/pkg/provider/provider.go @@ -798,7 +798,7 @@ func (p *cfnProvider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*pu } // Extract old inputs from the `__inputs` field of the old state. - oldInputs := parseCheckpointObject(oldState) + oldInputs := resources.ParseCheckpointObject(oldState) diff := oldInputs.Diff(newInputs) @@ -830,11 +830,11 @@ func (p *cfnProvider) Create(ctx context.Context, req *pulumirpc.CreateRequest) resourceToken := string(urn.Type()) var id *string - var outputs map[string]interface{} + var outputs resource.PropertyMap var createErr error - var payload map[string]interface{} + timeout := time.Duration(req.GetTimeout()) * time.Second if customResource, ok := p.customResources[resourceToken]; ok { - id, outputs, createErr = customResource.Create(ctx, urn, inputs) + id, outputs, createErr = customResource.Create(ctx, urn, inputs, timeout) } else { // Standard resource spec, hasSpec := p.resourceMap.Resources[resourceToken] @@ -844,7 +844,7 @@ func (p *cfnProvider) Create(ctx context.Context, req *pulumirpc.CreateRequest) cfType := spec.CfType // Convert SDK inputs to CFN payload. - payload, err = naming.SdkToCfn(&spec, p.resourceMap.Types, resourcex.Decode(inputs)) + payload, err := naming.SdkToCfn(&spec, p.resourceMap.Types, resourcex.Decode(inputs)) if err != nil { return nil, fmt.Errorf("Failed to convert value for %s: %w", resourceToken, err) } @@ -857,19 +857,20 @@ func (p *cfnProvider) Create(ctx context.Context, req *pulumirpc.CreateRequest) return nil, errors.Wrapf(createErr, "creating resource") } - outputs = naming.CfnToSdk(resourceState) + rawOutputs := naming.CfnToSdk(resourceState) // Write-only properties are not returned in the outputs, so we assume they should have the same value we sent from the inputs. if hasSpec && len(spec.WriteOnly) > 0 { inputsMap := inputs.Mappable() for _, writeOnlyProp := range spec.WriteOnly { - if _, ok := outputs[writeOnlyProp]; !ok { + if _, ok := rawOutputs[writeOnlyProp]; !ok { inputValue, ok := inputsMap[writeOnlyProp] if ok { - outputs[writeOnlyProp] = inputValue + rawOutputs[writeOnlyProp] = inputValue } } } } + outputs = resources.CheckpointObject(inputs, rawOutputs) } if createErr != nil { @@ -878,9 +879,8 @@ func (p *cfnProvider) Create(ctx context.Context, req *pulumirpc.CreateRequest) } // Resource was created but failed to fully initialize. // It has some state, so we return a partial error. - obj := checkpointObject(inputs, outputs) checkpoint, err := plugin.MarshalProperties( - obj, + outputs, plugin.MarshalOptions{ Label: "currentResourceStateCheckpoint.checkpoint", KeepSecrets: true, @@ -896,7 +896,7 @@ func (p *cfnProvider) Create(ctx context.Context, req *pulumirpc.CreateRequest) // Store both outputs and inputs into the state. checkpoint, err := plugin.MarshalProperties( - checkpointObject(inputs, outputs), + outputs, plugin.MarshalOptions{Label: fmt.Sprintf("%s.checkpoint", label), KeepSecrets: true, KeepUnknowns: true, SkipNulls: true}, ) if err != nil { @@ -923,12 +923,12 @@ func (p *cfnProvider) Read(ctx context.Context, req *pulumirpc.ReadRequest) (*pu return nil, err } // Extract old inputs from the `__inputs` field of the old state. - inputs := parseCheckpointObject(oldState) + inputs := resources.ParseCheckpointObject(oldState) // Read the resource state from AWS. resourceToken := string(urn.Type()) var newInputs resource.PropertyMap - var newState map[string]interface{} + var newState resource.PropertyMap var exists bool if customResource, ok := p.customResources[resourceToken]; ok { newState, newInputs, exists, err = customResource.Read(ctx, urn, id, inputs, oldState) @@ -954,12 +954,12 @@ func (p *cfnProvider) Read(ctx context.Context, req *pulumirpc.ReadRequest) (*pu // Not Exists means that the resource was deleted. return &pulumirpc.ReadResponse{Id: ""}, nil } - newState = naming.CfnToSdk(resourceState) + rawState := naming.CfnToSdk(resourceState) if inputs == nil { // There may be no old state (i.e., importing a new resource). // Extract inputs from the response body. - newStateProps := resource.NewPropertyMapFromMap(newState) + newStateProps := resource.NewPropertyMapFromMap(rawState) inputs, err = schema.GetInputsFromState(&spec, newStateProps) if err != nil { return nil, err @@ -981,18 +981,18 @@ func (p *cfnProvider) Read(ctx context.Context, req *pulumirpc.ReadRequest) (*pu if len(spec.WriteOnly) > 0 { missingProps := make([]string, 0, len(spec.WriteOnly)) for _, writeOnlyProp := range spec.WriteOnly { - if _, ok := newState[writeOnlyProp]; !ok { + if _, ok := rawState[writeOnlyProp]; !ok { oldValue, ok := oldStateMap[writeOnlyProp] missingProps = append(missingProps, writeOnlyProp) if ok { - newState[writeOnlyProp] = oldValue + rawState[writeOnlyProp] = oldValue } } } p.host.Log(ctx, diag.Warning, urn, fmt.Sprintf("Can't refresh write-only properties: %s", strings.Join(missingProps, ", "))) } // 2. Project new outputs to their corresponding input shape (exclude attributes). - newStateProps := resource.NewPropertyMapFromMap(newState) + newStateProps := resource.NewPropertyMapFromMap(rawState) newInputProjection, err := schema.GetInputsFromState(&spec, newStateProps) if err != nil { return nil, err @@ -1003,10 +1003,12 @@ func (p *cfnProvider) Read(ctx context.Context, req *pulumirpc.ReadRequest) (*pu // 4. Apply this difference to the actual inputs (not a projection) that we have in state. newInputs = resources.ApplyDiff(inputs, diff) } + + newState = resources.CheckpointObject(newInputs, rawState) } // Store both outputs and inputs into the state checkpoint. checkpoint, err := plugin.MarshalProperties( - checkpointObject(newInputs, newState), + newState, plugin.MarshalOptions{Label: fmt.Sprintf("%s.checkpoint", label), KeepSecrets: true, KeepUnknowns: true, SkipNulls: true}, ) if err != nil { @@ -1052,14 +1054,15 @@ func (p *cfnProvider) Update(ctx context.Context, req *pulumirpc.UpdateRequest) if err != nil { return nil, errors.Wrapf(err, "diff failed because malformed resource inputs") } - oldInputs := parseCheckpointObject(oldState) + oldInputs := resources.ParseCheckpointObject(oldState) - var outputs map[string]interface{} + var outputs resource.PropertyMap id := req.GetId() resourceToken := string(urn.Type()) if customResource, ok := p.customResources[resourceToken]; ok { + timeout := time.Duration(req.GetTimeout()) * time.Second // Custom resource - outputs, err = customResource.Update(ctx, urn, id, newInputs, oldInputs) + outputs, err = customResource.Update(ctx, urn, id, newInputs, oldInputs, timeout) if err != nil { return nil, err } @@ -1080,24 +1083,25 @@ func (p *cfnProvider) Update(ctx context.Context, req *pulumirpc.UpdateRequest) if err != nil { return nil, err } - outputs = naming.CfnToSdk(resourceState) + rawOutputs := naming.CfnToSdk(resourceState) // Write-only properties are not returned in the outputs, so we assume they should have the same value we sent from the inputs. if len(spec.WriteOnly) > 0 { inputsMap := newInputs.Mappable() for _, writeOnlyProp := range spec.WriteOnly { - if _, ok := outputs[writeOnlyProp]; !ok { + if _, ok := rawOutputs[writeOnlyProp]; !ok { inputValue, ok := inputsMap[writeOnlyProp] if ok { - outputs[writeOnlyProp] = inputValue + rawOutputs[writeOnlyProp] = inputValue } } } } + outputs = resources.CheckpointObject(newInputs, rawOutputs) } // Store both outputs and inputs into the state and return RPC checkpoint. checkpoint, err := plugin.MarshalProperties( - checkpointObject(newInputs, outputs), + outputs, plugin.MarshalOptions{Label: fmt.Sprintf("%s.checkpoint", label), KeepSecrets: true, KeepUnknowns: true, SkipNulls: true}, ) if err != nil { @@ -1125,7 +1129,8 @@ func (p *cfnProvider) Delete(ctx context.Context, req *pulumirpc.DeleteRequest) return nil, errors.Wrapf(err, "failed to parse inputs for update") } - err = customResource.Delete(ctx, urn, id, oldInputs) + timeout := time.Duration(req.GetTimeout()) * time.Second + err = customResource.Delete(ctx, urn, id, oldInputs, timeout) if err != nil { return nil, err } @@ -1173,22 +1178,6 @@ func (p *cfnProvider) Cancel(context.Context, *pbempty.Empty) (*pbempty.Empty, e return &pbempty.Empty{}, nil } -// checkpointObject puts inputs in the `__inputs` field of the state. -func checkpointObject(inputs resource.PropertyMap, outputs map[string]interface{}) resource.PropertyMap { - object := resource.NewPropertyMapFromMap(outputs) - object["__inputs"] = resource.MakeSecret(resource.NewObjectProperty(inputs)) - return object -} - -// parseCheckpointObject returns inputs that are saved in the `__inputs` field of the state. -func parseCheckpointObject(obj resource.PropertyMap) resource.PropertyMap { - if inputs, ok := obj["__inputs"]; ok { - return inputs.SecretValue().Element.ObjectValue() - } - - return nil -} - // pulumiUserAgentMiddleware adds a Pulumi-specific user-agent to the request middleware. // Example: APN/1.0 Pulumi/1.0 PulumiAwsNative/1.12, var pulumiUserAgentMiddleware = middleware.BuildMiddlewareFunc("PulumiUserAgent", func( diff --git a/provider/pkg/provider/provider_test.go b/provider/pkg/provider/provider_test.go new file mode 100644 index 0000000000..fc2a979d83 --- /dev/null +++ b/provider/pkg/provider/provider_test.go @@ -0,0 +1,605 @@ +package provider + +import ( + "context" + "testing" + "time" + + "github.com/pulumi/pulumi-aws-native/provider/pkg/client" + "github.com/pulumi/pulumi-aws-native/provider/pkg/metadata" + "github.com/pulumi/pulumi-aws-native/provider/pkg/resources" + "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/resource/plugin" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/rpcutil/rpcerror" + pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + gomock "go.uber.org/mock/gomock" + "google.golang.org/protobuf/types/known/structpb" +) + +func TestCreate(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockCCC := client.NewMockCloudControlClient(ctrl) + mockCustomResource := resources.NewMockCustomResource(ctrl) + + ctx, cancel := context.WithCancel(context.Background()) + provider := &cfnProvider{ + name: "test-provider", + resourceMap: &metadata.CloudAPIMetadata{Resources: map[string]metadata.CloudAPIResource{}}, + customResources: map[string]resources.CustomResource{"custom:resource": mockCustomResource}, + ccc: mockCCC, + canceler: &cancellationContext{ + context: ctx, + cancel: cancel, + }, + } + + urn := resource.NewURN("stack", "project", "parent", "custom:resource", "name") + req := &pulumirpc.CreateRequest{ + Urn: string(urn), + Properties: mustMarshalProperties(t, resource.PropertyMap{"my": resource.NewStringProperty("input value")}), + Timeout: float64((5 * time.Minute).Seconds()), + } + + t.Run("CustomResource", func(t *testing.T) { + mockCustomResource.EXPECT().Create(ctx, urn, gomock.Any(), 5*time.Minute).Return( + stringPtr("resource-id"), resource.PropertyMap{"foo": resource.NewStringProperty("bar")}, nil, + ) + + resp, err := provider.Create(ctx, req) + assert.NoError(t, err) + assert.Equal(t, "resource-id", resp.Id) + assert.NotNil(t, resp.Properties) + }) + + t.Run("CustomResource/Error", func(t *testing.T) { + mockCustomResource.EXPECT().Create(ctx, urn, gomock.Any(), 5*time.Minute).Return( + nil, nil, assert.AnError, + ) + + resp, err := provider.Create(ctx, req) + assert.Error(t, err) + assert.Nil(t, resp) + }) + + t.Run("StandardResource", func(t *testing.T) { + provider.resourceMap.Resources["aws:s3/bucket:Bucket"] = metadata.CloudAPIResource{ + CfType: "AWS::S3::Bucket", + } + req.Urn = string(resource.NewURN("stack", "project", "parent", "aws:s3/bucket:Bucket", "name")) + + mockCCC.EXPECT().Create(ctx, "AWS::S3::Bucket", gomock.Any()).Return( + stringPtr("bucket-id"), map[string]interface{}{"foo": "bar"}, nil, + ) + + resp, err := provider.Create(ctx, req) + assert.NoError(t, err) + assert.Equal(t, "bucket-id", resp.Id) + require.NotNil(t, resp.Properties) + props := mustUnmarshalProperties(t, resp.Properties) + require.True(t, props.HasValue("foo"), "Expected 'foo' property in response") + assert.Equal(t, "bar", props["foo"].StringValue()) + require.True(t, props.HasValue("__inputs"), "Expected '__inputs' property in response") + require.True(t, props["__inputs"].IsSecret(), "Expected '__inputs' to be a secret") + inputs := props["__inputs"].SecretValue().Element.ObjectValue() + require.True(t, inputs.HasValue("my"), "Expected 'my' property in '__inputs'") + assert.Equal(t, "input value", inputs["my"].StringValue()) + }) + + t.Run("StandardResource/NotFound", func(t *testing.T) { + req.Urn = string(resource.NewURN("stack", "project", "parent", "unknown:resource", "name")) + + resp, err := provider.Create(ctx, req) + assert.Error(t, err) + assert.Nil(t, resp) + }) + + t.Run("StandardResource/Error", func(t *testing.T) { + provider.resourceMap.Resources["aws:s3/bucket:Bucket"] = metadata.CloudAPIResource{ + CfType: "AWS::S3::Bucket", + } + req.Urn = string(resource.NewURN("stack", "project", "parent", "aws:s3/bucket:Bucket", "name")) + + mockCCC.EXPECT().Create(ctx, "AWS::S3::Bucket", gomock.Any()).Return( + nil, nil, assert.AnError, + ) + + resp, err := provider.Create(ctx, req) + assert.Error(t, err) + assert.Nil(t, resp) + }) + + t.Run("PartialError", func(t *testing.T) { + provider.resourceMap.Resources["aws:s3/bucket:Bucket"] = metadata.CloudAPIResource{ + CfType: "AWS::S3::Bucket", + } + req.Urn = string(resource.NewURN("stack", "project", "parent", "aws:s3/bucket:Bucket", "name")) + + mockCCC.EXPECT().Create(ctx, "AWS::S3::Bucket", gomock.Any()).Return( + stringPtr("bucket-id"), map[string]interface{}{"foo": "bar"}, assert.AnError, + ) + + resp, err := provider.Create(ctx, req) + assert.Error(t, err) + resourceStatus, id, ins, outs, resourceErr := parsePluginError(err) + + assert.Nil(t, resp) + assert.Equal(t, resource.StatusPartialFailure, resourceStatus) + assert.Equal(t, "bucket-id", id.String()) + assert.Error(t, resourceErr) + + require.NotNil(t, ins) + require.NotNil(t, outs) + props := mustUnmarshalProperties(t, outs) + + require.True(t, props.HasValue("foo"), "Expected 'foo' property in response") + assert.Equal(t, "bar", props["foo"].StringValue()) + require.True(t, props.HasValue("__inputs"), "Expected '__inputs' property in response") + require.True(t, props["__inputs"].IsSecret(), "Expected '__inputs' to be a secret") + checkpoint := props["__inputs"].SecretValue().Element.ObjectValue() + require.True(t, checkpoint.HasValue("my"), "Expected 'my' property in '__inputs'") + assert.Equal(t, "input value", checkpoint["my"].StringValue()) + + inputs := mustUnmarshalProperties(t, ins) + assert.True(t, inputs.DeepEquals(checkpoint), "Expected inputs and checkpoint to be equal") + }) +} + +func TestRead(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockCCC := client.NewMockCloudControlClient(ctrl) + mockCustomResource := resources.NewMockCustomResource(ctrl) + + ctx, cancel := context.WithCancel(context.Background()) + provider := &cfnProvider{ + name: "test-provider", + resourceMap: &metadata.CloudAPIMetadata{Resources: map[string]metadata.CloudAPIResource{}}, + customResources: map[string]resources.CustomResource{"custom:resource": mockCustomResource}, + ccc: mockCCC, + canceler: &cancellationContext{ + context: ctx, + cancel: cancel, + }, + } + + urn := resource.NewURN("stack", "project", "parent", "custom:resource", "name") + req := &pulumirpc.ReadRequest{ + Urn: string(urn), + Id: "resource-id", + Properties: mustMarshalProperties(t, resource.PropertyMap{"foo": resource.NewStringProperty("bar")}), + } + + t.Run("CustomResource", func(t *testing.T) { + mockCustomResource.EXPECT().Read(ctx, urn, "resource-id", gomock.Any(), gomock.Any()).Return( + resource.PropertyMap{"foo": resource.NewStringProperty("bar")}, + resource.PropertyMap{"foo": resource.NewStringProperty("bar")}, + true, + nil, + ) + + resp, err := provider.Read(ctx, req) + assert.NoError(t, err) + assert.Equal(t, "resource-id", resp.Id) + assert.NotNil(t, resp.Properties) + }) + + t.Run("CustomResource/NotFound", func(t *testing.T) { + mockCustomResource.EXPECT().Read(ctx, urn, "resource-id", gomock.Any(), gomock.Any()).Return( + nil, + nil, + false, + nil, + ) + + resp, err := provider.Read(ctx, req) + assert.NoError(t, err) + assert.Empty(t, resp.Id, "Expected empty ID for non-existent resource") + }) + + t.Run("CustomResource/Error", func(t *testing.T) { + mockCustomResource.EXPECT().Read(ctx, urn, "resource-id", gomock.Any(), gomock.Any()).Return( + nil, + nil, + false, + assert.AnError, + ) + + resp, err := provider.Read(ctx, req) + assert.Error(t, err) + assert.Nil(t, resp) + }) + + // In the import case there is no `__inputs` field in the old state. + t.Run("StandardResource/Import", func(t *testing.T) { + provider.resourceMap.Resources["aws:s3/bucket:Bucket"] = metadata.CloudAPIResource{ + CfType: "AWS::S3::Bucket", + Inputs: map[string]schema.PropertySpec{ + "bucketName": {TypeSpec: schema.TypeSpec{Type: "string"}}, + "objectLockEnabled": {TypeSpec: schema.TypeSpec{Type: "boolean"}}, + }, + } + req.Urn = string(resource.NewURN("stack", "project", "parent", "aws:s3/bucket:Bucket", "name")) + + mockCCC.EXPECT().Read(ctx, "AWS::S3::Bucket", "resource-id").Return( + map[string]interface{}{ + "bucketName": "my-bucket", + "objectLockEnabled": true, + }, true, nil, + ) + + resp, err := provider.Read(ctx, req) + assert.NoError(t, err) + assert.Equal(t, "resource-id", resp.Id) + require.NotNil(t, resp.Properties) + props := mustUnmarshalProperties(t, resp.Properties) + assert.True(t, props.HasValue("bucketName"), "Expected 'bucketName' property in response") + assert.Equal(t, "my-bucket", props["bucketName"].StringValue()) + assert.True(t, props.HasValue("objectLockEnabled"), "Expected 'objectLockEnabled' property in response") + assert.True(t, props["objectLockEnabled"].BoolValue()) + + require.NotNil(t, resp.Inputs) + inputs := mustUnmarshalProperties(t, resp.Inputs) + assert.True(t, inputs.HasValue("bucketName"), "Expected 'bucketName' property in inputs") + assert.Equal(t, "my-bucket", inputs["bucketName"].StringValue()) + assert.True(t, inputs.HasValue("objectLockEnabled"), "Expected 'objectLockEnabled' property in inputs") + assert.True(t, inputs["objectLockEnabled"].BoolValue()) + }) + + t.Run("StandardResource/NotFound", func(t *testing.T) { + req.Urn = string(resource.NewURN("stack", "project", "parent", "unknown:resource", "name")) + + resp, err := provider.Read(ctx, req) + assert.Error(t, err) + assert.Nil(t, resp) + }) + + t.Run("StandardResource/Error", func(t *testing.T) { + provider.resourceMap.Resources["aws:s3/bucket:Bucket"] = metadata.CloudAPIResource{ + CfType: "AWS::S3::Bucket", + } + req.Urn = string(resource.NewURN("stack", "project", "parent", "aws:s3/bucket:Bucket", "name")) + + mockCCC.EXPECT().Read(ctx, "AWS::S3::Bucket", "resource-id").Return( + nil, false, assert.AnError, + ) + + resp, err := provider.Read(ctx, req) + assert.Error(t, err) + assert.Nil(t, resp) + }) + + t.Run("StandardResource/NoDiff", func(t *testing.T) { + provider.resourceMap.Resources["aws:s3/bucket:Bucket"] = metadata.CloudAPIResource{ + CfType: "AWS::S3::Bucket", + Inputs: map[string]schema.PropertySpec{ + "bucketName": {TypeSpec: schema.TypeSpec{Type: "string"}}, + "objectLockEnabled": {TypeSpec: schema.TypeSpec{Type: "boolean"}}, + }, + } + req.Urn = string(resource.NewURN("stack", "project", "parent", "aws:s3/bucket:Bucket", "name")) + + inputs := resource.PropertyMap{ + "bucketName": resource.NewStringProperty("my-bucket"), + "objectLockEnabled": resource.NewBoolProperty(true), + } + + req.Properties = mustMarshalProperties(t, resource.PropertyMap{ + "foo": resource.NewStringProperty("bar"), + "__inputs": resource.MakeSecret(resource.NewObjectProperty(inputs)), + }) + + mockCCC.EXPECT().Read(ctx, "AWS::S3::Bucket", "resource-id").Return( + map[string]interface{}{ + "foo": "bar", + "bucketName": "my-bucket", + "objectLockEnabled": true, + }, true, nil, + ) + + resp, err := provider.Read(ctx, req) + assert.NoError(t, err) + require.NotNil(t, resp.Properties) + + props := mustUnmarshalProperties(t, resp.Properties) + assert.True(t, props.HasValue("foo"), "Expected 'foo' property in response") + assert.Equal(t, "bar", props["foo"].StringValue()) + + require.NotNil(t, resp.Inputs) + inputs = mustUnmarshalProperties(t, resp.Inputs) + assert.True(t, inputs.HasValue("bucketName"), "Expected 'bucketName' property in inputs") + assert.Equal(t, "my-bucket", inputs["bucketName"].StringValue()) + assert.True(t, inputs.HasValue("objectLockEnabled"), "Expected 'objectLockEnabled' property in inputs") + assert.True(t, inputs["objectLockEnabled"].BoolValue()) + }) + + t.Run("StandardResource/Diff", func(t *testing.T) { + provider.resourceMap.Resources["aws:s3/bucket:Bucket"] = metadata.CloudAPIResource{ + CfType: "AWS::S3::Bucket", + Inputs: map[string]schema.PropertySpec{ + "bucketName": {TypeSpec: schema.TypeSpec{Type: "string"}}, + "objectLockEnabled": {TypeSpec: schema.TypeSpec{Type: "boolean"}}, + }, + } + req.Urn = string(resource.NewURN("stack", "project", "parent", "aws:s3/bucket:Bucket", "name")) + + inputs := resource.PropertyMap{ + "bucketName": resource.NewStringProperty("my-bucket"), + "objectLockEnabled": resource.NewBoolProperty(true), + } + + req.Properties = mustMarshalProperties(t, resource.PropertyMap{ + "bucketName": resource.NewStringProperty("my-bucket"), + "objectLockEnabled": resource.NewBoolProperty(true), + "__inputs": resource.MakeSecret(resource.NewObjectProperty(inputs)), + }) + + mockCCC.EXPECT().Read(ctx, "AWS::S3::Bucket", "resource-id").Return( + map[string]interface{}{ + "foo": "bar", + // Change the bucket name and object lock status + "bucketName": "other-bucket", + "objectLockEnabled": false, + }, true, nil, + ) + + resp, err := provider.Read(ctx, req) + assert.NoError(t, err) + require.NotNil(t, resp.Properties) + + props := mustUnmarshalProperties(t, resp.Properties) + assert.True(t, props.HasValue("bucketName"), "Expected 'bucketName' property in response") + assert.Equal(t, "other-bucket", props["bucketName"].StringValue()) + assert.True(t, props.HasValue("objectLockEnabled"), "Expected 'objectLockEnabled' property in response") + assert.False(t, props["objectLockEnabled"].BoolValue()) + + require.NotNil(t, resp.Inputs) + // TODO: Enable those assertions after fixing https://github.com/pulumi/pulumi-aws-native/issues/1796 + // inputs = mustUnmarshalProperties(t, resp.Inputs) + // assert.True(t, inputs.HasValue("bucketName"), "Expected 'bucketName' property in inputs") + // assert.Equal(t, "other-bucket", inputs["bucketName"].StringValue()) + // assert.True(t, inputs.HasValue("objectLockEnabled"), "Expected 'objectLockEnabled' property in inputs") + // assert.False(t, inputs["objectLockEnabled"].BoolValue()) + }) +} + +func TestUpdate(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockCCC := client.NewMockCloudControlClient(ctrl) + mockCustomResource := resources.NewMockCustomResource(ctrl) + + ctx, cancel := context.WithCancel(context.Background()) + provider := &cfnProvider{ + name: "test-provider", + resourceMap: &metadata.CloudAPIMetadata{Resources: map[string]metadata.CloudAPIResource{}}, + customResources: map[string]resources.CustomResource{"custom:resource": mockCustomResource}, + ccc: mockCCC, + canceler: &cancellationContext{ + context: ctx, + cancel: cancel, + }, + } + + urn := resource.NewURN("stack", "project", "parent", "custom:resource", "name") + req := &pulumirpc.UpdateRequest{ + Urn: string(urn), + Id: "resource-id", + Olds: mustMarshalProperties(t, resource.PropertyMap{"my": resource.NewStringProperty("old value")}), + News: mustMarshalProperties(t, resource.PropertyMap{"my": resource.NewStringProperty("new value")}), + Timeout: float64((5 * time.Minute).Seconds()), + } + + t.Run("CustomResource", func(t *testing.T) { + mockCustomResource.EXPECT().Update(ctx, urn, "resource-id", gomock.Any(), gomock.Any(), 5*time.Minute).Return( + resource.PropertyMap{"foo": resource.NewStringProperty("bar")}, nil, + ) + + resp, err := provider.Update(ctx, req) + assert.NoError(t, err) + require.NotNil(t, resp.Properties) + props := mustUnmarshalProperties(t, resp.Properties) + assert.True(t, props.HasValue("foo"), "Expected 'foo' property in response") + assert.Equal(t, "bar", props["foo"].StringValue()) + }) + + t.Run("CustomResource/Error", func(t *testing.T) { + mockCustomResource.EXPECT().Update(ctx, urn, "resource-id", gomock.Any(), gomock.Any(), 5*time.Minute).Return( + nil, assert.AnError, + ) + + resp, err := provider.Update(ctx, req) + assert.Error(t, err) + assert.Nil(t, resp) + }) + + t.Run("StandardResource", func(t *testing.T) { + provider.resourceMap.Resources["aws:s3/bucket:Bucket"] = metadata.CloudAPIResource{ + CfType: "AWS::S3::Bucket", + Inputs: map[string]schema.PropertySpec{ + "bucketName": {TypeSpec: schema.TypeSpec{Type: "string"}}, + "objectLockEnabled": {TypeSpec: schema.TypeSpec{Type: "boolean"}}, + }, + } + req.Urn = string(resource.NewURN("stack", "project", "parent", "aws:s3/bucket:Bucket", "name")) + + inputs := resource.PropertyMap{ + "bucketName": resource.NewStringProperty("new-bucket"), + "objectLockEnabled": resource.NewBoolProperty(false), + } + req.News = mustMarshalProperties(t, inputs) + + req.Olds = mustMarshalProperties(t, resource.PropertyMap{ + "bucketName": resource.NewStringProperty("my-bucket"), + "objectLockEnabled": resource.NewBoolProperty(true), + "__inputs": resource.MakeSecret(resource.NewObjectProperty(inputs)), + }) + + mockCCC.EXPECT().Update(ctx, "AWS::S3::Bucket", "resource-id", gomock.Any()).Return( + map[string]interface{}{ + // Change the bucket name and object lock status according to the inputs + "bucketName": resource.NewStringProperty("new-bucket"), + "objectLockEnabled": resource.NewBoolProperty(false), + }, nil, + ) + + resp, err := provider.Update(ctx, req) + assert.NoError(t, err) + require.NotNil(t, resp.Properties) + props := mustUnmarshalProperties(t, resp.Properties) + assert.True(t, props.HasValue("bucketName"), "Expected 'bucketName' property in response") + assert.Equal(t, "new-bucket", props["bucketName"].StringValue()) + assert.True(t, props.HasValue("objectLockEnabled"), "Expected 'objectLockEnabled' property in response") + assert.False(t, props["objectLockEnabled"].BoolValue()) + + require.True(t, props.HasValue("__inputs"), "Expected '__inputs' property in response") + require.True(t, props["__inputs"].IsSecret(), "Expected '__inputs' to be a secret") + checkpoint := props["__inputs"].SecretValue().Element.ObjectValue() + require.True(t, checkpoint.HasValue("bucketName"), "Expected 'bucketName' property in '__inputs'") + assert.Equal(t, "new-bucket", checkpoint["bucketName"].StringValue()) + require.True(t, checkpoint.HasValue("objectLockEnabled"), "Expected 'objectLockEnabled' property in '__inputs'") + assert.False(t, checkpoint["objectLockEnabled"].BoolValue()) + }) + + t.Run("StandardResource/Error", func(t *testing.T) { + provider.resourceMap.Resources["aws:s3/bucket:Bucket"] = metadata.CloudAPIResource{ + CfType: "AWS::S3::Bucket", + } + req.Urn = string(resource.NewURN("stack", "project", "parent", "aws:s3/bucket:Bucket", "name")) + + mockCCC.EXPECT().Update(ctx, "AWS::S3::Bucket", "resource-id", gomock.Any()).Return( + nil, assert.AnError, + ) + + resp, err := provider.Update(ctx, req) + assert.Error(t, err) + assert.Nil(t, resp) + }) +} + +func TestDelete(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockCCC := client.NewMockCloudControlClient(ctrl) + mockCustomResource := resources.NewMockCustomResource(ctrl) + + ctx, cancel := context.WithCancel(context.Background()) + provider := &cfnProvider{ + name: "test-provider", + resourceMap: &metadata.CloudAPIMetadata{Resources: map[string]metadata.CloudAPIResource{}}, + customResources: map[string]resources.CustomResource{"custom:resource": mockCustomResource}, + ccc: mockCCC, + canceler: &cancellationContext{ + context: ctx, + cancel: cancel, + }, + } + + urn := resource.NewURN("stack", "project", "parent", "custom:resource", "name") + req := &pulumirpc.DeleteRequest{ + Urn: string(urn), + Id: "resource-id", + } + + t.Run("CustomResource", func(t *testing.T) { + mockCustomResource.EXPECT().Delete(ctx, urn, "resource-id", gomock.Any(), gomock.Any()).Return(nil) + + _, err := provider.Delete(ctx, req) + assert.NoError(t, err) + }) + + t.Run("CustomResource/Error", func(t *testing.T) { + mockCustomResource.EXPECT().Delete(ctx, urn, "resource-id", gomock.Any(), gomock.Any()).Return(assert.AnError) + + _, err := provider.Delete(ctx, req) + assert.Error(t, err) + }) + + t.Run("StandardResource", func(t *testing.T) { + provider.resourceMap.Resources["aws:s3/bucket:Bucket"] = metadata.CloudAPIResource{ + CfType: "AWS::S3::Bucket", + } + req.Urn = string(resource.NewURN("stack", "project", "parent", "aws:s3/bucket:Bucket", "name")) + + mockCCC.EXPECT().Delete(ctx, "AWS::S3::Bucket", "resource-id").Return(nil) + + _, err := provider.Delete(ctx, req) + assert.NoError(t, err) + }) + + t.Run("StandardResource/Error", func(t *testing.T) { + provider.resourceMap.Resources["aws:s3/bucket:Bucket"] = metadata.CloudAPIResource{ + CfType: "AWS::S3::Bucket", + } + req.Urn = string(resource.NewURN("stack", "project", "parent", "aws:s3/bucket:Bucket", "name")) + + mockCCC.EXPECT().Delete(ctx, "AWS::S3::Bucket", "resource-id").Return(assert.AnError) + + _, err := provider.Delete(ctx, req) + assert.Error(t, err) + }) + + t.Run("StandardResource/NotFound", func(t *testing.T) { + req.Urn = string(resource.NewURN("stack", "project", "parent", "unknown:resource", "name")) + + _, err := provider.Delete(ctx, req) + assert.Error(t, err) + }) +} + +func mustMarshalProperties(t *testing.T, props resource.PropertyMap) *structpb.Struct { + marshaled, err := plugin.MarshalProperties(props, plugin.MarshalOptions{ + Label: "test", + KeepUnknowns: true, + KeepSecrets: true, + }) + if err != nil { + t.Fatalf("failed to marshal properties: %v", err) + } + return marshaled +} + +func mustUnmarshalProperties(t *testing.T, props *structpb.Struct) resource.PropertyMap { + unmarshaled, err := plugin.UnmarshalProperties(props, plugin.MarshalOptions{ + Label: "test", + KeepUnknowns: true, + KeepSecrets: true, + }) + if err != nil { + t.Fatalf("failed to unmarshal properties: %v", err) + } + return unmarshaled +} + +func parsePluginError(err error) ( + resourceStatus resource.Status, id resource.ID, inputs, props *structpb.Struct, resourceErr error, +) { + responseErr := rpcerror.Convert(err) + + // If resource was successfully created but failed to initialize, the error will be packed + // with the live properties of the object. + resourceErr = responseErr + for _, detail := range responseErr.Details() { + if initErr, ok := detail.(*pulumirpc.ErrorResourceInitFailed); ok { + id = resource.ID(initErr.GetId()) + props = initErr.GetProperties() + inputs = initErr.GetInputs() + resourceStatus = resource.StatusPartialFailure + resourceErr = &plugin.InitError{Reasons: initErr.Reasons} + break + } + } + + return resourceStatus, id, inputs, props, resourceErr +} + +func stringPtr(s string) *string { + return &s +} diff --git a/provider/pkg/resources/checkpoint.go b/provider/pkg/resources/checkpoint.go new file mode 100644 index 0000000000..1145c28150 --- /dev/null +++ b/provider/pkg/resources/checkpoint.go @@ -0,0 +1,23 @@ +// Copyright 2024, Pulumi Corporation. + +package resources + +import ( + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" +) + +// CheckpointObject puts inputs in the `__inputs` field of the state. +func CheckpointObject(inputs resource.PropertyMap, outputs map[string]interface{}) resource.PropertyMap { + object := resource.NewPropertyMapFromMap(outputs) + object["__inputs"] = resource.MakeSecret(resource.NewObjectProperty(inputs)) + return object +} + +// ParseCheckpointObject returns inputs that are saved in the `__inputs` field of the state. +func ParseCheckpointObject(obj resource.PropertyMap) resource.PropertyMap { + if inputs, ok := obj["__inputs"]; ok { + return inputs.SecretValue().Element.ObjectValue() + } + + return nil +} diff --git a/provider/pkg/resources/checkpoint_test.go b/provider/pkg/resources/checkpoint_test.go new file mode 100644 index 0000000000..abd9c33858 --- /dev/null +++ b/provider/pkg/resources/checkpoint_test.go @@ -0,0 +1,90 @@ +package resources + +import ( + "testing" + + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/stretchr/testify/assert" +) + +func TestCheckpointObject(t *testing.T) { + inputs := resource.PropertyMap{ + "input1": resource.NewStringProperty("value1"), + "input2": resource.NewNumberProperty(42), + } + + outputs := map[string]interface{}{ + "output1": "value1", + "output2": 42, + } + + result := CheckpointObject(inputs, outputs) + + // Check if outputs are correctly set + assert.Equal(t, resource.NewStringProperty("value1"), result["output1"]) + assert.Equal(t, resource.NewNumberProperty(42), result["output2"]) + + // Check if __inputs field is correctly set and is a secret + inputsField, ok := result["__inputs"] + assert.True(t, ok) + assert.True(t, inputsField.IsSecret()) + + // Check if the secret value contains the correct inputs + secretInputs := inputsField.SecretValue().Element.ObjectValue() + assert.Equal(t, inputs, secretInputs) +} + +func TestParseCheckpointObject(t *testing.T) { + inputs := resource.PropertyMap{ + "input1": resource.NewStringProperty("value1"), + "input2": resource.NewNumberProperty(42), + } + + // Create a PropertyMap with __inputs as a secret + obj := resource.PropertyMap{ + "__inputs": resource.MakeSecret(resource.NewObjectProperty(inputs)), + } + + // Parse the checkpoint object + parsedInputs := ParseCheckpointObject(obj) + + // Check if the parsed inputs match the original inputs + assert.Equal(t, inputs, parsedInputs) + + // Test with an object that does not contain __inputs + objWithoutInputs := resource.PropertyMap{ + "output1": resource.NewStringProperty("value1"), + "output2": resource.NewNumberProperty(42), + } + + // Parse the checkpoint object + parsedInputs = ParseCheckpointObject(objWithoutInputs) + + // Check if the parsed inputs are nil + assert.Nil(t, parsedInputs) +} + +func TestRoundTripCheckpointObject(t *testing.T) { + inputs := resource.PropertyMap{ + "input1": resource.NewStringProperty("value1"), + "input2": resource.NewNumberProperty(42), + } + + outputs := map[string]interface{}{ + "output1": "value1", + "output2": 42, + } + + // Create a checkpoint object + checkpoint := CheckpointObject(inputs, outputs) + + // Parse the checkpoint object + parsedInputs := ParseCheckpointObject(checkpoint) + + // Check if the parsed inputs match the original inputs + assert.Equal(t, inputs, parsedInputs) + + // Check if the outputs are still correctly set + assert.Equal(t, resource.NewStringProperty("value1"), checkpoint["output1"]) + assert.Equal(t, resource.NewNumberProperty(42), checkpoint["output2"]) +} diff --git a/provider/pkg/resources/custom.go b/provider/pkg/resources/custom.go index 934298ebfd..b25a6214e2 100644 --- a/provider/pkg/resources/custom.go +++ b/provider/pkg/resources/custom.go @@ -4,19 +4,21 @@ package resources import ( "context" + "time" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" ) +//go:generate mockgen -package resources -source custom.go -destination mock_custom_resource.go CustomResource type CustomResource interface { // Check validates and transforms the inputs of the resource. Check(ctx context.Context, urn resource.URN, randomSeed []byte, inputs, state resource.PropertyMap, defaultTags map[string]string) (resource.PropertyMap, []ValidationFailure, error) // Create creates a new resource in the cloud provider and returns its unique identifier and outputs. - Create(ctx context.Context, urn resource.URN, inputs resource.PropertyMap) (identifier *string, outputs map[string]any, err error) + Create(ctx context.Context, urn resource.URN, inputs resource.PropertyMap, timeout time.Duration) (identifier *string, outputs resource.PropertyMap, err error) // Read returns the outputs and the updated inputs of the resource. - Read(ctx context.Context, urn resource.URN, id string, oldInputs, oldOutputs resource.PropertyMap) (outputs map[string]any, inputs resource.PropertyMap, exists bool, err error) + Read(ctx context.Context, urn resource.URN, id string, oldInputs, oldOutputs resource.PropertyMap) (outputs resource.PropertyMap, inputs resource.PropertyMap, exists bool, err error) // Update applies the diff of the inputs to the resource and returns the updated outputs. - Update(ctx context.Context, urn resource.URN, id string, inputs, oldInputs resource.PropertyMap) (map[string]any, error) + Update(ctx context.Context, urn resource.URN, id string, inputs, oldInputs resource.PropertyMap, timeout time.Duration) (resource.PropertyMap, error) // Delete removes the resource from the cloud provider. - Delete(ctx context.Context, urn resource.URN, id string, inputs resource.PropertyMap) error + Delete(ctx context.Context, urn resource.URN, id string, inputs resource.PropertyMap, timeout time.Duration) error } diff --git a/provider/pkg/resources/extension_resource.go b/provider/pkg/resources/extension_resource.go index 21061ac486..5f20ab970b 100644 --- a/provider/pkg/resources/extension_resource.go +++ b/provider/pkg/resources/extension_resource.go @@ -5,6 +5,7 @@ package resources import ( "context" "fmt" + "time" "github.com/pulumi/pulumi-aws-native/provider/pkg/autonaming" "github.com/pulumi/pulumi-aws-native/provider/pkg/client" @@ -112,7 +113,7 @@ func ApplyDefaults(typedInputs ExtensionResourceInputs) (ExtensionResourceInputs return typedInputs, nil } -func (r *extensionResource) Create(ctx context.Context, urn resource.URN, inputs resource.PropertyMap) (identifier *string, outputs map[string]any, err error) { +func (r *extensionResource) Create(ctx context.Context, urn resource.URN, inputs resource.PropertyMap, timeout time.Duration) (identifier *string, outputs resource.PropertyMap, err error) { var typedInputs ExtensionResourceInputs _, err = resourcex.Unmarshal(&typedInputs, inputs, resourcex.UnmarshalOptions{}) if err != nil { @@ -124,10 +125,11 @@ func (r *extensionResource) Create(ctx context.Context, urn resource.URN, inputs return nil, nil, fmt.Errorf("failed to create resource: %w", err) } - return id, typedInputs.toOutputs(resourceState), nil + rawOutputs := typedInputs.toOutputs(resourceState) + return id, CheckpointObject(inputs, rawOutputs), nil } -func (r *extensionResource) Read(ctx context.Context, urn resource.URN, id string, oldInputs, oldState resource.PropertyMap) (outputs map[string]any, inputs resource.PropertyMap, exists bool, err error) { +func (r *extensionResource) Read(ctx context.Context, urn resource.URN, id string, oldInputs, oldState resource.PropertyMap) (outputs resource.PropertyMap, inputs resource.PropertyMap, exists bool, err error) { if len(oldState) == 0 { // We can't yet support import because the type would not be known. return nil, nil, false, fmt.Errorf("ExtensionResource import not implemented") @@ -161,10 +163,11 @@ func (r *extensionResource) Read(ctx context.Context, urn resource.URN, id strin typedInputs.Properties = resourcex.Decode(newProperties) newInputs := oldInputs.Copy() newInputs[resource.PropertyKey("properties")] = resource.PropertyValue{V: newProperties} - return typedInputs.toOutputs(resourceState), newInputs, true, nil + rawState := typedInputs.toOutputs(resourceState) + return CheckpointObject(newInputs, rawState), newInputs, true, nil } -func (r *extensionResource) Update(ctx context.Context, urn resource.URN, id string, inputs resource.PropertyMap, oldInputs resource.PropertyMap) (map[string]any, error) { +func (r *extensionResource) Update(ctx context.Context, urn resource.URN, id string, inputs resource.PropertyMap, oldInputs resource.PropertyMap, timeout time.Duration) (resource.PropertyMap, error) { var typedOldInputs ExtensionResourceInputs _, err := resourcex.Unmarshal(&typedOldInputs, oldInputs, resourcex.UnmarshalOptions{}) if err != nil { @@ -191,10 +194,11 @@ func (r *extensionResource) Update(ctx context.Context, urn resource.URN, id str return nil, fmt.Errorf("failed to update resource: %w", err) } - return typedInputs.toOutputs(resourceState), nil + rawState := typedInputs.toOutputs(resourceState) + return CheckpointObject(inputs, rawState), nil } -func (r *extensionResource) Delete(ctx context.Context, urn resource.URN, id string, inputs resource.PropertyMap) error { +func (r *extensionResource) Delete(ctx context.Context, urn resource.URN, id string, inputs resource.PropertyMap, timeout time.Duration) error { var typedInputs ExtensionResourceInputs _, err := resourcex.Unmarshal(&typedInputs, inputs, resourcex.UnmarshalOptions{}) if err != nil { diff --git a/provider/pkg/resources/mock_custom_resource.go b/provider/pkg/resources/mock_custom_resource.go new file mode 100644 index 0000000000..756b1f85ec --- /dev/null +++ b/provider/pkg/resources/mock_custom_resource.go @@ -0,0 +1,121 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: custom.go +// +// Generated by this command: +// +// mockgen -package resources -source custom.go -destination mock_custom_resource.go CustomResource +// + +// Package resources is a generated GoMock package. +package resources + +import ( + context "context" + reflect "reflect" + time "time" + + resource "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + gomock "go.uber.org/mock/gomock" +) + +// MockCustomResource is a mock of CustomResource interface. +type MockCustomResource struct { + ctrl *gomock.Controller + recorder *MockCustomResourceMockRecorder + isgomock struct{} +} + +// MockCustomResourceMockRecorder is the mock recorder for MockCustomResource. +type MockCustomResourceMockRecorder struct { + mock *MockCustomResource +} + +// NewMockCustomResource creates a new mock instance. +func NewMockCustomResource(ctrl *gomock.Controller) *MockCustomResource { + mock := &MockCustomResource{ctrl: ctrl} + mock.recorder = &MockCustomResourceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCustomResource) EXPECT() *MockCustomResourceMockRecorder { + return m.recorder +} + +// Check mocks base method. +func (m *MockCustomResource) Check(ctx context.Context, urn resource.URN, randomSeed []byte, inputs, state resource.PropertyMap, defaultTags map[string]string) (resource.PropertyMap, []ValidationFailure, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Check", ctx, urn, randomSeed, inputs, state, defaultTags) + ret0, _ := ret[0].(resource.PropertyMap) + ret1, _ := ret[1].([]ValidationFailure) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Check indicates an expected call of Check. +func (mr *MockCustomResourceMockRecorder) Check(ctx, urn, randomSeed, inputs, state, defaultTags any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Check", reflect.TypeOf((*MockCustomResource)(nil).Check), ctx, urn, randomSeed, inputs, state, defaultTags) +} + +// Create mocks base method. +func (m *MockCustomResource) Create(ctx context.Context, urn resource.URN, inputs resource.PropertyMap, timeout time.Duration) (*string, resource.PropertyMap, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, urn, inputs, timeout) + ret0, _ := ret[0].(*string) + ret1, _ := ret[1].(resource.PropertyMap) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Create indicates an expected call of Create. +func (mr *MockCustomResourceMockRecorder) Create(ctx, urn, inputs, timeout any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockCustomResource)(nil).Create), ctx, urn, inputs, timeout) +} + +// Delete mocks base method. +func (m *MockCustomResource) Delete(ctx context.Context, urn resource.URN, id string, inputs resource.PropertyMap, timeout time.Duration) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, urn, id, inputs, timeout) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockCustomResourceMockRecorder) Delete(ctx, urn, id, inputs, timeout any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockCustomResource)(nil).Delete), ctx, urn, id, inputs, timeout) +} + +// Read mocks base method. +func (m *MockCustomResource) Read(ctx context.Context, urn resource.URN, id string, oldInputs, oldOutputs resource.PropertyMap) (resource.PropertyMap, resource.PropertyMap, bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", ctx, urn, id, oldInputs, oldOutputs) + ret0, _ := ret[0].(resource.PropertyMap) + ret1, _ := ret[1].(resource.PropertyMap) + ret2, _ := ret[2].(bool) + ret3, _ := ret[3].(error) + return ret0, ret1, ret2, ret3 +} + +// Read indicates an expected call of Read. +func (mr *MockCustomResourceMockRecorder) Read(ctx, urn, id, oldInputs, oldOutputs any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockCustomResource)(nil).Read), ctx, urn, id, oldInputs, oldOutputs) +} + +// Update mocks base method. +func (m *MockCustomResource) Update(ctx context.Context, urn resource.URN, id string, inputs, oldInputs resource.PropertyMap, timeout time.Duration) (resource.PropertyMap, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, urn, id, inputs, oldInputs, timeout) + ret0, _ := ret[0].(resource.PropertyMap) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockCustomResourceMockRecorder) Update(ctx, urn, id, inputs, oldInputs, timeout any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockCustomResource)(nil).Update), ctx, urn, id, inputs, oldInputs, timeout) +}