diff --git a/go.mod b/go.mod index c26aba81..707a1d3c 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,11 @@ go 1.22.7 require ( github.com/hashicorp/go-memdb v1.3.4 github.com/hashicorp/terraform-json v0.24.0 - github.com/hashicorp/terraform-plugin-framework v1.13.0 + github.com/hashicorp/terraform-plugin-framework v1.13.1-0.20250116190529-2e147507972f github.com/hashicorp/terraform-plugin-framework-timeouts v0.5.0 github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0 - github.com/hashicorp/terraform-plugin-go v0.25.0 + github.com/hashicorp/terraform-plugin-framework-validators v0.16.1-0.20250116191909-1452aa1f60c0 + github.com/hashicorp/terraform-plugin-go v0.25.1-0.20250116190359-f977ddce3f6c github.com/hashicorp/terraform-plugin-mux v0.17.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0 github.com/hashicorp/terraform-plugin-testing v1.11.0 @@ -40,7 +41,7 @@ require ( github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.21.0 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect - github.com/hashicorp/terraform-registry-address v0.2.3 // indirect + github.com/hashicorp/terraform-registry-address v0.2.4 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -54,15 +55,15 @@ require ( github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/crypto v0.31.0 // indirect + golang.org/x/crypto v0.32.0 // indirect golang.org/x/mod v0.21.0 // indirect - golang.org/x/net v0.28.0 // indirect + golang.org/x/net v0.34.0 // indirect golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.8 // indirect - 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 + google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/grpc v1.69.4 // indirect + google.golang.org/protobuf v1.36.3 // indirect ) diff --git a/go.sum b/go.sum index 3376b91e..fba02832 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,10 @@ github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+ github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 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/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -42,6 +46,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= @@ -81,14 +87,16 @@ github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVW github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg= github.com/hashicorp/terraform-json v0.24.0 h1:rUiyF+x1kYawXeRth6fKFm/MdfBS6+lW4NbeATsYz8Q= github.com/hashicorp/terraform-json v0.24.0/go.mod h1:Nfj5ubo9xbu9uiAoZVBsNOjvNKB66Oyrvtit74kC7ow= -github.com/hashicorp/terraform-plugin-framework v1.13.0 h1:8OTG4+oZUfKgnfTdPTJwZ532Bh2BobF4H+yBiYJ/scw= -github.com/hashicorp/terraform-plugin-framework v1.13.0/go.mod h1:j64rwMGpgM3NYXTKuxrCnyubQb/4VKldEKlcG8cvmjU= +github.com/hashicorp/terraform-plugin-framework v1.13.1-0.20250116190529-2e147507972f h1:q2bmhRSM68j8z76TygKncKkcXf7N0DJCtyn/FllnXOI= +github.com/hashicorp/terraform-plugin-framework v1.13.1-0.20250116190529-2e147507972f/go.mod h1:zrIM+i1WrpIUn7ui8DDsJVg+nJCmteS/gisopH0M3AE= github.com/hashicorp/terraform-plugin-framework-timeouts v0.5.0 h1:I/N0g/eLZ1ZkLZXUQ0oRSXa8YG/EF0CEuQP1wXdrzKw= github.com/hashicorp/terraform-plugin-framework-timeouts v0.5.0/go.mod h1:t339KhmxnaF4SzdpxmqW8HnQBHVGYazwtfxU0qCs4eE= github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0 h1:v3DapR8gsp3EM8fKMh6up9cJUFQ2iRaFsYLP8UJnCco= github.com/hashicorp/terraform-plugin-framework-timetypes v0.5.0/go.mod h1:c3PnGE9pHBDfdEVG9t1S1C9ia5LW+gkFR0CygXlM8ak= -github.com/hashicorp/terraform-plugin-go v0.25.0 h1:oi13cx7xXA6QciMcpcFi/rwA974rdTxjqEhXJjbAyks= -github.com/hashicorp/terraform-plugin-go v0.25.0/go.mod h1:+SYagMYadJP86Kvn+TGeV+ofr/R3g4/If0O5sO96MVw= +github.com/hashicorp/terraform-plugin-framework-validators v0.16.1-0.20250116191909-1452aa1f60c0 h1:KaMZ7N/IYOT8OumvsSUADHSn31fWGEpxbdQwecoNNT0= +github.com/hashicorp/terraform-plugin-framework-validators v0.16.1-0.20250116191909-1452aa1f60c0/go.mod h1:MkoT/VDC6IAoKipt31OthEQXfgkP+7DnV6Q8tSwn27A= +github.com/hashicorp/terraform-plugin-go v0.25.1-0.20250116190359-f977ddce3f6c h1:jRBaf696GIfT5LT+ZflOQUWI6MNEdntXFp6YO11ACUU= +github.com/hashicorp/terraform-plugin-go v0.25.1-0.20250116190359-f977ddce3f6c/go.mod h1:+CXjuLDiFgqR+GcrM5a2E2Kal5t5q2jb0E3D57tTdNY= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-plugin-mux v0.17.0 h1:/J3vv3Ps2ISkbLPiZOLspFcIZ0v5ycUXCEQScudGCCw= @@ -97,8 +105,8 @@ github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0 h1:wyKCCtn6pBBL46c1uIIBNUOW github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0/go.mod h1:B0Al8NyYVr8Mp/KLwssKXG1RqnTk7FySqSn4fRuLNgw= github.com/hashicorp/terraform-plugin-testing v1.11.0 h1:MeDT5W3YHbONJt2aPQyaBsgQeAIckwPX41EUHXEn29A= github.com/hashicorp/terraform-plugin-testing v1.11.0/go.mod h1:WNAHQ3DcgV/0J+B15WTE6hDvxcUdkPPpnB1FR3M910U= -github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= -github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= +github.com/hashicorp/terraform-registry-address v0.2.4 h1:JXu/zHB2Ymg/TGVCRu10XqNa4Sh2bWcqCNyKWjnCPJA= +github.com/hashicorp/terraform-registry-address v0.2.4/go.mod h1:tUNYTVyCtU4OIGXXMDp7WNcJ+0W1B4nmstVDgHMjfAU= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= @@ -161,10 +169,20 @@ github.com/zclconf/go-cty v1.16.1 h1:a5TZEPzBFFR53udlIKApXzj8JIF4ZNQ6abH79z5R1S0 github.com/zclconf/go-cty v1.16.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= +go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= @@ -172,8 +190,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -191,8 +209,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -212,14 +230,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= -google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= +google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/framework5provider/invalid_refinement_resource.go b/internal/framework5provider/invalid_refinement_resource.go new file mode 100644 index 00000000..d13f2ee1 --- /dev/null +++ b/internal/framework5provider/invalid_refinement_resource.go @@ -0,0 +1,88 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = InvalidRefinementResource{} + +func NewInvalidRefinement() resource.Resource { + return &InvalidRefinementResource{} +} + +type InvalidRefinementResource struct{} + +func (r InvalidRefinementResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_invalid_refinement" +} + +func (r InvalidRefinementResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "string_with_prefix": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.WillHavePrefix("prefix://"), + }, + }, + }, + } +} + +func (r InvalidRefinementResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data InvalidRefinementResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + data.StringWithPrefix = types.StringValue("not correct") + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r InvalidRefinementResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data InvalidRefinementResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + data.StringWithPrefix = types.StringValue("not correct") + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r InvalidRefinementResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data InvalidRefinementResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + data.StringWithPrefix = types.StringValue("not correct") + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r InvalidRefinementResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +type InvalidRefinementResourceModel struct { + StringWithPrefix types.String `tfsdk:"string_with_prefix"` +} diff --git a/internal/framework5provider/invalid_refinement_resource_test.go b/internal/framework5provider/invalid_refinement_resource_test.go new file mode 100644 index 00000000..b5b6a944 --- /dev/null +++ b/internal/framework5provider/invalid_refinement_resource_test.go @@ -0,0 +1,48 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +// This resource tests Terraform's data consistency rules for refinements. It has an invalid +// promise (string must have prefix of "prefix://") in the plan, which will fail during apply. +func TestInvalidRefinementResource(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_invalid_refinement" "test" {} + + resource "terraform_data" "test_out" { + count = startswith(framework_invalid_refinement.test.string_with_prefix, "prefix://") ? 1 : 0 + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("framework_invalid_refinement.test", tfjsonpath.New("string_with_prefix")), + plancheck.ExpectResourceAction("terraform_data.test_out[0]", plancheck.ResourceActionCreate), + }, + }, + ExpectError: regexp.MustCompile(`Error: Provider produced inconsistent result after apply`), + }, + }, + }) +} diff --git a/internal/framework5provider/provider.go b/internal/framework5provider/provider.go index 76aa4077..438268a4 100644 --- a/internal/framework5provider/provider.go +++ b/internal/framework5provider/provider.go @@ -91,6 +91,9 @@ func (p *testProvider) Resources(_ context.Context) []func() resource.Resource { NewTFSDKReflectionResource, NewMoveStateResource, NewSetNestedBlockWithDefaultsResource, + NewRefinementProducer, + NewRefinementConsumer, + NewInvalidRefinement, } } diff --git a/internal/framework5provider/refinement_consumer_resource.go b/internal/framework5provider/refinement_consumer_resource.go new file mode 100644 index 00000000..2c3200d1 --- /dev/null +++ b/internal/framework5provider/refinement_consumer_resource.go @@ -0,0 +1,144 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = RefinementConsumerResource{} +var _ resource.ResourceWithConfigValidators = RefinementConsumerResource{} + +func NewRefinementConsumer() resource.Resource { + return &RefinementConsumerResource{} +} + +type RefinementConsumerResource struct{} + +func (r RefinementConsumerResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_refinement_consumer" +} + +func (r RefinementConsumerResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + resourcevalidator.Conflicting( + path.MatchRoot("conflicting_bool_one"), + path.MatchRoot("conflicting_bool_two"), + ), + } +} + +func (r RefinementConsumerResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "conflicting_bool_one": schema.BoolAttribute{ + Optional: true, + }, + "conflicting_bool_two": schema.BoolAttribute{ + Optional: true, + }, + "at_most_int64": schema.Int64Attribute{ + Optional: true, + Validators: []validator.Int64{ + int64validator.AtMost(9), + }, + }, + "at_least_float64": schema.Float64Attribute{ + Optional: true, + Validators: []validator.Float64{ + float64validator.AtLeast(20.235), + }, + }, + "at_least_list_size": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(6), + }, + }, + "at_most_list_size": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + }, + "at_least_set_size": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(6), + }, + }, + "at_most_string_length": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(8), + }, + }, + }, + } +} + +func (r RefinementConsumerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data RefinementConsumerResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r RefinementConsumerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data RefinementConsumerResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r RefinementConsumerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data RefinementConsumerResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r RefinementConsumerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +type RefinementConsumerResourceModel struct { + ConflictingBoolOne types.Bool `tfsdk:"conflicting_bool_one"` + ConflictingBoolTwo types.Bool `tfsdk:"conflicting_bool_two"` + AtMostInt64 types.Int64 `tfsdk:"at_most_int64"` + AtLeastFloat64 types.Float64 `tfsdk:"at_least_float64"` + AtLeastListSize types.List `tfsdk:"at_least_list_size"` + AtMostListSize types.List `tfsdk:"at_most_list_size"` + AtLeastSetSize types.Set `tfsdk:"at_least_set_size"` + AtMostStringLength types.String `tfsdk:"at_most_string_length"` +} diff --git a/internal/framework5provider/refinement_consumer_resource_test.go b/internal/framework5provider/refinement_consumer_resource_test.go new file mode 100644 index 00000000..d605132e --- /dev/null +++ b/internal/framework5provider/refinement_consumer_resource_test.go @@ -0,0 +1,204 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestRefinementConsumerResource_conflicting_validation_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "framework_refinement_consumer" "test" { + conflicting_bool_one = framework_refinement_producer.test.bool_with_not_null + conflicting_bool_two = framework_refinement_producer.test.bool_with_not_null + } + `, + // Even though "framework_refinement_producer.test.bool_with_not_null" is unknown, there is + // enough information to fail validation early. + PlanOnly: true, + ExpectError: regexp.MustCompile(`Error: Invalid Attribute Combination`), + }, + }, + }) +} + +func TestRefinementConsumerResource_at_most_string_length_validation_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "framework_refinement_consumer" "test" { + at_most_string_length = framework_refinement_producer.test.string_with_prefix + } + `, + // Even though "framework_refinement_producer.test.string_with_prefix" is unknown, there is + // enough information to fail validation early. + PlanOnly: true, + ExpectError: regexp.MustCompile(`Attribute at_most_string_length string length must be at most 8`), + }, + }, + }) +} + +func TestRefinementConsumerResource_at_most_int64_validation_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "framework_refinement_consumer" "test" { + at_most_int64 = framework_refinement_producer.test.int64_with_bounds + } + `, + // Even though "framework_refinement_producer.test.int64_with_bounds" is unknown, there is + // enough information to fail validation early. + PlanOnly: true, + ExpectError: regexp.MustCompile(`Attribute at_most_int64 value must be at most 9`), + }, + }, + }) +} + +func TestRefinementConsumerResource_at_least_float64_validation_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "framework_refinement_consumer" "test" { + at_least_float64 = framework_refinement_producer.test.float64_with_bounds + } + `, + // Even though "framework_refinement_producer.test.float64_with_bounds" is unknown, there is + // enough information to fail validation early. + PlanOnly: true, + ExpectError: regexp.MustCompile(`Attribute at_least_float64 value must be at least 20.235000`), + }, + }, + }) +} + +func TestRefinementConsumerResource_at_least_list_size_validation_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "framework_refinement_consumer" "test" { + at_least_list_size = framework_refinement_producer.test.list_with_length_bounds + } + `, + // Even though "framework_refinement_producer.test.list_with_length_bounds" is unknown, there is + // enough information to fail validation early. + PlanOnly: true, + ExpectError: regexp.MustCompile(`Attribute at_least_list_size list must contain at least 6 elements`), + }, + }, + }) +} + +func TestRefinementConsumerResource_at_most_list_size_validation_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "framework_refinement_consumer" "test" { + at_most_list_size = framework_refinement_producer.test.list_with_length_bounds + } + `, + // Even though "framework_refinement_producer.test.list_with_length_bounds" is unknown, there is + // enough information to fail validation early. + PlanOnly: true, + ExpectError: regexp.MustCompile(`Attribute at_most_list_size list must contain at most 1 elements`), + }, + }, + }) +} + +func TestRefinementConsumerResource_at_least_set_size_validation_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "framework_refinement_consumer" "test" { + at_least_set_size = framework_refinement_producer.test.list_with_length_bounds + } + `, + // Even though "framework_refinement_producer.test.list_with_length_bounds" is unknown, there is + // enough information to fail validation early. + PlanOnly: true, + ExpectError: regexp.MustCompile(`Attribute at_least_set_size set must contain at least 6 elements`), + }, + }, + }) +} diff --git a/internal/framework5provider/refinement_producer_resource.go b/internal/framework5provider/refinement_producer_resource.go new file mode 100644 index 00000000..932a5d44 --- /dev/null +++ b/internal/framework5provider/refinement_producer_resource.go @@ -0,0 +1,158 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/float64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = RefinementProducerResource{} + +func NewRefinementProducer() resource.Resource { + return &RefinementProducerResource{} +} + +type RefinementProducerResource struct{} + +func (r RefinementProducerResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_refinement_producer" +} + +func (r RefinementProducerResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "bool_with_not_null": schema.BoolAttribute{ + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.WillNotBeNull(), + }, + }, + "int64_with_bounds": schema.Int64Attribute{ + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.WillBeAtLeast(10), + int64planmodifier.WillBeAtMost(20), + }, + }, + "float64_with_bounds": schema.Float64Attribute{ + Computed: true, + PlanModifiers: []planmodifier.Float64{ + float64planmodifier.WillBeAtLeast(10.234), + float64planmodifier.WillBeAtMost(20.234), + }, + }, + "list_with_length_bounds": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + PlanModifiers: []planmodifier.List{ + listplanmodifier.WillHaveSizeAtLeast(2), + listplanmodifier.WillHaveSizeAtMost(5), + }, + }, + "string_with_prefix": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.WillHavePrefix("prefix://"), + }, + }, + }, + } +} + +func (r RefinementProducerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data RefinementProducerResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + data.BoolWithNotNull = types.BoolValue(true) + data.Int64WithBounds = types.Int64Value(15) + data.Float64WithBounds = types.Float64Value(12.102) + data.ListWithLengthBounds = types.ListValueMust( + types.StringType, + []attr.Value{ + types.StringValue("hello"), + types.StringValue("there"), + types.StringValue("world!"), + }, + ) + data.StringWithPrefix = types.StringValue("prefix://hello-world") + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r RefinementProducerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data RefinementProducerResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + data.BoolWithNotNull = types.BoolValue(true) + data.Int64WithBounds = types.Int64Value(15) + data.Float64WithBounds = types.Float64Value(12.102) + data.ListWithLengthBounds = types.ListValueMust( + types.StringType, + []attr.Value{ + types.StringValue("hello"), + types.StringValue("there"), + types.StringValue("world!"), + }, + ) + data.StringWithPrefix = types.StringValue("prefix://hello-world") + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r RefinementProducerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data RefinementProducerResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + data.BoolWithNotNull = types.BoolValue(true) + data.Int64WithBounds = types.Int64Value(15) + data.Float64WithBounds = types.Float64Value(12.102) + data.ListWithLengthBounds = types.ListValueMust( + types.StringType, + []attr.Value{ + types.StringValue("hello"), + types.StringValue("there"), + types.StringValue("world!"), + }, + ) + data.StringWithPrefix = types.StringValue("prefix://hello-world") + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r RefinementProducerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +type RefinementProducerResourceModel struct { + BoolWithNotNull types.Bool `tfsdk:"bool_with_not_null"` + Int64WithBounds types.Int64 `tfsdk:"int64_with_bounds"` + Float64WithBounds types.Float64 `tfsdk:"float64_with_bounds"` + ListWithLengthBounds types.List `tfsdk:"list_with_length_bounds"` + StringWithPrefix types.String `tfsdk:"string_with_prefix"` +} diff --git a/internal/framework5provider/refinement_producer_resource_test.go b/internal/framework5provider/refinement_producer_resource_test.go new file mode 100644 index 00000000..540993df --- /dev/null +++ b/internal/framework5provider/refinement_producer_resource_test.go @@ -0,0 +1,335 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestRefinementProducerResource_basic_pre_1_3(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // This test runs against earlier Terraform versions to ensure provider-returned refinements + // don't cause unexpected issues (core should ignore them) + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Terraform 1.2 and older treat the entire output value as unknown + tfversion.SkipAbove(tfversion.Version1_2_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_refinement_producer" "test" {} + + output "test_out" { + value = framework_refinement_producer.test + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValue("test_out"), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue("test_out", knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "bool_with_not_null": knownvalue.Bool(true), + "int64_with_bounds": knownvalue.Int64Exact(15), + "float64_with_bounds": knownvalue.Float64Exact(12.102), + "list_with_length_bounds": knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("hello"), + knownvalue.StringExact("there"), + knownvalue.StringExact("world!"), + }), + "string_with_prefix": knownvalue.StringExact("prefix://hello-world"), + }, + )), + }, + }, + }, + }) +} + +func TestRefinementProducerResource_basic(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // This test runs against earlier Terraform versions to ensure provider-returned refinements don't cause unexpected issues (core should ignore them) + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Terraform 1.3 and above have more fine-grained unknown output values to assert during plan + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_refinement_producer" "test" {} + + output "test_out" { + value = framework_refinement_producer.test + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValueAtPath("test_out", tfjsonpath.New("bool_with_not_null")), + plancheck.ExpectUnknownOutputValueAtPath("test_out", tfjsonpath.New("int64_with_bounds")), + plancheck.ExpectUnknownOutputValueAtPath("test_out", tfjsonpath.New("float64_with_bounds")), + plancheck.ExpectUnknownOutputValueAtPath("test_out", tfjsonpath.New("list_with_length_bounds")), + plancheck.ExpectUnknownOutputValueAtPath("test_out", tfjsonpath.New("string_with_prefix")), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue("test_out", knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "bool_with_not_null": knownvalue.Bool(true), + "int64_with_bounds": knownvalue.Int64Exact(15), + "float64_with_bounds": knownvalue.Float64Exact(12.102), + "list_with_length_bounds": knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("hello"), + knownvalue.StringExact("there"), + knownvalue.StringExact("world!"), + }), + "string_with_prefix": knownvalue.StringExact("prefix://hello-world"), + }, + )), + }, + }, + }, + }) +} + +func TestRefinementProducerResource_notnull(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + // Without refinement support in the provider, this config would return an error like: + // + // Error: Invalid count argument + // + // on terraform_plugin_test.tf line 15, in resource "terraform_data" "test_out": + // 15: count = framework_refinement_producer.test.bool_with_not_null != null ? 1 : 0 + // + // The "count" value depends on resource attributes that cannot be determined + // until apply, so Terraform cannot predict how many instances will be created. + // To work around this, use the -target argument to first apply only the + // resources that the count depends on. + // + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "terraform_data" "test_out" { + count = framework_refinement_producer.test.bool_with_not_null != null ? 1 : 0 + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("framework_refinement_producer.test", tfjsonpath.New("bool_with_not_null")), + plancheck.ExpectResourceAction("terraform_data.test_out[0]", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_refinement_producer.test", tfjsonpath.New("bool_with_not_null"), knownvalue.Bool(true)), + }, + }, + }, + }) +} + +func TestRefinementProducerResource_stringprefix(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + // Without refinement support in the provider, this config would return an error like: + // + // Error: Invalid count argument + // + // on terraform_plugin_test.tf line 15, in resource "terraform_data" "test_out": + // 15: count = startswith(framework_refinement_producer.test.string_with_prefix, "prefix://") ? 1 : 0 + // + // The "count" value depends on resource attributes that cannot be determined + // until apply, so Terraform cannot predict how many instances will be created. + // To work around this, use the -target argument to first apply only the + // resources that the count depends on. + // + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "terraform_data" "test_out" { + count = startswith(framework_refinement_producer.test.string_with_prefix, "prefix://") ? 1 : 0 + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("framework_refinement_producer.test", tfjsonpath.New("string_with_prefix")), + plancheck.ExpectResourceAction("terraform_data.test_out[0]", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_refinement_producer.test", tfjsonpath.New("string_with_prefix"), knownvalue.StringExact("prefix://hello-world")), + }, + }, + }, + }) +} + +func TestRefinementProducerResource_int64_bounds(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + // Without refinement support in the provider, this config would return an error like: + // + // Error: Invalid count argument + // + // on terraform_plugin_test.tf line 15, in resource "terraform_data" "test_out": + // 15: count = framework_refinement_producer.test.int64_with_bounds > 9 ? 1 : 0 + // + // The "count" value depends on resource attributes that cannot be determined + // until apply, so Terraform cannot predict how many instances will be created. + // To work around this, use the -target argument to first apply only the + // resources that the count depends on. + // + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "terraform_data" "test_out" { + count = framework_refinement_producer.test.int64_with_bounds > 9 ? 1 : 0 + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("framework_refinement_producer.test", tfjsonpath.New("int64_with_bounds")), + plancheck.ExpectResourceAction("terraform_data.test_out[0]", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_refinement_producer.test", tfjsonpath.New("int64_with_bounds"), knownvalue.Int64Exact(15)), + }, + }, + }, + }) +} +func TestRefinementProducerResource_float64_bounds(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + // Without refinement support in the provider, this config would return an error like: + // + // Error: Invalid count argument + // + // on terraform_plugin_test.tf line 15, in resource "terraform_data" "test_out": + // 15: count = framework_refinement_producer.test.float64_with_bounds > 10.233 ? 1 : 0 + // + // The "count" value depends on resource attributes that cannot be determined + // until apply, so Terraform cannot predict how many instances will be created. + // To work around this, use the -target argument to first apply only the + // resources that the count depends on. + // + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "terraform_data" "test_out" { + count = framework_refinement_producer.test.float64_with_bounds > 10.233 ? 1 : 0 + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("framework_refinement_producer.test", tfjsonpath.New("float64_with_bounds")), + plancheck.ExpectResourceAction("terraform_data.test_out[0]", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_refinement_producer.test", tfjsonpath.New("float64_with_bounds"), knownvalue.Float64Exact(12.102)), + }, + }, + }, + }) +} + +func TestRefinementProducerResource_list_length_bounds(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + // Without refinement support in the provider, this config would return an error like: + // + // Error: Invalid count argument + + // on terraform_plugin_test.tf line 15, in resource "terraform_data" "test_out": + // 15: count = length(framework_refinement_producer.test.list_with_length_bounds) > 1 ? 1 : 0 + + // The "count" value depends on resource attributes that cannot be determined + // until apply, so Terraform cannot predict how many instances will be created. + // To work around this, use the -target argument to first apply only the + // resources that the count depends on. + // + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "terraform_data" "test_out" { + count = length(framework_refinement_producer.test.list_with_length_bounds) > 1 ? 1 : 0 + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("framework_refinement_producer.test", tfjsonpath.New("list_with_length_bounds")), + plancheck.ExpectResourceAction("terraform_data.test_out[0]", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_refinement_producer.test", tfjsonpath.New("list_with_length_bounds"), knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.StringExact("hello"), + knownvalue.StringExact("there"), + knownvalue.StringExact("world!"), + }, + )), + }, + }, + }, + }) +} diff --git a/internal/framework6provider/invalid_refinement_resource.go b/internal/framework6provider/invalid_refinement_resource.go new file mode 100644 index 00000000..d13f2ee1 --- /dev/null +++ b/internal/framework6provider/invalid_refinement_resource.go @@ -0,0 +1,88 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = InvalidRefinementResource{} + +func NewInvalidRefinement() resource.Resource { + return &InvalidRefinementResource{} +} + +type InvalidRefinementResource struct{} + +func (r InvalidRefinementResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_invalid_refinement" +} + +func (r InvalidRefinementResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "string_with_prefix": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.WillHavePrefix("prefix://"), + }, + }, + }, + } +} + +func (r InvalidRefinementResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data InvalidRefinementResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + data.StringWithPrefix = types.StringValue("not correct") + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r InvalidRefinementResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data InvalidRefinementResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + data.StringWithPrefix = types.StringValue("not correct") + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r InvalidRefinementResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data InvalidRefinementResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + data.StringWithPrefix = types.StringValue("not correct") + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r InvalidRefinementResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +type InvalidRefinementResourceModel struct { + StringWithPrefix types.String `tfsdk:"string_with_prefix"` +} diff --git a/internal/framework6provider/invalid_refinement_resource_test.go b/internal/framework6provider/invalid_refinement_resource_test.go new file mode 100644 index 00000000..87149abc --- /dev/null +++ b/internal/framework6provider/invalid_refinement_resource_test.go @@ -0,0 +1,48 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +// This resource tests Terraform's data consistency rules for refinements. It has an invalid +// promise (string must have prefix of "prefix://") in the plan, which will fail during apply. +func TestInvalidRefinementResource(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_invalid_refinement" "test" {} + + resource "terraform_data" "test_out" { + count = startswith(framework_invalid_refinement.test.string_with_prefix, "prefix://") ? 1 : 0 + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("framework_invalid_refinement.test", tfjsonpath.New("string_with_prefix")), + plancheck.ExpectResourceAction("terraform_data.test_out[0]", plancheck.ResourceActionCreate), + }, + }, + ExpectError: regexp.MustCompile(`Error: Provider produced inconsistent result after apply`), + }, + }, + }) +} diff --git a/internal/framework6provider/provider.go b/internal/framework6provider/provider.go index e5a3df36..a9be0ef1 100644 --- a/internal/framework6provider/provider.go +++ b/internal/framework6provider/provider.go @@ -91,6 +91,9 @@ func (p *testProvider) Resources(_ context.Context) []func() resource.Resource { NewMoveStateResource, NewSetNestedBlockWithDefaultsResource, NewSetNestedAttributeWithDefaultsResource, + NewRefinementProducer, + NewRefinementConsumer, + NewInvalidRefinement, } } diff --git a/internal/framework6provider/refinement_consumer_resource.go b/internal/framework6provider/refinement_consumer_resource.go new file mode 100644 index 00000000..2c3200d1 --- /dev/null +++ b/internal/framework6provider/refinement_consumer_resource.go @@ -0,0 +1,144 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework-validators/float64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = RefinementConsumerResource{} +var _ resource.ResourceWithConfigValidators = RefinementConsumerResource{} + +func NewRefinementConsumer() resource.Resource { + return &RefinementConsumerResource{} +} + +type RefinementConsumerResource struct{} + +func (r RefinementConsumerResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_refinement_consumer" +} + +func (r RefinementConsumerResource) ConfigValidators(ctx context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + resourcevalidator.Conflicting( + path.MatchRoot("conflicting_bool_one"), + path.MatchRoot("conflicting_bool_two"), + ), + } +} + +func (r RefinementConsumerResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "conflicting_bool_one": schema.BoolAttribute{ + Optional: true, + }, + "conflicting_bool_two": schema.BoolAttribute{ + Optional: true, + }, + "at_most_int64": schema.Int64Attribute{ + Optional: true, + Validators: []validator.Int64{ + int64validator.AtMost(9), + }, + }, + "at_least_float64": schema.Float64Attribute{ + Optional: true, + Validators: []validator.Float64{ + float64validator.AtLeast(20.235), + }, + }, + "at_least_list_size": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(6), + }, + }, + "at_most_list_size": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + }, + "at_least_set_size": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Validators: []validator.Set{ + setvalidator.SizeAtLeast(6), + }, + }, + "at_most_string_length": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(8), + }, + }, + }, + } +} + +func (r RefinementConsumerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data RefinementConsumerResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r RefinementConsumerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data RefinementConsumerResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r RefinementConsumerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data RefinementConsumerResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r RefinementConsumerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +type RefinementConsumerResourceModel struct { + ConflictingBoolOne types.Bool `tfsdk:"conflicting_bool_one"` + ConflictingBoolTwo types.Bool `tfsdk:"conflicting_bool_two"` + AtMostInt64 types.Int64 `tfsdk:"at_most_int64"` + AtLeastFloat64 types.Float64 `tfsdk:"at_least_float64"` + AtLeastListSize types.List `tfsdk:"at_least_list_size"` + AtMostListSize types.List `tfsdk:"at_most_list_size"` + AtLeastSetSize types.Set `tfsdk:"at_least_set_size"` + AtMostStringLength types.String `tfsdk:"at_most_string_length"` +} diff --git a/internal/framework6provider/refinement_consumer_resource_test.go b/internal/framework6provider/refinement_consumer_resource_test.go new file mode 100644 index 00000000..eb68babf --- /dev/null +++ b/internal/framework6provider/refinement_consumer_resource_test.go @@ -0,0 +1,204 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestRefinementConsumerResource_conflicting_validation_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "framework_refinement_consumer" "test" { + conflicting_bool_one = framework_refinement_producer.test.bool_with_not_null + conflicting_bool_two = framework_refinement_producer.test.bool_with_not_null + } + `, + // Even though "framework_refinement_producer.test.bool_with_not_null" is unknown, there is + // enough information to fail validation early. + PlanOnly: true, + ExpectError: regexp.MustCompile(`Error: Invalid Attribute Combination`), + }, + }, + }) +} + +func TestRefinementConsumerResource_at_most_string_length_validation_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "framework_refinement_consumer" "test" { + at_most_string_length = framework_refinement_producer.test.string_with_prefix + } + `, + // Even though "framework_refinement_producer.test.string_with_prefix" is unknown, there is + // enough information to fail validation early. + PlanOnly: true, + ExpectError: regexp.MustCompile(`Attribute at_most_string_length string length must be at most 8`), + }, + }, + }) +} + +func TestRefinementConsumerResource_at_most_int64_validation_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "framework_refinement_consumer" "test" { + at_most_int64 = framework_refinement_producer.test.int64_with_bounds + } + `, + // Even though "framework_refinement_producer.test.int64_with_bounds" is unknown, there is + // enough information to fail validation early. + PlanOnly: true, + ExpectError: regexp.MustCompile(`Attribute at_most_int64 value must be at most 9`), + }, + }, + }) +} + +func TestRefinementConsumerResource_at_least_float64_validation_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "framework_refinement_consumer" "test" { + at_least_float64 = framework_refinement_producer.test.float64_with_bounds + } + `, + // Even though "framework_refinement_producer.test.float64_with_bounds" is unknown, there is + // enough information to fail validation early. + PlanOnly: true, + ExpectError: regexp.MustCompile(`Attribute at_least_float64 value must be at least 20.235000`), + }, + }, + }) +} + +func TestRefinementConsumerResource_at_least_list_size_validation_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "framework_refinement_consumer" "test" { + at_least_list_size = framework_refinement_producer.test.list_with_length_bounds + } + `, + // Even though "framework_refinement_producer.test.list_with_length_bounds" is unknown, there is + // enough information to fail validation early. + PlanOnly: true, + ExpectError: regexp.MustCompile(`Attribute at_least_list_size list must contain at least 6 elements`), + }, + }, + }) +} + +func TestRefinementConsumerResource_at_most_list_size_validation_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "framework_refinement_consumer" "test" { + at_most_list_size = framework_refinement_producer.test.list_with_length_bounds + } + `, + // Even though "framework_refinement_producer.test.list_with_length_bounds" is unknown, there is + // enough information to fail validation early. + PlanOnly: true, + ExpectError: regexp.MustCompile(`Attribute at_most_list_size list must contain at most 1 elements`), + }, + }, + }) +} + +func TestRefinementConsumerResource_at_least_set_size_validation_error(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "framework_refinement_consumer" "test" { + at_least_set_size = framework_refinement_producer.test.list_with_length_bounds + } + `, + // Even though "framework_refinement_producer.test.list_with_length_bounds" is unknown, there is + // enough information to fail validation early. + PlanOnly: true, + ExpectError: regexp.MustCompile(`Attribute at_least_set_size set must contain at least 6 elements`), + }, + }, + }) +} diff --git a/internal/framework6provider/refinement_producer_resource.go b/internal/framework6provider/refinement_producer_resource.go new file mode 100644 index 00000000..932a5d44 --- /dev/null +++ b/internal/framework6provider/refinement_producer_resource.go @@ -0,0 +1,158 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/float64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = RefinementProducerResource{} + +func NewRefinementProducer() resource.Resource { + return &RefinementProducerResource{} +} + +type RefinementProducerResource struct{} + +func (r RefinementProducerResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_refinement_producer" +} + +func (r RefinementProducerResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "bool_with_not_null": schema.BoolAttribute{ + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.WillNotBeNull(), + }, + }, + "int64_with_bounds": schema.Int64Attribute{ + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.WillBeAtLeast(10), + int64planmodifier.WillBeAtMost(20), + }, + }, + "float64_with_bounds": schema.Float64Attribute{ + Computed: true, + PlanModifiers: []planmodifier.Float64{ + float64planmodifier.WillBeAtLeast(10.234), + float64planmodifier.WillBeAtMost(20.234), + }, + }, + "list_with_length_bounds": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + PlanModifiers: []planmodifier.List{ + listplanmodifier.WillHaveSizeAtLeast(2), + listplanmodifier.WillHaveSizeAtMost(5), + }, + }, + "string_with_prefix": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.WillHavePrefix("prefix://"), + }, + }, + }, + } +} + +func (r RefinementProducerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data RefinementProducerResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + data.BoolWithNotNull = types.BoolValue(true) + data.Int64WithBounds = types.Int64Value(15) + data.Float64WithBounds = types.Float64Value(12.102) + data.ListWithLengthBounds = types.ListValueMust( + types.StringType, + []attr.Value{ + types.StringValue("hello"), + types.StringValue("there"), + types.StringValue("world!"), + }, + ) + data.StringWithPrefix = types.StringValue("prefix://hello-world") + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r RefinementProducerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data RefinementProducerResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + data.BoolWithNotNull = types.BoolValue(true) + data.Int64WithBounds = types.Int64Value(15) + data.Float64WithBounds = types.Float64Value(12.102) + data.ListWithLengthBounds = types.ListValueMust( + types.StringType, + []attr.Value{ + types.StringValue("hello"), + types.StringValue("there"), + types.StringValue("world!"), + }, + ) + data.StringWithPrefix = types.StringValue("prefix://hello-world") + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r RefinementProducerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data RefinementProducerResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + data.BoolWithNotNull = types.BoolValue(true) + data.Int64WithBounds = types.Int64Value(15) + data.Float64WithBounds = types.Float64Value(12.102) + data.ListWithLengthBounds = types.ListValueMust( + types.StringType, + []attr.Value{ + types.StringValue("hello"), + types.StringValue("there"), + types.StringValue("world!"), + }, + ) + data.StringWithPrefix = types.StringValue("prefix://hello-world") + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r RefinementProducerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +type RefinementProducerResourceModel struct { + BoolWithNotNull types.Bool `tfsdk:"bool_with_not_null"` + Int64WithBounds types.Int64 `tfsdk:"int64_with_bounds"` + Float64WithBounds types.Float64 `tfsdk:"float64_with_bounds"` + ListWithLengthBounds types.List `tfsdk:"list_with_length_bounds"` + StringWithPrefix types.String `tfsdk:"string_with_prefix"` +} diff --git a/internal/framework6provider/refinement_producer_resource_test.go b/internal/framework6provider/refinement_producer_resource_test.go new file mode 100644 index 00000000..ae1f09e0 --- /dev/null +++ b/internal/framework6provider/refinement_producer_resource_test.go @@ -0,0 +1,335 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestRefinementProducerResource_basic_pre_1_3(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // This test runs against earlier Terraform versions to ensure provider-returned refinements + // don't cause unexpected issues (core should ignore them) + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Terraform 1.2 and older treat the entire output value as unknown + tfversion.SkipAbove(tfversion.Version1_2_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_refinement_producer" "test" {} + + output "test_out" { + value = framework_refinement_producer.test + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValue("test_out"), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue("test_out", knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "bool_with_not_null": knownvalue.Bool(true), + "int64_with_bounds": knownvalue.Int64Exact(15), + "float64_with_bounds": knownvalue.Float64Exact(12.102), + "list_with_length_bounds": knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("hello"), + knownvalue.StringExact("there"), + knownvalue.StringExact("world!"), + }), + "string_with_prefix": knownvalue.StringExact("prefix://hello-world"), + }, + )), + }, + }, + }, + }) +} + +func TestRefinementProducerResource_basic(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + // This test runs against earlier Terraform versions to ensure provider-returned refinements don't cause unexpected issues (core should ignore them) + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Terraform 1.3+ have more fine-grained unknown output values to assert during plan + tfversion.SkipBelow(tfversion.Version1_3_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: ` + resource "framework_refinement_producer" "test" {} + + output "test_out" { + value = framework_refinement_producer.test + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownOutputValueAtPath("test_out", tfjsonpath.New("bool_with_not_null")), + plancheck.ExpectUnknownOutputValueAtPath("test_out", tfjsonpath.New("int64_with_bounds")), + plancheck.ExpectUnknownOutputValueAtPath("test_out", tfjsonpath.New("float64_with_bounds")), + plancheck.ExpectUnknownOutputValueAtPath("test_out", tfjsonpath.New("list_with_length_bounds")), + plancheck.ExpectUnknownOutputValueAtPath("test_out", tfjsonpath.New("string_with_prefix")), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownOutputValue("test_out", knownvalue.ObjectExact( + map[string]knownvalue.Check{ + "bool_with_not_null": knownvalue.Bool(true), + "int64_with_bounds": knownvalue.Int64Exact(15), + "float64_with_bounds": knownvalue.Float64Exact(12.102), + "list_with_length_bounds": knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("hello"), + knownvalue.StringExact("there"), + knownvalue.StringExact("world!"), + }), + "string_with_prefix": knownvalue.StringExact("prefix://hello-world"), + }, + )), + }, + }, + }, + }) +} + +func TestRefinementProducerResource_notnull(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + // Without refinement support in the provider, this config would return an error like: + // + // Error: Invalid count argument + // + // on terraform_plugin_test.tf line 15, in resource "terraform_data" "test_out": + // 15: count = framework_refinement_producer.test.bool_with_not_null != null ? 1 : 0 + // + // The "count" value depends on resource attributes that cannot be determined + // until apply, so Terraform cannot predict how many instances will be created. + // To work around this, use the -target argument to first apply only the + // resources that the count depends on. + // + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "terraform_data" "test_out" { + count = framework_refinement_producer.test.bool_with_not_null != null ? 1 : 0 + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("framework_refinement_producer.test", tfjsonpath.New("bool_with_not_null")), + plancheck.ExpectResourceAction("terraform_data.test_out[0]", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_refinement_producer.test", tfjsonpath.New("bool_with_not_null"), knownvalue.Bool(true)), + }, + }, + }, + }) +} + +func TestRefinementProducerResource_stringprefix(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + // Without refinement support in the provider, this config would return an error like: + // + // Error: Invalid count argument + // + // on terraform_plugin_test.tf line 15, in resource "terraform_data" "test_out": + // 15: count = startswith(framework_refinement_producer.test.string_with_prefix, "prefix://") ? 1 : 0 + // + // The "count" value depends on resource attributes that cannot be determined + // until apply, so Terraform cannot predict how many instances will be created. + // To work around this, use the -target argument to first apply only the + // resources that the count depends on. + // + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "terraform_data" "test_out" { + count = startswith(framework_refinement_producer.test.string_with_prefix, "prefix://") ? 1 : 0 + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("framework_refinement_producer.test", tfjsonpath.New("string_with_prefix")), + plancheck.ExpectResourceAction("terraform_data.test_out[0]", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_refinement_producer.test", tfjsonpath.New("string_with_prefix"), knownvalue.StringExact("prefix://hello-world")), + }, + }, + }, + }) +} + +func TestRefinementProducerResource_int64_bounds(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + // Without refinement support in the provider, this config would return an error like: + // + // Error: Invalid count argument + // + // on terraform_plugin_test.tf line 15, in resource "terraform_data" "test_out": + // 15: count = framework_refinement_producer.test.int64_with_bounds > 9 ? 1 : 0 + // + // The "count" value depends on resource attributes that cannot be determined + // until apply, so Terraform cannot predict how many instances will be created. + // To work around this, use the -target argument to first apply only the + // resources that the count depends on. + // + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "terraform_data" "test_out" { + count = framework_refinement_producer.test.int64_with_bounds > 9 ? 1 : 0 + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("framework_refinement_producer.test", tfjsonpath.New("int64_with_bounds")), + plancheck.ExpectResourceAction("terraform_data.test_out[0]", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_refinement_producer.test", tfjsonpath.New("int64_with_bounds"), knownvalue.Int64Exact(15)), + }, + }, + }, + }) +} +func TestRefinementProducerResource_float64_bounds(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + // Without refinement support in the provider, this config would return an error like: + // + // Error: Invalid count argument + // + // on terraform_plugin_test.tf line 15, in resource "terraform_data" "test_out": + // 15: count = framework_refinement_producer.test.float64_with_bounds > 10.233 ? 1 : 0 + // + // The "count" value depends on resource attributes that cannot be determined + // until apply, so Terraform cannot predict how many instances will be created. + // To work around this, use the -target argument to first apply only the + // resources that the count depends on. + // + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "terraform_data" "test_out" { + count = framework_refinement_producer.test.float64_with_bounds > 10.233 ? 1 : 0 + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("framework_refinement_producer.test", tfjsonpath.New("float64_with_bounds")), + plancheck.ExpectResourceAction("terraform_data.test_out[0]", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_refinement_producer.test", tfjsonpath.New("float64_with_bounds"), knownvalue.Float64Exact(12.102)), + }, + }, + }, + }) +} + +func TestRefinementProducerResource_list_length_bounds(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + // Unknown value refinements were introduced to Terraform v1.6.0 via go-cty + tfversion.SkipBelow(tfversion.Version1_6_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + // Without refinement support in the provider, this config would return an error like: + // + // Error: Invalid count argument + + // on terraform_plugin_test.tf line 15, in resource "terraform_data" "test_out": + // 15: count = length(framework_refinement_producer.test.list_with_length_bounds) > 1 ? 1 : 0 + + // The "count" value depends on resource attributes that cannot be determined + // until apply, so Terraform cannot predict how many instances will be created. + // To work around this, use the -target argument to first apply only the + // resources that the count depends on. + // + Config: ` + resource "framework_refinement_producer" "test" {} + + resource "terraform_data" "test_out" { + count = length(framework_refinement_producer.test.list_with_length_bounds) > 1 ? 1 : 0 + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectUnknownValue("framework_refinement_producer.test", tfjsonpath.New("list_with_length_bounds")), + plancheck.ExpectResourceAction("terraform_data.test_out[0]", plancheck.ResourceActionCreate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("framework_refinement_producer.test", tfjsonpath.New("list_with_length_bounds"), knownvalue.ListExact( + []knownvalue.Check{ + knownvalue.StringExact("hello"), + knownvalue.StringExact("there"), + knownvalue.StringExact("world!"), + }, + )), + }, + }, + }, + }) +}