diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3d3aa6aa..0d1a8c97 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,10 +59,6 @@ jobs: - windows-latest - ubuntu-latest terraform: - - '0.12.*' - - '0.13.*' - - '0.14.*' - - '0.15.*' - '1.0.*' - '1.1.*' - '1.2.*' diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ee87e5c..fb1e7900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## 4.0.0-rc.1 (unreleased) + +NOTES: + +* Provider has been re-written using the new [`terraform-plugin-framework`](https://www.terraform.io/plugin/framework) ([#177](https://github.com/hashicorp/terraform-provider-random/pull/177)). + +BREAKING CHANGES: + +* [Terraform `>=1.0`](https://www.terraform.io/language/upgrade-guides/1-0) is now required to use this provider. + +* resource/random_password: Deprecated attribute `number` has been removed ([266](https://github.com/hashicorp/terraform-provider-random/issues/266)). +* resource/random_string: Deprecated attribute `number` has been removed ([266](https://github.com/hashicorp/terraform-provider-random/issues/266)). + ## 3.3.2 (June 23, 2022) BUG FIXES: diff --git a/docs/resources/password.md b/docs/resources/password.md index 22bd8a16..1a214204 100644 --- a/docs/resources/password.md +++ b/docs/resources/password.md @@ -45,7 +45,6 @@ resource "aws_db_instance" "example" { - `min_numeric` (Number) Minimum number of numeric characters in the result. Default value is `0`. - `min_special` (Number) Minimum number of special characters in the result. Default value is `0`. - `min_upper` (Number) Minimum number of uppercase alphabet characters in the result. Default value is `0`. -- `number` (Boolean, Deprecated) Include numeric characters in the result. Default value is `true`. **NOTE**: This is deprecated, use `numeric` instead. - `numeric` (Boolean) Include numeric characters in the result. Default value is `true`. - `override_special` (String) Supply your own list of special characters to use for string generation. This overrides the default character list in the special argument. The `special` argument must still be set to true for any overwritten characters to be used in generation. - `special` (Boolean) Include special characters in the result. These are `!@#$%&*()-_=+[]{}<>:?`. Default value is `true`. diff --git a/docs/resources/pet.md b/docs/resources/pet.md index 536681a5..a610ba83 100644 --- a/docs/resources/pet.md +++ b/docs/resources/pet.md @@ -52,6 +52,6 @@ resource "aws_instance" "server" { ### Read-Only -- `id` (String) The random pet name +- `id` (String) The random pet name. diff --git a/docs/resources/string.md b/docs/resources/string.md index 2ad2427f..e6ba9c37 100644 --- a/docs/resources/string.md +++ b/docs/resources/string.md @@ -40,7 +40,6 @@ resource "random_string" "random" { - `min_numeric` (Number) Minimum number of numeric characters in the result. Default value is `0`. - `min_special` (Number) Minimum number of special characters in the result. Default value is `0`. - `min_upper` (Number) Minimum number of uppercase alphabet characters in the result. Default value is `0`. -- `number` (Boolean, Deprecated) Include numeric characters in the result. Default value is `true`. **NOTE**: This is deprecated, use `numeric` instead. - `numeric` (Boolean) Include numeric characters in the result. Default value is `true`. - `override_special` (String) Supply your own list of special characters to use for string generation. This overrides the default character list in the special argument. The `special` argument must still be set to true for any overwritten characters to be used in generation. - `special` (Boolean) Include special characters in the result. These are `!@#$%&*()-_=+[]{}<>:?`. Default value is `true`. diff --git a/examples/resources/random_string/import.sh b/examples/resources/random_string/import.sh index cf09ad6c..da101043 100644 --- a/examples/resources/random_string/import.sh +++ b/examples/resources/random_string/import.sh @@ -1,2 +1,2 @@ -# Random String can be imported by specifying the value of the string. +# Random String can be imported by specifying the value of the string. terraform import random_string.test test \ No newline at end of file diff --git a/go.mod b/go.mod index 3474e46b..f36670a7 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,10 @@ require ( github.com/google/go-cmp v0.5.8 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/terraform-plugin-docs v0.12.0 + github.com/hashicorp/terraform-plugin-framework v0.9.0 + github.com/hashicorp/terraform-plugin-framework-validators v0.2.0 + github.com/hashicorp/terraform-plugin-go v0.9.1 + github.com/hashicorp/terraform-plugin-log v0.4.1 github.com/hashicorp/terraform-plugin-sdk/v2 v2.17.0 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d ) @@ -27,7 +31,7 @@ require ( github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect - github.com/hashicorp/go-hclog v1.2.0 // indirect + github.com/hashicorp/go-hclog v1.2.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.4.4 // indirect github.com/hashicorp/go-version v1.6.0 // indirect @@ -36,11 +40,9 @@ require ( github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.17.1 // indirect github.com/hashicorp/terraform-json v0.14.0 // indirect - github.com/hashicorp/terraform-plugin-go v0.9.1 // indirect - github.com/hashicorp/terraform-plugin-log v0.4.0 // indirect - github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896 // indirect + github.com/hashicorp/terraform-registry-address v0.0.0-20220422185603-6772e136ec01 // indirect github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 // indirect - github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect + github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.13 // indirect github.com/mattn/go-colorable v0.1.12 // indirect @@ -51,20 +53,20 @@ require ( github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/oklog/run v1.0.0 // indirect + github.com/oklog/run v1.1.0 // indirect github.com/posener/complete v1.2.3 // indirect github.com/russross/blackfriday v1.6.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect - github.com/vmihailenco/tagparser v0.1.1 // indirect + github.com/vmihailenco/tagparser v0.1.2 // indirect github.com/zclconf/go-cty v1.10.0 // indirect - golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect + golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b // indirect golang.org/x/text v0.3.7 // indirect - google.golang.org/appengine v1.6.6 // indirect - google.golang.org/genproto v0.0.0-20200711021454-869866162049 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3 // indirect google.golang.org/grpc v1.46.0 // indirect google.golang.org/protobuf v1.28.0 // indirect ) diff --git a/go.sum b/go.sum index abe4c0db..748fc8d1 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= @@ -57,6 +58,7 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -123,11 +125,13 @@ github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/S github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw= +github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-plugin v1.4.4 h1:NVdrSdFRt3SkZtNckJ6tog7gbpRrcbOjQi/rgF7JYWQ= github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -155,19 +159,29 @@ github.com/hashicorp/terraform-json v0.14.0 h1:sh9iZ1Y8IFJLx+xQiKHGud6/TSUCM0N8e github.com/hashicorp/terraform-json v0.14.0/go.mod h1:5A9HIWPkk4e5aeeXIBbkcOvaZbIYnAIkEyqP2pNSckM= github.com/hashicorp/terraform-plugin-docs v0.12.0 h1:EAvFVEoV/wj15t/VSeKVpnAd+BBnIxzYepAnScBWrU4= github.com/hashicorp/terraform-plugin-docs v0.12.0/go.mod h1:HVn60yjtl4XxLINPgNmPCwX8SQ4T99Ut9CTD/ac6i5w= +github.com/hashicorp/terraform-plugin-framework v0.8.0/go.mod h1:jUhqrbeI48gAleP8LXzg9jtRH07EAcpwEGQlYmKNIVg= +github.com/hashicorp/terraform-plugin-framework v0.9.0 h1:vOKG9+keJv062zGhXFgfOFEuGcfgV6LHciwleFTSek0= +github.com/hashicorp/terraform-plugin-framework v0.9.0/go.mod h1:ActelD2V6yt2m0MwIX4jESGDYJ573rAvZswGjSGm1rY= +github.com/hashicorp/terraform-plugin-framework-validators v0.2.0 h1:+bmU82GBrmImRfP7jq9g9hHKWrsvWc8xOFJiojPxiEY= +github.com/hashicorp/terraform-plugin-framework-validators v0.2.0/go.mod h1:1h4uI89dby5g+/esMKVVys5Xju4NpnSYisGcLZxoFeU= +github.com/hashicorp/terraform-plugin-go v0.9.0/go.mod h1:EawBkgjBWNf7jiKnVoyDyF39OSV+u6KUX+Y73EPj3oM= github.com/hashicorp/terraform-plugin-go v0.9.1 h1:vXdHaQ6aqL+OF076nMSBV+JKPdmXlzG5mzVDD04WyPs= github.com/hashicorp/terraform-plugin-go v0.9.1/go.mod h1:ItjVSlQs70otlzcCwlPcU8FRXLdO973oYFRZwAOxy8M= -github.com/hashicorp/terraform-plugin-log v0.4.0 h1:F3eVnm8r2EfQCe2k9blPIiF/r2TT01SHijXnS7bujvc= +github.com/hashicorp/terraform-plugin-log v0.3.0/go.mod h1:EjueSP/HjlyFAsDqt+okpCPjkT4NDynAe32AeDC4vps= github.com/hashicorp/terraform-plugin-log v0.4.0/go.mod h1:9KclxdunFownr4pIm1jdmwKRmE4d6HVG2c9XDq47rpg= +github.com/hashicorp/terraform-plugin-log v0.4.1 h1:xpbmVhvuU3mgHzLetOmx9pkOL2rmgpu302XxddON6eo= +github.com/hashicorp/terraform-plugin-log v0.4.1/go.mod h1:p4R1jWBXRTvL4odmEkFfDdhUjHf9zcs/BCoNHAc7IK4= github.com/hashicorp/terraform-plugin-sdk/v2 v2.17.0 h1:Qr5fWNg1SPSfCRMtou67Y6Kcy9UnMYRNlIJTKRuUvXU= github.com/hashicorp/terraform-plugin-sdk/v2 v2.17.0/go.mod h1:b+LFg8WpYgFgvEBP/6Htk5H9/pJp1V1E8NJAekfH2Ws= -github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896 h1:1FGtlkJw87UsTMg5s8jrekrHmUPUJaMcu6ELiVhQrNw= github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896/go.mod h1:bzBPnUIkI0RxauU8Dqo+2KrZZ28Cf48s8V6IHt3p4co= +github.com/hashicorp/terraform-registry-address v0.0.0-20220422185603-6772e136ec01 h1:HgJRxDmThXP6Jdjv2pHufKfESG23Y3x8VtXgmtMDsq8= +github.com/hashicorp/terraform-registry-address v0.0.0-20220422185603-6772e136ec01/go.mod h1:bdLC+qQlJIBHKbCMA6GipcuaKjmjcvZlnVdpU583z3Y= github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 h1:HKLsbzeOsfXmKNpr3GiT18XAblV0BjCbzL8KQAMZGa0= github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 h1:xixZ2bWeofWV68J+x6AzmKuVM/JWCQwkWm6GW/MUR6I= +github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= @@ -227,8 +241,9 @@ github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758= github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4v2r9jz+c9i1IfIttS/OkmLfrk1jne5hs= -github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -262,15 +277,17 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvCazn8G65U= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= -github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= +github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -317,8 +334,10 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -345,14 +364,19 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8= golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -374,15 +398,17 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200711021454-869866162049 h1:YFTFpQhgvrLrmxtiIncJxFXeCyq84ixuKWVCaCAi9Oc= google.golang.org/genproto v0.0.0-20200711021454-869866162049/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3 h1:q1kiSVscqoDeqTF27eQ2NnLLDmqF0I373qQNXYMy0fo= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= @@ -391,6 +417,7 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0/go.mod h1:DNq5QpG7LJqD2AamLZ7zvKE0DEpVl2BSEVjFycAAjRY= @@ -423,7 +450,8 @@ gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/diagnostics/diagnostics.go b/internal/diagnostics/diagnostics.go new file mode 100644 index 00000000..2930a009 --- /dev/null +++ b/internal/diagnostics/diagnostics.go @@ -0,0 +1,48 @@ +package diagnostics + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +const RetryMsg = "Retry the Terraform operation. If the error still occurs or happens regularly, please contact the provider developer with hardware and operating system information.\n\n" + +func RandomReadError(errMsg string) diag.Diagnostics { + var diags diag.Diagnostics + + diags.AddError( + "Random Read Error", + "While attempting to generate a random value for this resource, a read error was generated.\n\n"+ + RetryMsg+ + fmt.Sprintf("Original Error: %s", errMsg), + ) + + return diags +} + +func HashGenerationError(errMsg string) diag.Diagnostics { + var diags diag.Diagnostics + + diags.AddError( + "Hash Generation Error", + "While attempting to generate a hash from the password an error occurred.\n\n"+ + "Verify that the state contains a populated 'result' field, using 'terraform state show', and retry the operation\n\n"+ + fmt.Sprintf("Original Error: %s", errMsg), + ) + + return diags +} + +func RandomnessGenerationError(errMsg string) diag.Diagnostics { + var diags diag.Diagnostics + + diags.AddError( + "Randomness Generation Error", + "While attempting to generate a random value for this resource, an insufficient number of random bytes were generated.\n\n"+ + RetryMsg+ + fmt.Sprintf("Original Error: %s", errMsg), + ) + + return diags +} diff --git a/internal/planmodifiers/attribute.go b/internal/planmodifiers/attribute.go new file mode 100644 index 00000000..8ba10059 --- /dev/null +++ b/internal/planmodifiers/attribute.go @@ -0,0 +1,86 @@ +package planmodifiers + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// DefaultValue accepts an attr.Value and uses the supplied value to set a default if the config for +// the attribute is null. +func DefaultValue(val attr.Value) tfsdk.AttributePlanModifier { + return &defaultValueAttributePlanModifier{val} +} + +type defaultValueAttributePlanModifier struct { + val attr.Value +} + +func (d *defaultValueAttributePlanModifier) Description(ctx context.Context) string { + return "If the config does not contain a value, a default will be set using val." +} + +func (d *defaultValueAttributePlanModifier) MarkdownDescription(ctx context.Context) string { + return d.Description(ctx) +} + +// Modify checks that the value of the attribute in the configuration and assigns the default value if +// the value in the config is null. This is a destructive operation in that it will overwrite any value +// present in the plan. +func (d *defaultValueAttributePlanModifier) Modify(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) { + // Do not set default if the attribute configuration has been set. + if !req.AttributeConfig.IsNull() { + return + } + + resp.AttributePlan = d.val +} + +// RequiresReplace returns an attribute plan modifier that is identical to tfsdk.RequiresReplace() with +// the exception that there is no check for `configRaw.IsNull && attrSchema.Computed` as a replacement +// needs to be triggered when the attribute has been removed from the config. +func RequiresReplace() tfsdk.AttributePlanModifier { + return RequiresReplaceModifier{} +} + +type RequiresReplaceModifier struct{} + +// Description returns a human-readable description of the plan modifier. +func (r RequiresReplaceModifier) Description(ctx context.Context) string { + return "If the value of this attribute changes, Terraform will destroy and recreate the resource." +} + +// MarkdownDescription returns a markdown description of the plan modifier. +func (r RequiresReplaceModifier) MarkdownDescription(ctx context.Context) string { + return r.Description(ctx) +} + +// Modify will trigger replacement (i.e., destroy-create) when `configRaw.IsNull && attrSchema.Computed`, +// which differs from the behaviour of `tfsdk.RequiresReplace()`. +func (r RequiresReplaceModifier) Modify(ctx context.Context, req tfsdk.ModifyAttributePlanRequest, resp *tfsdk.ModifyAttributePlanResponse) { + if req.AttributeConfig == nil || req.AttributePlan == nil || req.AttributeState == nil { + // shouldn't happen, but let's not panic if it does + return + } + + if req.State.Raw.IsNull() { + // if we're creating the resource, no need to delete and + // recreate it + return + } + + if req.Plan.Raw.IsNull() { + // if we're deleting the resource, no need to delete and + // recreate it + return + } + + if req.AttributePlan.Equal(req.AttributeState) { + // if the plan and the state are in agreement, this attribute + // isn't changing, don't require replace + return + } + + resp.RequiresReplace = true +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 1fc2f1a8..d0af56b1 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -2,32 +2,38 @@ package provider import ( "context" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) -func init() { - schema.DescriptionKind = schema.StringMarkdown +func New() tfsdk.Provider { + return &provider{} +} + +var _ tfsdk.Provider = (*provider)(nil) + +type provider struct{} + +func (p *provider) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{}, nil +} + +func (p *provider) Configure(context.Context, tfsdk.ConfigureProviderRequest, *tfsdk.ConfigureProviderResponse) { } -// New returns a *schema.Provider. -func New() *schema.Provider { - return &schema.Provider{ - Schema: map[string]*schema.Schema{}, - - ResourcesMap: map[string]*schema.Resource{ - "random_id": resourceId(), - "random_shuffle": resourceShuffle(), - "random_pet": resourcePet(), - "random_string": resourceString(), - "random_password": resourcePassword(), - "random_integer": resourceInteger(), - "random_uuid": resourceUuid(), - }, - } +func (p *provider) GetResources(context.Context) (map[string]tfsdk.ResourceType, diag.Diagnostics) { + return map[string]tfsdk.ResourceType{ + "random_id": &idResourceType{}, + "random_integer": &integerResourceType{}, + "random_password": &passwordResourceType{}, + "random_pet": &petResourceType{}, + "random_shuffle": &shuffleResourceType{}, + "random_string": &stringResourceType{}, + "random_uuid": &uuidResourceType{}, + }, nil } -func RemoveResourceFromState(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - d.SetId("") - return nil +func (p *provider) GetDataSources(context.Context) (map[string]tfsdk.DataSourceType, diag.Diagnostics) { + return map[string]tfsdk.DataSourceType{}, nil } diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index bde42d24..020fa511 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -1,30 +1,23 @@ package provider import ( - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -var testAccProvider *schema.Provider -var testAccProviders map[string]func() (*schema.Provider, error) - -func init() { - testAccProvider = New() - testAccProviders = map[string]func() (*schema.Provider, error){ - "random": func() (*schema.Provider, error) { return testAccProvider, nil }, +//nolint:unparam +func protoV6ProviderFactories() map[string]func() (tfprotov6.ProviderServer, error) { + return map[string]func() (tfprotov6.ProviderServer, error){ + "random": providerserver.NewProtocol6WithError(New()), } } -func TestProvider(t *testing.T) { - if err := New().InternalValidate(); err != nil { - t.Fatalf("err: %s", err) +func providerVersion332() map[string]resource.ExternalProvider { + return map[string]resource.ExternalProvider{ + "tls": { + VersionConstraint: "3.3.2", + Source: "hashicorp/random", + }, } } - -func TestProvider_impl(t *testing.T) { - var _ *schema.Provider = New() -} - -func testAccPreCheck(t *testing.T) { -} diff --git a/internal/provider/resource_id.go b/internal/provider/resource_id.go index 936ef168..4492c6cc 100644 --- a/internal/provider/resource_id.go +++ b/internal/provider/resource_id.go @@ -9,12 +9,19 @@ import ( "math/big" "strings" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/terraform-providers/terraform-provider-random/internal/diagnostics" ) -func resourceId() *schema.Resource { - return &schema.Resource{ +var _ tfsdk.ResourceType = (*idResourceType)(nil) + +type idResourceType struct{} + +func (r *idResourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{ Description: ` The resource ` + "`random_id`" + ` generates random numbers that are intended to be used as unique identifiers for other resources. @@ -29,152 +36,199 @@ the ` + "`create_before_destroy`" + ` lifecycle flag set to avoid conflicts with unique names during the brief period where both the old and new resources exist concurrently. `, - CreateContext: CreateID, - ReadContext: RepopulateEncodings, - DeleteContext: RemoveResourceFromState, - Importer: &schema.ResourceImporter{ - StateContext: ImportID, - }, - - Schema: map[string]*schema.Schema{ + Attributes: map[string]tfsdk.Attribute{ "keepers": { Description: "Arbitrary map of values that, when changed, will trigger recreation of " + "resource. See [the main provider documentation](../index.html) for more information.", - Type: schema.TypeMap, + Type: types.MapType{ + ElemType: types.StringType, + }, Optional: true, - ForceNew: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, }, - "byte_length": { Description: "The number of random bytes to produce. The minimum value is 1, which produces " + "eight bits of randomness.", - Type: schema.TypeInt, + Type: types.Int64Type, Required: true, - ForceNew: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, }, - "prefix": { Description: "Arbitrary string to prefix the output value with. This string is supplied as-is, " + "meaning it is not guaranteed to be URL-safe or base64 encoded.", - Type: schema.TypeString, + Type: types.StringType, Optional: true, - ForceNew: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, }, - "b64_url": { Description: "The generated id presented in base64, using the URL-friendly character set: " + "case-sensitive letters, digits and the characters `_` and `-`.", - Type: schema.TypeString, + Type: types.StringType, Computed: true, }, - "b64_std": { Description: "The generated id presented in base64 without additional transformations.", - Type: schema.TypeString, + Type: types.StringType, Computed: true, }, - "hex": { Description: "The generated id presented in padded hexadecimal digits. This result will " + "always be twice as long as the requested byte length.", - Type: schema.TypeString, + Type: types.StringType, Computed: true, }, - "dec": { Description: "The generated id presented in non-padded decimal digits.", - Type: schema.TypeString, + Type: types.StringType, Computed: true, }, - "id": { Description: "The generated id presented in base64 without additional transformations or prefix.", - Type: schema.TypeString, + Type: types.StringType, Computed: true, }, }, - } + }, nil } -func CreateID(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - var diags diag.Diagnostics - byteLength := d.Get("byte_length").(int) - bytes := make([]byte, byteLength) +func (r *idResourceType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return &idResource{}, nil +} - n, err := rand.Reader.Read(bytes) - if n != byteLength { - return append(diags, diag.Errorf("generated insufficient random bytes: %s", err)...) - } - if err != nil { - return append(diags, diag.Errorf("error generating random bytes: %s", err)...) - } +var ( + _ tfsdk.Resource = (*idResource)(nil) + _ tfsdk.ResourceWithImportState = (*idResource)(nil) +) - b64Str := base64.RawURLEncoding.EncodeToString(bytes) - d.SetId(b64Str) +type idResource struct{} - repopEncsDiags := RepopulateEncodings(ctx, d, meta) - if repopEncsDiags != nil { - return append(diags, repopEncsDiags...) - } +func (r *idResource) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { + var plan idModelV0 - return diags -} + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } -func RepopulateEncodings(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - var diags diag.Diagnostics - prefix := d.Get("prefix").(string) - base64Str := d.Id() + byteLength := plan.ByteLength.Value + bytes := make([]byte, byteLength) - bytes, err := base64.RawURLEncoding.DecodeString(base64Str) + n, err := rand.Reader.Read(bytes) + if int64(n) != byteLength { + resp.Diagnostics.Append(diagnostics.RandomnessGenerationError(err.Error())...) + return + } if err != nil { - return append(diags, diag.Errorf("error decoding ID: %s", err)...) + resp.Diagnostics.Append(diagnostics.RandomReadError(err.Error())...) + return } - b64StdStr := base64.StdEncoding.EncodeToString(bytes) + id := base64.RawURLEncoding.EncodeToString(bytes) + prefix := plan.Prefix.Value + b64Std := base64.StdEncoding.EncodeToString(bytes) hexStr := hex.EncodeToString(bytes) bigInt := big.Int{} bigInt.SetBytes(bytes) - decStr := bigInt.String() - - if err := d.Set("b64_url", prefix+base64Str); err != nil { - return append(diags, diag.Errorf("error setting b64_url: %s", err)...) + dec := bigInt.String() + + i := idModelV0{ + ID: types.String{Value: id}, + Keepers: plan.Keepers, + ByteLength: types.Int64{Value: plan.ByteLength.Value}, + Prefix: plan.Prefix, + B64URL: types.String{Value: prefix + id}, + B64Std: types.String{Value: prefix + b64Std}, + Hex: types.String{Value: prefix + hexStr}, + Dec: types.String{Value: prefix + dec}, } - if err := d.Set("b64_std", prefix+b64StdStr); err != nil { - return append(diags, diag.Errorf("error setting b64_std: %s", err)...) - } - if err := d.Set("hex", prefix+hexStr); err != nil { - return append(diags, diag.Errorf("error setting hex: %s", err)...) - } - if err := d.Set("dec", prefix+decStr); err != nil { - return append(diags, diag.Errorf("error setting dec: %s", err)...) + + diags = resp.State.Set(ctx, i) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } +} - return nil +// Read does not need to perform any operations as the state in ReadResourceResponse is already populated. +func (r *idResource) Read(context.Context, tfsdk.ReadResourceRequest, *tfsdk.ReadResourceResponse) { } -func ImportID(_ context.Context, d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) { - id := d.Id() +// Update is intentionally left blank as all required and optional attributes force replacement of the resource +// through the RequiresReplace AttributePlanModifier. +func (r *idResource) Update(context.Context, tfsdk.UpdateResourceRequest, *tfsdk.UpdateResourceResponse) { +} + +// Delete does not need to explicitly call resp.State.RemoveResource() as this is automatically handled by the +// [framework](https://github.com/hashicorp/terraform-plugin-framework/pull/301). +func (r *idResource) Delete(context.Context, tfsdk.DeleteResourceRequest, *tfsdk.DeleteResourceResponse) { +} + +func (r *idResource) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { + id := req.ID + var prefix string sep := strings.LastIndex(id, ",") if sep != -1 { - if err := d.Set("prefix", id[:sep]); err != nil { - return nil, fmt.Errorf("error setting prefix: %w", err) - } - + prefix = id[:sep] id = id[sep+1:] } bytes, err := base64.RawURLEncoding.DecodeString(id) if err != nil { - return nil, fmt.Errorf("error decoding ID: %w", err) + resp.Diagnostics.AddError( + "Import Random ID Error", + "While attempting to import a random id there was a decoding error.\n\n+"+ + diagnostics.RetryMsg+ + fmt.Sprintf("Original Error: %s", err), + ) + return } - if err := d.Set("byte_length", len(bytes)); err != nil { - return nil, fmt.Errorf("error setting byte_length: %w", err) + b64Std := base64.StdEncoding.EncodeToString(bytes) + hexStr := hex.EncodeToString(bytes) + + bigInt := big.Int{} + bigInt.SetBytes(bytes) + dec := bigInt.String() + + var state idModelV0 + + state.ID.Value = id + state.ByteLength.Value = int64(len(bytes)) + state.Keepers.ElemType = types.StringType + state.B64Std.Value = prefix + b64Std + state.B64URL.Value = prefix + id + state.Hex.Value = prefix + hexStr + state.Dec.Value = prefix + dec + + if prefix == "" { + state.Prefix.Null = true + } else { + state.Prefix.Value = prefix } - d.SetId(id) + diags := resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} - return []*schema.ResourceData{d}, nil +type idModelV0 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + ByteLength types.Int64 `tfsdk:"byte_length"` + Prefix types.String `tfsdk:"prefix"` + B64URL types.String `tfsdk:"b64_url"` + B64Std types.String `tfsdk:"b64_std"` + Hex types.String `tfsdk:"hex"` + Dec types.String `tfsdk:"dec"` } diff --git a/internal/provider/resource_id_test.go b/internal/provider/resource_id_test.go index d1d03afd..be04a020 100644 --- a/internal/provider/resource_id_test.go +++ b/internal/provider/resource_id_test.go @@ -1,32 +1,24 @@ package provider import ( - "fmt" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -type idLens struct { - b64UrlLen int - b64StdLen int - hexLen int -} - func TestAccResourceID(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testAccResourceIDConfig, + Config: `resource "random_id" "foo" { + byte_length = 4 + }`, Check: resource.ComposeTestCheckFunc( - testAccResourceIDCheck("random_id.foo", &idLens{ - b64UrlLen: 6, - b64StdLen: 8, - hexLen: 8, - }), + resource.TestCheckResourceAttrWith("random_id.foo", "b64_url", testCheckLen(6)), + resource.TestCheckResourceAttrWith("random_id.foo", "b64_std", testCheckLen(8)), + resource.TestCheckResourceAttrWith("random_id.foo", "hex", testCheckLen(8)), + resource.TestCheckResourceAttrWith("random_id.foo", "dec", testCheckMinLen(1)), ), }, { @@ -38,19 +30,20 @@ func TestAccResourceID(t *testing.T) { }) } -func TestAccResourceID_importWithPrefix(t *testing.T) { +func TestAccResourceID_ImportWithPrefix(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testAccResourceIDConfigWithPrefix, + Config: `resource "random_id" "bar" { + byte_length = 4 + prefix = "cloud-" + }`, Check: resource.ComposeTestCheckFunc( - testAccResourceIDCheck("random_id.bar", &idLens{ - b64UrlLen: 12, - b64StdLen: 14, - hexLen: 14, - }), + resource.TestCheckResourceAttrWith("random_id.bar", "b64_url", testCheckLen(12)), + resource.TestCheckResourceAttrWith("random_id.bar", "b64_std", testCheckLen(14)), + resource.TestCheckResourceAttrWith("random_id.bar", "hex", testCheckLen(14)), + resource.TestCheckResourceAttrWith("random_id.bar", "dec", testCheckMinLen(1)), ), }, { @@ -63,48 +56,43 @@ func TestAccResourceID_importWithPrefix(t *testing.T) { }) } -func testAccResourceIDCheck(id string, want *idLens) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[id] - if !ok { - return fmt.Errorf("Not found: %s", id) - } - if rs.Primary.ID == "" { - return fmt.Errorf("No ID is set") - } - - b64UrlStr := rs.Primary.Attributes["b64_url"] - b64StdStr := rs.Primary.Attributes["b64_std"] - hexStr := rs.Primary.Attributes["hex"] - decStr := rs.Primary.Attributes["dec"] - - if got, want := len(b64UrlStr), want.b64UrlLen; got != want { - return fmt.Errorf("base64 URL string length is %d; want %d", got, want) - } - if got, want := len(b64StdStr), want.b64StdLen; got != want { - return fmt.Errorf("base64 STD string length is %d; want %d", got, want) - } - if got, want := len(hexStr), want.hexLen; got != want { - return fmt.Errorf("hex string length is %d; want %d", got, want) - } - if len(decStr) < 1 { - return fmt.Errorf("decimal string is empty; want at least one digit") - } - - return nil - } -} - -const ( - testAccResourceIDConfig = ` -resource "random_id" "foo" { - byte_length = 4 -}` - - testAccResourceIDConfigWithPrefix = ` -resource "random_id" "bar" { - byte_length = 4 - prefix = "cloud-" +func TestAccResourceID_UpgradeFromVersion3_3_2(t *testing.T) { + resource.Test(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ExternalProviders: providerVersion332(), + Config: `resource "random_id" "bar" { + byte_length = 4 + prefix = "cloud-" + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrWith("random_id.bar", "b64_url", testCheckLen(12)), + resource.TestCheckResourceAttrWith("random_id.bar", "b64_std", testCheckLen(14)), + resource.TestCheckResourceAttrWith("random_id.bar", "hex", testCheckLen(14)), + resource.TestCheckResourceAttrWith("random_id.bar", "dec", testCheckMinLen(1)), + ), + }, + { + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Config: `resource "random_id" "bar" { + byte_length = 4 + prefix = "cloud-" + }`, + PlanOnly: true, + }, + { + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Config: `resource "random_id" "bar" { + byte_length = 4 + prefix = "cloud-" + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrWith("random_id.bar", "b64_url", testCheckLen(12)), + resource.TestCheckResourceAttrWith("random_id.bar", "b64_std", testCheckLen(14)), + resource.TestCheckResourceAttrWith("random_id.bar", "hex", testCheckLen(14)), + resource.TestCheckResourceAttrWith("random_id.bar", "dec", testCheckMinLen(1)), + ), + }, + }, + }) } -` -) diff --git a/internal/provider/resource_integer.go b/internal/provider/resource_integer.go index be66f2dc..cf145280 100644 --- a/internal/provider/resource_integer.go +++ b/internal/provider/resource_integer.go @@ -6,135 +6,201 @@ import ( "strconv" "strings" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/terraform-providers/terraform-provider-random/internal/random" ) -func resourceInteger() *schema.Resource { - return &schema.Resource{ +var _ tfsdk.ResourceType = (*integerResourceType)(nil) + +type integerResourceType struct{} + +func (r *integerResourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{ Description: "The resource `random_integer` generates random values from a given range, described " + "by the `min` and `max` attributes of a given resource.\n" + "\n" + "This resource can be used in conjunction with resources that have the `create_before_destroy` " + "lifecycle flag set, to avoid conflicts with unique names during the brief period where both the " + "old and new resources exist concurrently.", - CreateContext: CreateInteger, - ReadContext: schema.NoopContext, - DeleteContext: RemoveResourceFromState, - Importer: &schema.ResourceImporter{ - StateContext: ImportInteger, - }, - - Schema: map[string]*schema.Schema{ + Attributes: map[string]tfsdk.Attribute{ "keepers": { Description: "Arbitrary map of values that, when changed, will trigger recreation of " + "resource. See [the main provider documentation](../index.html) for more information.", - Type: schema.TypeMap, - Optional: true, - ForceNew: true, + Type: types.MapType{ + ElemType: types.StringType, + }, + Optional: true, + PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, }, - "min": { - Description: "The minimum inclusive value of the range.", - Type: schema.TypeInt, - Required: true, - ForceNew: true, + Description: "The minimum inclusive value of the range.", + Type: types.Int64Type, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, }, - "max": { - Description: "The maximum inclusive value of the range.", - Type: schema.TypeInt, - Required: true, - ForceNew: true, + Description: "The maximum inclusive value of the range.", + Type: types.Int64Type, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, }, - "seed": { - Description: "A custom seed to always produce the same value.", - Type: schema.TypeString, - Optional: true, - ForceNew: true, + Description: "A custom seed to always produce the same value.", + Type: types.StringType, + Optional: true, + PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, }, - "result": { Description: "The random integer result.", - Type: schema.TypeInt, + Type: types.Int64Type, Computed: true, }, - "id": { Description: "The string representation of the integer result.", - Type: schema.TypeString, + Type: types.StringType, Computed: true, }, }, - UseJSONNumber: true, - } + }, nil } -func CreateInteger(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - var diags diag.Diagnostics - min := d.Get("min").(int) - max := d.Get("max").(int) - seed := d.Get("seed").(string) +func (r *integerResourceType) NewResource(_ context.Context, _ tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return &integerResource{}, nil +} + +var ( + _ tfsdk.Resource = (*integerResource)(nil) + _ tfsdk.ResourceWithImportState = (*integerResource)(nil) +) + +type integerResource struct{} + +func (r *integerResource) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { + var plan integerModelV0 + + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + max := int(plan.Max.Value) + min := int(plan.Min.Value) + seed := plan.Seed.Value if max < min { - return append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "minimum value needs to be smaller than or equal to maximum value", - }) + resp.Diagnostics.AddError( + "Create Random Integer Error", + "The minimum (min) value needs to be smaller than or equal to maximum (max) value.", + ) + return } - rand := NewRand(seed) + + rand := random.NewRand(seed) number := rand.Intn((max+1)-min) + min - if err := d.Set("result", number); err != nil { - return diag.Errorf("error setting result: %s", err) + u := &integerModelV0{ + ID: types.String{Value: strconv.Itoa(number)}, + Keepers: plan.Keepers, + Min: types.Int64{Value: int64(min)}, + Max: types.Int64{Value: int64(max)}, + Result: types.Int64{Value: int64(number)}, } - d.SetId(strconv.Itoa(number)) + if seed != "" { + u.Seed.Value = seed + } else { + u.Seed.Null = true + } - return nil + diags = resp.State.Set(ctx, u) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } } -func ImportInteger(_ context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - parts := strings.Split(d.Id(), ",") - if len(parts) != 3 && len(parts) != 4 { - return nil, fmt.Errorf("Invalid import usage: expecting {result},{min},{max} or {result},{min},{max},{seed}") - } +// Read does not need to perform any operations as the state in ReadResourceResponse is already populated. +func (r *integerResource) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { +} - result, err := strconv.Atoi(parts[0]) - if err != nil { - return nil, fmt.Errorf("error parsing result: %w", err) - } +// Update is intentionally left blank as all required and optional attributes force replacement of the resource +// through the RequiresReplace AttributePlanModifier. +func (r *integerResource) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { +} + +// Delete does not need to explicitly call resp.State.RemoveResource() as this is automatically handled by the +// [framework](https://github.com/hashicorp/terraform-plugin-framework/pull/301). +func (r *integerResource) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { +} - if err := d.Set("result", result); err != nil { - return nil, fmt.Errorf("error setting result: %w", err) +func (r *integerResource) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { + parts := strings.Split(req.ID, ",") + if len(parts) != 3 && len(parts) != 4 { + resp.Diagnostics.AddError( + "Import Random Integer Error", + "Invalid import usage: expecting {result},{min},{max} or {result},{min},{max},{seed}", + ) + return } - min, err := strconv.Atoi(parts[1]) + result, err := strconv.ParseInt(parts[0], 10, 64) if err != nil { - return nil, fmt.Errorf("error parsing min: %w", err) + resp.Diagnostics.AddError( + "Import Random Integer Error", + "The value supplied could not be parsed as an integer.\n\n"+ + fmt.Sprintf("Original Error: %s", err), + ) + return } - if err := d.Set("min", min); err != nil { - return nil, fmt.Errorf("error setting min: %w", err) + min, err := strconv.ParseInt(parts[1], 10, 64) + if err != nil { + resp.Diagnostics.AddError( + "Import Random Integer Error", + "The min value supplied could not be parsed as an integer.\n\n"+ + fmt.Sprintf("Original Error: %s", err), + ) + return } - max, err := strconv.Atoi(parts[2]) + max, err := strconv.ParseInt(parts[2], 10, 64) if err != nil { - return nil, fmt.Errorf("error parsing max: %w", err) + resp.Diagnostics.AddError( + "Import Random Integer Error", + "The max value supplied could not be parsed as an integer.\n\n"+ + fmt.Sprintf("Original Error: %s", err), + ) + return } - if err := d.Set("max", max); err != nil { - return nil, fmt.Errorf("error setting max: %w", err) - } + var state integerModelV0 + + state.ID.Value = parts[0] + state.Keepers.ElemType = types.StringType + state.Result.Value = result + state.Min.Value = min + state.Max.Value = max if len(parts) == 4 { - if err := d.Set("seed", parts[3]); err != nil { - return nil, fmt.Errorf("error setting seed: %w", err) - } + state.Seed.Value = parts[3] } - d.SetId(parts[0]) + diags := resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} - return []*schema.ResourceData{d}, nil +type integerModelV0 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + Min types.Int64 `tfsdk:"min"` + Max types.Int64 `tfsdk:"max"` + Seed types.String `tfsdk:"seed"` + Result types.Int64 `tfsdk:"result"` } diff --git a/internal/provider/resource_integer_test.go b/internal/provider/resource_integer_test.go index b9ad040a..e04edf46 100644 --- a/internal/provider/resource_integer_test.go +++ b/internal/provider/resource_integer_test.go @@ -5,19 +5,21 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -func TestAccResourceIntegerBasic(t *testing.T) { +func TestAccResourceInteger(t *testing.T) { t.Parallel() resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testRandomIntegerBasic, + Config: `resource "random_integer" "integer_1" { + min = 1 + max = 3 + seed = "12345" + }`, Check: resource.ComposeTestCheckFunc( - testAccResourceIntegerBasic("random_integer.integer_1"), + resource.TestCheckResourceAttr("random_integer.integer_1", "result", "3"), ), }, { @@ -30,80 +32,102 @@ func TestAccResourceIntegerBasic(t *testing.T) { }) } -func TestAccResourceIntegerUpdate(t *testing.T) { +func TestAccResourceInteger_ChangeSeed(t *testing.T) { t.Parallel() resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testRandomIntegerBasic, + Config: `resource "random_integer" "integer_1" { + min = 1 + max = 3 + seed = "12345" + }`, Check: resource.ComposeTestCheckFunc( - testAccResourceIntegerBasic("random_integer.integer_1"), + resource.TestCheckResourceAttr("random_integer.integer_1", "result", "3"), ), }, { - Config: testRandomIntegerUpdate, + Config: `resource "random_integer" "integer_1" { + min = 1 + max = 3 + seed = "123456" + }`, Check: resource.ComposeTestCheckFunc( - testAccResourceIntegerUpdate("random_integer.integer_1"), + resource.TestCheckResourceAttr("random_integer.integer_1", "result", "2"), ), }, }, }) } -func TestAccResourceIntegerSeedless_to_seeded(t *testing.T) { +func TestAccResourceInteger_SeedlessToSeeded(t *testing.T) { t.Parallel() resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testRandomIntegerSeedless, + Config: `resource "random_integer" "integer_1" { + min = 1 + max = 3 + }`, Check: resource.ComposeTestCheckFunc( - testAccResourceIntegerSeedless("random_integer.integer_1"), + resource.TestCheckResourceAttrWith("random_integer.integer_1", "result", testCheckNotEmptyString("result")), ), }, { - Config: testRandomIntegerUpdate, + Config: `resource "random_integer" "integer_1" { + min = 1 + max = 3 + seed = "123456" + }`, Check: resource.ComposeTestCheckFunc( - testAccResourceIntegerUpdate("random_integer.integer_1"), + resource.TestCheckResourceAttr("random_integer.integer_1", "result", "2"), ), }, }, }) } -func TestAccResourceIntegerSeeded_to_seedless(t *testing.T) { +func TestAccResourceInteger_SeededToSeedless(t *testing.T) { t.Parallel() resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testRandomIntegerBasic, + Config: `resource "random_integer" "integer_1" { + min = 1 + max = 3 + seed = "12345" + }`, Check: resource.ComposeTestCheckFunc( - testAccResourceIntegerBasic("random_integer.integer_1"), + resource.TestCheckResourceAttr("random_integer.integer_1", "result", "3"), ), }, { - Config: testRandomIntegerSeedless, + Config: `resource "random_integer" "integer_1" { + min = 1 + max = 3 + }`, Check: resource.ComposeTestCheckFunc( - testAccResourceIntegerSeedless("random_integer.integer_1"), + resource.TestCheckResourceAttrWith("random_integer.integer_1", "result", testCheckNotEmptyString("result")), ), }, }, }) } -func TestAccResourceIntegerBig(t *testing.T) { +func TestAccResourceInteger_Big(t *testing.T) { t.Parallel() resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testRandomIntegerBig, + Config: `resource "random_integer" "integer_1" { + max = 7227701560655103598 + min = 7227701560655103597 + seed = 12345 + }`, }, { ResourceName: "random_integer.integer_1", @@ -115,101 +139,50 @@ func TestAccResourceIntegerBig(t *testing.T) { }) } -func testAccResourceIntegerBasic(id string) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[id] - if !ok { - return fmt.Errorf("Not found: %s", id) - } - result := rs.Primary.Attributes["result"] - - if rs.Primary.ID == "" { - return fmt.Errorf("No ID is set") - } - - if result == "" { - return fmt.Errorf("Result not found") - } - - if result != "3" { - return fmt.Errorf("Invalid result %s. Seed does not result in correct value", result) - } - return nil - } -} - -func testAccResourceIntegerUpdate(id string) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[id] - if !ok { - return fmt.Errorf("Not found: %s", id) - } - result := rs.Primary.Attributes["result"] - - if rs.Primary.ID == "" { - return fmt.Errorf("No ID is set") - } - - if result == "" { - return fmt.Errorf("Result not found") - } - - if result != "2" { - return fmt.Errorf("Invalid result %s. Seed does not result in correct value", result) - } - return nil - } +func TestAccResourceInteger_UpgradeFromVersion3_3_2(t *testing.T) { + resource.Test(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ExternalProviders: providerVersion332(), + Config: `resource "random_integer" "integer_1" { + min = 1 + max = 3 + seed = "12345" + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("random_integer.integer_1", "result", "3"), + ), + }, + { + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Config: `resource "random_integer" "integer_1" { + min = 1 + max = 3 + seed = "12345" + }`, + PlanOnly: true, + }, + { + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Config: `resource "random_integer" "integer_1" { + min = 1 + max = 3 + seed = "12345" + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("random_integer.integer_1", "result", "3"), + ), + }, + }, + }) } -// testAccResourceIntegerSeedless only checks that some result was returned, and does not validate the value. -func testAccResourceIntegerSeedless(id string) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[id] - if !ok { - return fmt.Errorf("Not found: %s", id) - } - result := rs.Primary.Attributes["result"] - - if rs.Primary.ID == "" { - return fmt.Errorf("No ID is set") - } - - if result == "" { - return fmt.Errorf("Result not found") +func testCheckNotEmptyString(field string) func(input string) error { + return func(input string) error { + if input == "" { + return fmt.Errorf("%s is empty string", field) } return nil } } - -const ( - testRandomIntegerBasic = ` -resource "random_integer" "integer_1" { - min = 1 - max = 3 - seed = "12345" -} -` - - testRandomIntegerUpdate = ` -resource "random_integer" "integer_1" { - min = 1 - max = 3 - seed = "123456" -} -` - - testRandomIntegerSeedless = ` -resource "random_integer" "integer_1" { - min = 1 - max = 3 -} -` - - testRandomIntegerBig = ` -resource "random_integer" "integer_1" { - max = 7227701560655103598 - min = 7227701560655103597 - seed = 12345 -}` -) diff --git a/internal/provider/resource_password.go b/internal/provider/resource_password.go index 24fbc9b8..8780989b 100644 --- a/internal/provider/resource_password.go +++ b/internal/provider/resource_password.go @@ -2,144 +2,255 @@ package provider import ( "context" - "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" "golang.org/x/crypto/bcrypt" + + "github.com/terraform-providers/terraform-provider-random/internal/diagnostics" + "github.com/terraform-providers/terraform-provider-random/internal/planmodifiers" + "github.com/terraform-providers/terraform-provider-random/internal/random" + "github.com/terraform-providers/terraform-provider-random/internal/validators" ) -// resourcePassword and resourceString both use the same set of CustomizeDiffFunc(s) in order to handle the deprecation -// of the `number` attribute and the simultaneous addition of the `numeric` attribute. planDefaultIfAllNull handles -// ensuring that both `number` and `numeric` default to `true` when they are both absent from config. -// planSyncIfChange handles keeping number and numeric in-sync when either one has been changed. -func resourcePassword() *schema.Resource { - customizeDiffFuncs := planDefaultIfAllNull(true, "number", "numeric") - customizeDiffFuncs = append(customizeDiffFuncs, planSyncIfChange("number", "numeric")) - customizeDiffFuncs = append(customizeDiffFuncs, planSyncIfChange("numeric", "number")) +var _ tfsdk.ResourceType = (*passwordResourceType)(nil) - return &schema.Resource{ - Description: "Identical to [random_string](string.html) with the exception that the result is " + - "treated as sensitive and, thus, _not_ displayed in console output. Read more about sensitive " + - "data handling in the [Terraform documentation](https://www.terraform.io/docs/language/state/sensitive-data.html).\n" + - "\n" + - "This resource *does* use a cryptographic random number generator.", - CreateContext: createPassword, - ReadContext: readNil, - DeleteContext: RemoveResourceFromState, - Schema: passwordSchemaV2(), - Importer: &schema.ResourceImporter{ - StateContext: importPasswordFunc, - }, - SchemaVersion: 2, - StateUpgraders: []schema.StateUpgrader{ - { - Version: 0, - Type: resourcePasswordV0().CoreConfigSchema().ImpliedType(), - Upgrade: resourcePasswordStateUpgradeV0, - }, - { - Version: 1, - Type: resourcePasswordV1().CoreConfigSchema().ImpliedType(), - Upgrade: resourcePasswordStringStateUpgradeV1, - }, - }, - CustomizeDiff: customdiff.All( - customizeDiffFuncs..., - ), - } +type passwordResourceType struct{} + +func (r *passwordResourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { + return passwordSchemaV2(), nil } -func createPassword(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - diags := createStringFunc(true)(ctx, d, meta) - if diags.HasError() { - return diags - } +func (r *passwordResourceType) NewResource(_ context.Context, _ tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return &passwordResource{}, nil +} - hash, err := generateHash(d.Get("result").(string)) - if err != nil { - diags = append(diags, diag.Errorf("err: %s", err)...) - return diags +var ( + _ tfsdk.Resource = (*passwordResource)(nil) + _ tfsdk.ResourceWithImportState = (*passwordResource)(nil) + _ tfsdk.ResourceWithUpgradeState = (*passwordResource)(nil) +) + +type passwordResource struct{} + +func (r *passwordResource) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { + var plan passwordModelV2 + + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } - if err := d.Set("bcrypt_hash", hash); err != nil { - diags = append(diags, diag.Errorf("err: %s", err)...) - return diags + params := random.StringParams{ + Length: plan.Length.Value, + Upper: plan.Upper.Value, + MinUpper: plan.MinUpper.Value, + Lower: plan.Lower.Value, + MinLower: plan.MinLower.Value, + Numeric: plan.Numeric.Value, + MinNumeric: plan.MinNumeric.Value, + Special: plan.Special.Value, + MinSpecial: plan.MinSpecial.Value, + OverrideSpecial: plan.OverrideSpecial.Value, } - return nil -} + result, err := random.CreateString(params) + if err != nil { + resp.Diagnostics.Append(diagnostics.RandomReadError(err.Error())...) + return + } -func importPasswordFunc(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - for k, v := range passwordSchemaV2() { - if v.Default == nil { - continue - } - if err := d.Set(k, v.Default); err != nil { - return nil, fmt.Errorf("error setting %s: %w", k, err) - } + state := passwordModelV2{ + ID: types.String{Value: "none"}, + Keepers: plan.Keepers, + Length: types.Int64{Value: plan.Length.Value}, + Special: types.Bool{Value: plan.Special.Value}, + Upper: types.Bool{Value: plan.Upper.Value}, + Lower: types.Bool{Value: plan.Lower.Value}, + Numeric: types.Bool{Value: plan.Numeric.Value}, + MinNumeric: types.Int64{Value: plan.MinNumeric.Value}, + MinUpper: types.Int64{Value: plan.MinUpper.Value}, + MinLower: types.Int64{Value: plan.MinLower.Value}, + MinSpecial: types.Int64{Value: plan.MinSpecial.Value}, + OverrideSpecial: types.String{Value: plan.OverrideSpecial.Value}, + Result: types.String{Value: string(result)}, } - for _, key := range []string{"number", "numeric"} { - if err := d.Set(key, true); err != nil { - return nil, fmt.Errorf("error setting %s: %w", key, err) - } + hash, err := generateHash(plan.Result.Value) + if err != nil { + resp.Diagnostics.Append(diagnostics.HashGenerationError(err.Error())...) } - val := d.Id() - d.SetId("none") + state.BcryptHash = types.String{Value: hash} - if err := d.Set("result", val); err != nil { - return nil, fmt.Errorf("resource password import failed, error setting result: %w", err) + diags = resp.State.Set(ctx, state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } +} + +// Read does not need to perform any operations as the state in ReadResourceResponse is already populated. +func (r *passwordResource) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { +} + +// Update is intentionally left blank as all required and optional attributes force replacement of the resource +// through the RequiresReplace AttributePlanModifier. +func (r *passwordResource) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { +} + +// Delete does not need to explicitly call resp.State.RemoveResource() as this is automatically handled by the +// [framework](https://github.com/hashicorp/terraform-plugin-framework/pull/301). +func (r *passwordResource) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { +} + +func (r *passwordResource) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { + id := req.ID - if err := d.Set("length", len(val)); err != nil { - return nil, fmt.Errorf("error setting length: %w", err) + state := passwordModelV2{ + ID: types.String{Value: "none"}, + Result: types.String{Value: id}, + Length: types.Int64{Value: int64(len(id))}, + Special: types.Bool{Value: true}, + Upper: types.Bool{Value: true}, + Lower: types.Bool{Value: true}, + Numeric: types.Bool{Value: true}, + MinSpecial: types.Int64{Value: 0}, + MinUpper: types.Int64{Value: 0}, + MinLower: types.Int64{Value: 0}, + MinNumeric: types.Int64{Value: 0}, } - hash, err := generateHash(val) + state.Keepers.ElemType = types.StringType + + hash, err := generateHash(id) if err != nil { - return nil, fmt.Errorf("resource password import failed, generate hash error: %w", err) + resp.Diagnostics.Append(diagnostics.HashGenerationError(err.Error())...) } - if err := d.Set("bcrypt_hash", hash); err != nil { - return nil, fmt.Errorf("resource password import failed, error setting bcrypt_hash: %w", err) - } + state.BcryptHash = types.String{Value: hash} - return []*schema.ResourceData{d}, nil + diags := resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } } -func resourcePasswordV1() *schema.Resource { - return &schema.Resource{ - Schema: passwordSchemaV1(), +func (r *passwordResource) UpgradeState(context.Context) map[int64]tfsdk.ResourceStateUpgrader { + schemaV0 := passwordSchemaV0() + schemaV1 := passwordSchemaV1() + + return map[int64]tfsdk.ResourceStateUpgrader{ + 0: { + PriorSchema: &schemaV0, + StateUpgrader: upgradePasswordStateV0toV2, + }, + 1: { + PriorSchema: &schemaV1, + StateUpgrader: upgradePasswordStateV1toV2, + }, } } -func resourcePasswordV0() *schema.Resource { - return &schema.Resource{ - Schema: passwordSchemaV0(), +func upgradePasswordStateV0toV2(ctx context.Context, req tfsdk.UpgradeResourceStateRequest, resp *tfsdk.UpgradeResourceStateResponse) { + type modelV0 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + Length types.Int64 `tfsdk:"length"` + Special types.Bool `tfsdk:"special"` + Upper types.Bool `tfsdk:"upper"` + Lower types.Bool `tfsdk:"lower"` + Number types.Bool `tfsdk:"number"` + MinNumeric types.Int64 `tfsdk:"min_numeric"` + MinUpper types.Int64 `tfsdk:"min_upper"` + MinLower types.Int64 `tfsdk:"min_lower"` + MinSpecial types.Int64 `tfsdk:"min_special"` + OverrideSpecial types.String `tfsdk:"override_special"` + Result types.String `tfsdk:"result"` } -} -func resourcePasswordStateUpgradeV0(_ context.Context, rawState map[string]interface{}, _ interface{}) (map[string]interface{}, error) { - if rawState == nil { - return nil, fmt.Errorf("resource password state upgrade failed, state is nil") + var passwordDataV0 modelV0 + + resp.Diagnostics.Append(req.State.Get(ctx, &passwordDataV0)...) + if resp.Diagnostics.HasError() { + return } - result, ok := rawState["result"].(string) - if !ok { - return nil, fmt.Errorf("resource password state upgrade failed, result is not a string: %T", rawState["result"]) + passwordDataV2 := passwordModelV2{ + Keepers: passwordDataV0.Keepers, + Length: passwordDataV0.Length, + Special: passwordDataV0.Special, + Upper: passwordDataV0.Upper, + Lower: passwordDataV0.Lower, + Numeric: passwordDataV0.Number, + MinNumeric: passwordDataV0.MinNumeric, + MinLower: passwordDataV0.MinLower, + MinSpecial: passwordDataV0.MinSpecial, + OverrideSpecial: passwordDataV0.OverrideSpecial, + Result: passwordDataV0.Result, + ID: passwordDataV0.ID, } - hash, err := generateHash(result) + hash, err := generateHash(passwordDataV2.Result.Value) if err != nil { - return nil, fmt.Errorf("resource password state upgrade failed, generate hash error: %w", err) + resp.Diagnostics.Append(diagnostics.HashGenerationError(err.Error())...) + return + } + + passwordDataV2.BcryptHash.Value = hash + + diags := resp.State.Set(ctx, passwordDataV2) + resp.Diagnostics.Append(diags...) +} + +func upgradePasswordStateV1toV2(ctx context.Context, req tfsdk.UpgradeResourceStateRequest, resp *tfsdk.UpgradeResourceStateResponse) { + type modelV1 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + Length types.Int64 `tfsdk:"length"` + Special types.Bool `tfsdk:"special"` + Upper types.Bool `tfsdk:"upper"` + Lower types.Bool `tfsdk:"lower"` + Number types.Bool `tfsdk:"number"` + MinNumeric types.Int64 `tfsdk:"min_numeric"` + MinUpper types.Int64 `tfsdk:"min_upper"` + MinLower types.Int64 `tfsdk:"min_lower"` + MinSpecial types.Int64 `tfsdk:"min_special"` + OverrideSpecial types.String `tfsdk:"override_special"` + Result types.String `tfsdk:"result"` + BcryptHash types.String `tfsdk:"bcrypt_hash"` + } + + var passwordDataV1 modelV1 + + resp.Diagnostics.Append(req.State.Get(ctx, &passwordDataV1)...) + if resp.Diagnostics.HasError() { + return } - rawState["bcrypt_hash"] = hash + passwordDataV2 := passwordModelV2{ + Keepers: passwordDataV1.Keepers, + Length: passwordDataV1.Length, + Special: passwordDataV1.Special, + Upper: passwordDataV1.Upper, + Lower: passwordDataV1.Lower, + Numeric: passwordDataV1.Number, + MinNumeric: passwordDataV1.MinNumeric, + MinLower: passwordDataV1.MinLower, + MinSpecial: passwordDataV1.MinSpecial, + OverrideSpecial: passwordDataV1.OverrideSpecial, + BcryptHash: passwordDataV1.BcryptHash, + Result: passwordDataV1.Result, + ID: passwordDataV1.ID, + } - return rawState, nil + diags := resp.State.Set(ctx, passwordDataV2) + resp.Diagnostics.Append(diags...) } func generateHash(toHash string) (string, error) { @@ -147,3 +258,495 @@ func generateHash(toHash string) (string, error) { return string(hash), err } + +func passwordSchemaV2() tfsdk.Schema { + return tfsdk.Schema{ + Version: 2, + Description: "Identical to [random_string](string.html) with the exception that the result is " + + "treated as sensitive and, thus, _not_ displayed in console output. Read more about sensitive " + + "data handling in the " + + "[Terraform documentation](https://www.terraform.io/docs/language/state/sensitive-data.html).\n\n" + + "This resource *does* use a cryptographic random number generator.", + Attributes: map[string]tfsdk.Attribute{ + "keepers": { + Description: "Arbitrary map of values that, when changed, will trigger recreation of " + + "resource. See [the main provider documentation](../index.html) for more information.", + Type: types.MapType{ + ElemType: types.StringType, + }, + Optional: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, + }, + + "length": { + Description: "The length of the string desired. The minimum value for length is 1 and, length " + + "must also be >= (`min_upper` + `min_lower` + `min_numeric` + `min_special`).", + Type: types.Int64Type, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, + Validators: []tfsdk.AttributeValidator{ + int64validator.AtLeast(1), + validators.NewIntIsAtLeastSumOfValidator( + tftypes.NewAttributePath().WithAttributeName("min_upper"), + tftypes.NewAttributePath().WithAttributeName("min_lower"), + tftypes.NewAttributePath().WithAttributeName("min_numeric"), + tftypes.NewAttributePath().WithAttributeName("min_special"), + ), + }, + }, + + "special": { + Description: "Include special characters in the result. These are `!@#$%&*()-_=+[]{}<>:?`. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "upper": { + Description: "Include uppercase alphabet characters in the result. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "lower": { + Description: "Include lowercase alphabet characters in the result. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "numeric": { + Description: "Include numeric characters in the result. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_numeric": { + Description: "Minimum number of numeric characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_upper": { + Description: "Minimum number of uppercase alphabet characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_lower": { + Description: "Minimum number of lowercase alphabet characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_special": { + Description: "Minimum number of special characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "override_special": { + Description: "Supply your own list of special characters to use for string generation. This " + + "overrides the default character list in the special argument. The `special` argument must " + + "still be set to true for any overwritten characters to be used in generation.", + Type: types.StringType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, + }, + + "result": { + Description: "The generated random string.", + Type: types.StringType, + Computed: true, + Sensitive: true, + }, + + "bcrypt_hash": { + Description: "A bcrypt hash of the generated random string.", + Type: types.StringType, + Computed: true, + Sensitive: true, + }, + + "id": { + Description: "A static value used internally by Terraform, this should not be referenced in configurations.", + Computed: true, + Type: types.StringType, + }, + }, + } +} + +func passwordSchemaV1() tfsdk.Schema { + return tfsdk.Schema{ + Version: 1, + Description: "Identical to [random_string](string.html) with the exception that the result is " + + "treated as sensitive and, thus, _not_ displayed in console output. Read more about sensitive " + + "data handling in the " + + "[Terraform documentation](https://www.terraform.io/docs/language/state/sensitive-data.html).\n\n" + + "This resource *does* use a cryptographic random number generator.", + Attributes: map[string]tfsdk.Attribute{ + "keepers": { + Description: "Arbitrary map of values that, when changed, will trigger recreation of " + + "resource. See [the main provider documentation](../index.html) for more information.", + Type: types.MapType{ + ElemType: types.StringType, + }, + Optional: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, + }, + + "length": { + Description: "The length of the string desired. The minimum value for length is 1 and, length " + + "must also be >= (`min_upper` + `min_lower` + `min_numeric` + `min_special`).", + Type: types.Int64Type, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, + Validators: []tfsdk.AttributeValidator{ + int64validator.AtLeast(1), + validators.NewIntIsAtLeastSumOfValidator( + tftypes.NewAttributePath().WithAttributeName("min_upper"), + tftypes.NewAttributePath().WithAttributeName("min_lower"), + tftypes.NewAttributePath().WithAttributeName("min_numeric"), + tftypes.NewAttributePath().WithAttributeName("min_special"), + ), + }, + }, + + "special": { + Description: "Include special characters in the result. These are `!@#$%&*()-_=+[]{}<>:?`. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "upper": { + Description: "Include uppercase alphabet characters in the result. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "lower": { + Description: "Include lowercase alphabet characters in the result. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "number": { + Description: "Include numeric characters in the result. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_numeric": { + Description: "Minimum number of numeric characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_upper": { + Description: "Minimum number of uppercase alphabet characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_lower": { + Description: "Minimum number of lowercase alphabet characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_special": { + Description: "Minimum number of special characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "override_special": { + Description: "Supply your own list of special characters to use for string generation. This " + + "overrides the default character list in the special argument. The `special` argument must " + + "still be set to true for any overwritten characters to be used in generation.", + Type: types.StringType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, + }, + + "result": { + Description: "The generated random string.", + Type: types.StringType, + Computed: true, + Sensitive: true, + }, + + "bcrypt_hash": { + Description: "A bcrypt hash of the generated random string.", + Type: types.StringType, + Computed: true, + Sensitive: true, + }, + + "id": { + Description: "A static value used internally by Terraform, this should not be referenced in configurations.", + Computed: true, + Type: types.StringType, + }, + }, + } +} + +func passwordSchemaV0() tfsdk.Schema { + return tfsdk.Schema{ + Description: "Identical to [random_string](string.html) with the exception that the result is " + + "treated as sensitive and, thus, _not_ displayed in console output. Read more about sensitive " + + "data handling in the " + + "[Terraform documentation](https://www.terraform.io/docs/language/state/sensitive-data.html).\n\n" + + "This resource *does* use a cryptographic random number generator.", + Attributes: map[string]tfsdk.Attribute{ + "keepers": { + Description: "Arbitrary map of values that, when changed, will trigger recreation of " + + "resource. See [the main provider documentation](../index.html) for more information.", + Type: types.MapType{ + ElemType: types.StringType, + }, + Optional: true, + PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, + }, + + "length": { + Description: "The length of the string desired. The minimum value for length is 1 and, length " + + "must also be >= (`min_upper` + `min_lower` + `min_numeric` + `min_special`).", + Type: types.Int64Type, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, + Validators: []tfsdk.AttributeValidator{ + int64validator.AtLeast(1), + validators.NewIntIsAtLeastSumOfValidator( + tftypes.NewAttributePath().WithAttributeName("min_upper"), + tftypes.NewAttributePath().WithAttributeName("min_lower"), + tftypes.NewAttributePath().WithAttributeName("min_numeric"), + tftypes.NewAttributePath().WithAttributeName("min_special"), + ), + }, + }, + + "special": { + Description: "Include special characters in the result. These are `!@#$%&*()-_=+[]{}<>:?`. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "upper": { + Description: "Include uppercase alphabet characters in the result. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "lower": { + Description: "Include lowercase alphabet characters in the result. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "number": { + Description: "Include numeric characters in the result. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_numeric": { + Description: "Minimum number of numeric characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_upper": { + Description: "Minimum number of uppercase alphabet characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_lower": { + Description: "Minimum number of lowercase alphabet characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_special": { + Description: "Minimum number of special characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "override_special": { + Description: "Supply your own list of special characters to use for string generation. This " + + "overrides the default character list in the special argument. The `special` argument must " + + "still be set to true for any overwritten characters to be used in generation.", + Type: types.StringType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, + }, + + "result": { + Description: "The generated random string.", + Type: types.StringType, + Computed: true, + Sensitive: true, + }, + + "id": { + Description: "A static value used internally by Terraform, this should not be referenced in configurations.", + Computed: true, + Type: types.StringType, + }, + }, + } +} + +type passwordModelV2 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + Length types.Int64 `tfsdk:"length"` + Special types.Bool `tfsdk:"special"` + Upper types.Bool `tfsdk:"upper"` + Lower types.Bool `tfsdk:"lower"` + Numeric types.Bool `tfsdk:"numeric"` + MinNumeric types.Int64 `tfsdk:"min_numeric"` + MinUpper types.Int64 `tfsdk:"min_upper"` + MinLower types.Int64 `tfsdk:"min_lower"` + MinSpecial types.Int64 `tfsdk:"min_special"` + OverrideSpecial types.String `tfsdk:"override_special"` + Result types.String `tfsdk:"result"` + BcryptHash types.String `tfsdk:"bcrypt_hash"` +} diff --git a/internal/provider/resource_password_test.go b/internal/provider/resource_password_test.go index fd8042c9..11b4fae1 100644 --- a/internal/provider/resource_password_test.go +++ b/internal/provider/resource_password_test.go @@ -2,30 +2,29 @@ package provider import ( "context" - "errors" "fmt" "regexp" "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "golang.org/x/crypto/bcrypt" ) -func TestAccResourcePasswordBasic(t *testing.T) { +func TestAccResourcePassword(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: `resource "random_password" "basic" { length = 12 }`, Check: resource.ComposeTestCheckFunc( - testAccResourceStringCheck("random_password.basic", &customLens{ - customLen: 12, - }), + resource.TestCheckResourceAttrWith("random_password.basic", "result", testCheckLen(12)), ), }, { @@ -53,10 +52,9 @@ func TestAccResourcePasswordBasic(t *testing.T) { }) } -func TestAccResourcePasswordOverride(t *testing.T) { +func TestAccResourcePassword_Override(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { Config: `resource "random_password" "override" { @@ -64,111 +62,243 @@ func TestAccResourcePasswordOverride(t *testing.T) { override_special = "!" lower = false upper = false - number = false + numeric = false }`, Check: resource.ComposeTestCheckFunc( - testAccResourceStringCheck("random_password.override", &customLens{ - customLen: 4, - }), - patternMatch("random_password.override", "!!!!"), + resource.TestCheckResourceAttrWith("random_password.override", "result", testCheckLen(4)), + resource.TestCheckResourceAttr("random_password.override", "result", "!!!!"), ), }, }, }) } -func TestAccResourcePasswordMin(t *testing.T) { - resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, - Steps: []resource.TestStep{ - { - Config: `resource "random_password" "min" { - length = 12 - override_special = "!#@" - min_lower = 2 - min_upper = 3 - min_special = 1 - min_numeric = 4 - }`, - Check: resource.ComposeTestCheckFunc( - testAccResourceStringCheck("random_password.min", &customLens{ - customLen: 12, - }), - regexMatch("random_password.min", regexp.MustCompile(`([a-z])`), 2), - regexMatch("random_password.min", regexp.MustCompile(`([A-Z])`), 3), - regexMatch("random_password.min", regexp.MustCompile(`([0-9])`), 4), - regexMatch("random_password.min", regexp.MustCompile(`([!#@])`), 1), - ), +// TestAccResourcePassword_StateUpgradeV0toV2 covers the state upgrades from V0 to V2. +// This includes the deprecation and removal of `number` and the addition of `numeric` +// and `bcrypt_hash` attributes. +// v3.1.3 is used as this is last version before `bcrypt_hash` attributed was added. +func TestAccResourcePassword_StateUpgradeV0toV2(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + configBeforeUpgrade string + configDuringUpgrade string + beforeStateUpgrade []resource.TestCheckFunc + afterStateUpgrade []resource.TestCheckFunc + }{ + { + name: "bcrypt_hash", + configBeforeUpgrade: `resource "random_password" "default" { + length = 12 + }`, + configDuringUpgrade: `resource "random_password" "default" { + length = 12 + }`, + beforeStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckNoResourceAttr("random_password.default", "bcrypt_hash"), + }, + afterStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckResourceAttrSet("random_password.default", "bcrypt_hash"), }, }, - }) -} - -func TestAccResourcePassword_UpdateNumberAndNumeric(t *testing.T) { - t.Parallel() - resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, - Steps: []resource.TestStep{ - { - Config: `resource "random_password" "default" { - length = 12 - }`, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("random_password.default", "number", "true"), - resource.TestCheckResourceAttr("random_password.default", "numeric", "true"), - ), + { + name: "number is absent before numeric is absent during", + configBeforeUpgrade: `resource "random_password" "default" { + length = 12 + }`, + configDuringUpgrade: `resource "random_password" "default" { + length = 12 + }`, + beforeStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("random_password.default", "number", "true"), + resource.TestCheckNoResourceAttr("random_password.default", "numeric"), }, - { - Config: `resource "random_password" "default" { - length = 12 - number = false - }`, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("random_password.default", "number", "false"), - resource.TestCheckResourceAttr("random_password.default", "numeric", "false"), - ), + afterStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("random_password.default", "numeric", "true"), + resource.TestCheckNoResourceAttr("random_password.default", "number"), }, - { - Config: `resource "random_password" "default" { - length = 12 - numeric = true - }`, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("random_password.default", "number", "true"), - resource.TestCheckResourceAttr("random_password.default", "numeric", "true"), - ), + }, + { + name: "number is absent before numeric is true during", + configBeforeUpgrade: `resource "random_password" "default" { + length = 12 + }`, + configDuringUpgrade: `resource "random_password" "default" { + length = 12 + numeric = true + }`, + beforeStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("random_password.default", "number", "true"), + resource.TestCheckNoResourceAttr("random_password.default", "numeric"), }, - { - Config: `resource "random_password" "default" { - length = 12 - numeric = false - }`, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("random_password.default", "number", "false"), - resource.TestCheckResourceAttr("random_password.default", "numeric", "false"), - ), + afterStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("random_password.default", "numeric", "true"), + resource.TestCheckNoResourceAttr("random_password.default", "number"), }, - { - Config: `resource "random_password" "default" { - length = 12 - }`, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("random_password.default", "number", "true"), - resource.TestCheckResourceAttr("random_password.default", "numeric", "true"), - ), + }, + { + name: "number is absent before numeric is false during", + configBeforeUpgrade: `resource "random_password" "default" { + length = 12 + }`, + configDuringUpgrade: `resource "random_password" "default" { + length = 12 + numeric = false + }`, + beforeStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("random_password.default", "number", "true"), + resource.TestCheckNoResourceAttr("random_password.default", "numeric"), + }, + afterStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("random_password.default", "numeric", "false"), + resource.TestCheckNoResourceAttr("random_password.default", "number"), }, }, - }) + { + name: "number is true before numeric is absent during", + configBeforeUpgrade: `resource "random_password" "default" { + length = 12 + number = true + }`, + configDuringUpgrade: `resource "random_password" "default" { + length = 12 + }`, + beforeStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("random_password.default", "number", "true"), + resource.TestCheckNoResourceAttr("random_password.default", "numeric"), + }, + afterStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("random_password.default", "numeric", "true"), + resource.TestCheckNoResourceAttr("random_password.default", "number"), + }, + }, + { + name: "number is true before numeric is absent during", + configBeforeUpgrade: `resource "random_password" "default" { + length = 12 + number = true + }`, + configDuringUpgrade: `resource "random_password" "default" { + length = 12 + }`, + beforeStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("random_password.default", "number", "true"), + resource.TestCheckNoResourceAttr("random_password.default", "numeric"), + }, + afterStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("random_password.default", "numeric", "true"), + resource.TestCheckNoResourceAttr("random_password.default", "number"), + }, + }, + { + name: "number is true before numeric is false during", + configBeforeUpgrade: `resource "random_password" "default" { + length = 12 + number = true + }`, + configDuringUpgrade: `resource "random_password" "default" { + length = 12 + numeric = false + }`, + beforeStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("random_password.default", "number", "true"), + resource.TestCheckNoResourceAttr("random_password.default", "numeric"), + }, + afterStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("random_password.default", "numeric", "false"), + resource.TestCheckNoResourceAttr("random_password.default", "number"), + }, + }, + { + name: "number is false before numeric is false during", + configBeforeUpgrade: `resource "random_password" "default" { + length = 12 + number = false + }`, + configDuringUpgrade: `resource "random_password" "default" { + length = 12 + numeric = false + }`, + beforeStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("random_password.default", "number", "false"), + resource.TestCheckNoResourceAttr("random_password.default", "numeric"), + }, + afterStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("random_password.default", "numeric", "false"), + resource.TestCheckNoResourceAttr("random_password.default", "number"), + }, + }, + { + name: "number is false before numeric is absent during", + configBeforeUpgrade: `resource "random_password" "default" { + length = 12 + number = false + }`, + configDuringUpgrade: `resource "random_password" "default" { + length = 12 + }`, + beforeStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("random_password.default", "number", "false"), + resource.TestCheckNoResourceAttr("random_password.default", "numeric"), + }, + afterStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("random_password.default", "numeric", "true"), + resource.TestCheckNoResourceAttr("random_password.default", "number"), + }, + }, + { + name: "number is false before numeric is true during", + configBeforeUpgrade: `resource "random_password" "default" { + length = 12 + number = false + }`, + configDuringUpgrade: `resource "random_password" "default" { + length = 12 + numeric = true + }`, + beforeStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("random_password.default", "number", "false"), + resource.TestCheckNoResourceAttr("random_password.default", "numeric"), + }, + afterStateUpgrade: []resource.TestCheckFunc{ + resource.TestCheckResourceAttr("random_password.default", "numeric", "true"), + resource.TestCheckNoResourceAttr("random_password.default", "number"), + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{"random": { + VersionConstraint: "3.1.3", + Source: "hashicorp/random", + }}, + Config: c.configBeforeUpgrade, + Check: resource.ComposeTestCheckFunc(c.beforeStateUpgrade...), + }, + { + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Config: c.configDuringUpgrade, + Check: resource.ComposeTestCheckFunc(c.afterStateUpgrade...), + }, + }, + }) + }) + } } -// TestAccResourcePassword_StateUpgraders covers the state upgrades from v0 to V2 and V1 to V2. -// This includes the addition of bcrypt_hash and numeric attributes. -func TestAccResourcePassword_StateUpgraders(t *testing.T) { +// TestAccResourcePassword_StateUpgrade_V1toV2 covers the state upgrades from V1 to V2. +// This includes the deprecation and removal of `number` and the addition of `numeric` attributes. +// v3.2.0 was used as this is the last version before `number` was deprecated and `numeric` attribute +// was added. +func TestAccResourcePassword_StateUpgradeV1toV2(t *testing.T) { t.Parallel() - v1Cases := []struct { + cases := []struct { name string configBeforeUpgrade string configDuringUpgrade string @@ -176,72 +306,79 @@ func TestAccResourcePassword_StateUpgraders(t *testing.T) { afterStateUpgrade []resource.TestCheckFunc }{ { - name: "%s number is absent", + name: "number is absent before numeric is absent during", configBeforeUpgrade: `resource "random_password" "default" { length = 12 }`, + configDuringUpgrade: `resource "random_password" "default" { + length = 12 + }`, beforeStateUpgrade: []resource.TestCheckFunc{ resource.TestCheckResourceAttr("random_password.default", "number", "true"), resource.TestCheckNoResourceAttr("random_password.default", "numeric"), }, afterStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("random_password.default", "number", "true"), - resource.TestCheckResourceAttrPair("random_password.default", "number", "random_password.default", "numeric"), + resource.TestCheckResourceAttr("random_password.default", "numeric", "true"), + resource.TestCheckNoResourceAttr("random_password.default", "number"), }, }, { - name: "%s number is absent then true", + name: "number is absent before numeric is true during", configBeforeUpgrade: `resource "random_password" "default" { length = 12 }`, configDuringUpgrade: `resource "random_password" "default" { length = 12 - number = true + numeric = true }`, beforeStateUpgrade: []resource.TestCheckFunc{ resource.TestCheckResourceAttr("random_password.default", "number", "true"), resource.TestCheckNoResourceAttr("random_password.default", "numeric"), }, afterStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("random_password.default", "number", "true"), - resource.TestCheckResourceAttrPair("random_password.default", "number", "random_password.default", "numeric"), + resource.TestCheckResourceAttr("random_password.default", "numeric", "true"), + resource.TestCheckNoResourceAttr("random_password.default", "number"), }, }, { - name: "%s number is absent then false", + name: "number is absent before numeric is false during", configBeforeUpgrade: `resource "random_password" "default" { length = 12 }`, configDuringUpgrade: `resource "random_password" "default" { length = 12 - number = false + numeric = false }`, beforeStateUpgrade: []resource.TestCheckFunc{ resource.TestCheckResourceAttr("random_password.default", "number", "true"), resource.TestCheckNoResourceAttr("random_password.default", "numeric"), }, afterStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("random_password.default", "number", "false"), - resource.TestCheckResourceAttrPair("random_password.default", "number", "random_password.default", "numeric"), + resource.TestCheckResourceAttr("random_password.default", "numeric", "false"), + resource.TestCheckNoResourceAttr("random_password.default", "number"), }, }, { - name: "%s number is true", + name: "number is true before numeric is true during", configBeforeUpgrade: `resource "random_password" "default" { length = 12 number = true }`, + configDuringUpgrade: `resource "random_password" "default" { + length = 12 + numeric = true + }`, beforeStateUpgrade: []resource.TestCheckFunc{ resource.TestCheckResourceAttr("random_password.default", "number", "true"), resource.TestCheckNoResourceAttr("random_password.default", "numeric"), }, afterStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("random_password.default", "number", "true"), - resource.TestCheckResourceAttrPair("random_password.default", "number", "random_password.default", "numeric"), + resource.TestCheckResourceAttr("random_password.default", "numeric", "true"), + resource.TestCheckNoResourceAttr("random_password.default", "number"), }, }, { - name: "%s number is true then absent", + name: "number is true before numeric is absent during", configBeforeUpgrade: `resource "random_password" "default" { length = 12 number = true @@ -254,46 +391,50 @@ func TestAccResourcePassword_StateUpgraders(t *testing.T) { resource.TestCheckNoResourceAttr("random_password.default", "numeric"), }, afterStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("random_password.default", "number", "true"), - resource.TestCheckResourceAttrPair("random_password.default", "number", "random_password.default", "numeric"), + resource.TestCheckResourceAttr("random_password.default", "numeric", "true"), + resource.TestCheckNoResourceAttr("random_password.default", "number"), }, }, { - name: "%s number is true then false", + name: "number is true before numeric is false during", configBeforeUpgrade: `resource "random_password" "default" { length = 12 number = true }`, configDuringUpgrade: `resource "random_password" "default" { length = 12 - number = false + numeric = false }`, beforeStateUpgrade: []resource.TestCheckFunc{ resource.TestCheckResourceAttr("random_password.default", "number", "true"), resource.TestCheckNoResourceAttr("random_password.default", "numeric"), }, afterStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("random_password.default", "number", "false"), - resource.TestCheckResourceAttrPair("random_password.default", "number", "random_password.default", "numeric"), + resource.TestCheckResourceAttr("random_password.default", "numeric", "false"), + resource.TestCheckNoResourceAttr("random_password.default", "number"), }, }, { - name: "%s number is false", + name: "number is false before numeric is false during", configBeforeUpgrade: `resource "random_password" "default" { length = 12 number = false }`, + configDuringUpgrade: `resource "random_password" "default" { + length = 12 + numeric = false + }`, beforeStateUpgrade: []resource.TestCheckFunc{ resource.TestCheckResourceAttr("random_password.default", "number", "false"), resource.TestCheckNoResourceAttr("random_password.default", "numeric"), }, afterStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("random_password.default", "number", "false"), - resource.TestCheckResourceAttrPair("random_password.default", "number", "random_password.default", "numeric"), + resource.TestCheckResourceAttr("random_password.default", "numeric", "false"), + resource.TestCheckNoResourceAttr("random_password.default", "number"), }, }, { - name: "%s number is false then absent", + name: "number is false before numeric is absent during", configBeforeUpgrade: `resource "random_password" "default" { length = 12 number = false @@ -306,143 +447,281 @@ func TestAccResourcePassword_StateUpgraders(t *testing.T) { resource.TestCheckNoResourceAttr("random_password.default", "numeric"), }, afterStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("random_password.default", "number", "true"), - resource.TestCheckResourceAttrPair("random_password.default", "number", "random_password.default", "numeric"), + resource.TestCheckResourceAttr("random_password.default", "numeric", "true"), + resource.TestCheckNoResourceAttr("random_password.default", "number"), }, }, { - name: "%s number is false then true", + name: "number is false before numeric is true during", configBeforeUpgrade: `resource "random_password" "default" { length = 12 number = false }`, configDuringUpgrade: `resource "random_password" "default" { length = 12 - number = true + numeric = true }`, beforeStateUpgrade: []resource.TestCheckFunc{ resource.TestCheckResourceAttr("random_password.default", "number", "false"), resource.TestCheckNoResourceAttr("random_password.default", "numeric"), }, afterStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("random_password.default", "number", "true"), - resource.TestCheckResourceAttrPair("random_password.default", "number", "random_password.default", "numeric"), + resource.TestCheckResourceAttr("random_password.default", "numeric", "true"), + resource.TestCheckNoResourceAttr("random_password.default", "number"), }, }, } - v0Cases := v1Cases - v0Cases = append(v0Cases, struct { - name string - configBeforeUpgrade string - configDuringUpgrade string - beforeStateUpgrade []resource.TestCheckFunc - afterStateUpgrade []resource.TestCheckFunc - }{ - name: "%s bcrypt_hash", - configBeforeUpgrade: `resource "random_password" "default" { - length = 12 - }`, - beforeStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckNoResourceAttr("random_password.default", "bcrypt_hash"), + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if c.configDuringUpgrade == "" { + c.configDuringUpgrade = c.configBeforeUpgrade + } + + // TODO: Why is resource.Test not being used here + resource.UnitTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{"random": { + VersionConstraint: "3.2.0", + Source: "hashicorp/random", + }}, + Config: c.configBeforeUpgrade, + Check: resource.ComposeTestCheckFunc(c.beforeStateUpgrade...), + }, + { + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Config: c.configDuringUpgrade, + Check: resource.ComposeTestCheckFunc(c.afterStateUpgrade...), + }, + }, + }) + }) + } +} + +func TestAccResourcePassword_Min(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: `resource "random_password" "min" { + length = 12 + override_special = "!#@" + min_lower = 2 + min_upper = 3 + min_special = 1 + min_numeric = 4 + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrWith("random_password.min", "result", testCheckLen(12)), + resource.TestMatchResourceAttr("random_password.min", "result", regexp.MustCompile(`([a-z].*){2,}`)), + resource.TestMatchResourceAttr("random_password.min", "result", regexp.MustCompile(`([A-Z].*){3,}`)), + resource.TestMatchResourceAttr("random_password.min", "result", regexp.MustCompile(`([0-9].*){4,}`)), + resource.TestMatchResourceAttr("random_password.min", "result", regexp.MustCompile(`([!#@])`)), + ), + }, }, - afterStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckResourceAttrSet("random_password.default", "bcrypt_hash"), + }) +} + +func TestAccResourcePassword_UpgradeFromVersion3_3_2(t *testing.T) { + resource.Test(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ExternalProviders: providerVersion332(), + Config: `resource "random_password" "min" { + length = 12 + override_special = "!#@" + min_lower = 2 + min_upper = 3 + min_special = 1 + min_numeric = 4 + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrWith("random_password.min", "result", testCheckLen(12)), + resource.TestMatchResourceAttr("random_password.min", "result", regexp.MustCompile(`([a-z].*){2,}`)), + resource.TestMatchResourceAttr("random_password.min", "result", regexp.MustCompile(`([A-Z].*){3,}`)), + resource.TestMatchResourceAttr("random_password.min", "result", regexp.MustCompile(`([0-9].*){4,}`)), + resource.TestMatchResourceAttr("random_password.min", "result", regexp.MustCompile(`([!#@])`)), + resource.TestCheckResourceAttr("random_password.min", "special", "true"), + resource.TestCheckResourceAttr("random_password.min", "upper", "true"), + resource.TestCheckResourceAttr("random_password.min", "lower", "true"), + resource.TestCheckResourceAttr("random_password.min", "numeric", "true"), + resource.TestCheckResourceAttr("random_password.min", "min_special", "1"), + resource.TestCheckResourceAttr("random_password.min", "min_upper", "3"), + resource.TestCheckResourceAttr("random_password.min", "min_lower", "2"), + resource.TestCheckResourceAttr("random_password.min", "min_numeric", "4"), + resource.TestCheckResourceAttrSet("random_password.min", "bcrypt_hash"), + ), + }, + { + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Config: `resource "random_password" "min" { + length = 12 + override_special = "!#@" + min_lower = 2 + min_upper = 3 + min_special = 1 + min_numeric = 4 + }`, + PlanOnly: true, + }, + { + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Config: `resource "random_password" "min" { + length = 12 + override_special = "!#@" + min_lower = 2 + min_upper = 3 + min_special = 1 + min_numeric = 4 + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrWith("random_password.min", "result", testCheckLen(12)), + resource.TestMatchResourceAttr("random_password.min", "result", regexp.MustCompile(`([a-z].*){2,}`)), + resource.TestMatchResourceAttr("random_password.min", "result", regexp.MustCompile(`([A-Z].*){3,}`)), + resource.TestMatchResourceAttr("random_password.min", "result", regexp.MustCompile(`([0-9].*){4,}`)), + resource.TestMatchResourceAttr("random_password.min", "result", regexp.MustCompile(`([!#@])`)), + resource.TestCheckResourceAttr("random_password.min", "special", "true"), + resource.TestCheckResourceAttr("random_password.min", "upper", "true"), + resource.TestCheckResourceAttr("random_password.min", "lower", "true"), + resource.TestCheckResourceAttr("random_password.min", "numeric", "true"), + resource.TestCheckResourceAttr("random_password.min", "min_special", "1"), + resource.TestCheckResourceAttr("random_password.min", "min_upper", "3"), + resource.TestCheckResourceAttr("random_password.min", "min_lower", "2"), + resource.TestCheckResourceAttr("random_password.min", "min_numeric", "4"), + resource.TestCheckResourceAttrSet("random_password.min", "bcrypt_hash"), + ), + }, }, }) +} - cases := map[string][]struct { - name string - configBeforeUpgrade string - configDuringUpgrade string - beforeStateUpgrade []resource.TestCheckFunc - afterStateUpgrade []resource.TestCheckFunc - }{ - "3.1.3": v0Cases, - "3.2.0": v1Cases, +func TestUpgradePasswordStateV0toV2(t *testing.T) { + raw := tftypes.NewValue(tftypes.Object{}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "none"), + "keepers": tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, nil), + "length": tftypes.NewValue(tftypes.Number, 16), + "lower": tftypes.NewValue(tftypes.Bool, true), + "min_lower": tftypes.NewValue(tftypes.Number, 0), + "min_numeric": tftypes.NewValue(tftypes.Number, 0), + "min_special": tftypes.NewValue(tftypes.Number, 0), + "min_upper": tftypes.NewValue(tftypes.Number, 0), + "number": tftypes.NewValue(tftypes.Bool, true), + "override_special": tftypes.NewValue(tftypes.String, "!#$%\u0026*()-_=+[]{}\u003c\u003e:?"), + "result": tftypes.NewValue(tftypes.String, "DZy_3*tnonj%Q%Yx"), + "special": tftypes.NewValue(tftypes.Bool, true), + "upper": tftypes.NewValue(tftypes.Bool, true), + }) + + req := tfsdk.UpgradeResourceStateRequest{ + State: &tfsdk.State{ + Raw: raw, + Schema: passwordSchemaV0(), + }, + } + + resp := &tfsdk.UpgradeResourceStateResponse{ + State: tfsdk.State{ + Schema: passwordSchemaV2(), + }, } - for providerVersion, v := range cases { - for _, c := range v { - name := fmt.Sprintf(c.name, providerVersion) - t.Run(name, func(t *testing.T) { - if c.configDuringUpgrade == "" { - c.configDuringUpgrade = c.configBeforeUpgrade - } + upgradePasswordStateV0toV2(context.Background(), req, resp) - resource.UnitTest(t, resource.TestCase{ - Steps: []resource.TestStep{ - { - ExternalProviders: map[string]resource.ExternalProvider{"random": { - VersionConstraint: providerVersion, - Source: "hashicorp/random", - }}, - Config: c.configBeforeUpgrade, - Check: resource.ComposeTestCheckFunc(c.beforeStateUpgrade...), - }, - { - ProviderFactories: testAccProviders, - Config: c.configDuringUpgrade, - Check: resource.ComposeTestCheckFunc(c.afterStateUpgrade...), - }, - }, - }) - }) - } + expected := passwordModelV2{ + ID: types.String{Value: "none"}, + Keepers: types.Map{Null: true, ElemType: types.StringType}, + Length: types.Int64{Value: 16}, + Special: types.Bool{Value: true}, + Upper: types.Bool{Value: true}, + Lower: types.Bool{Value: true}, + Numeric: types.Bool{Value: true}, + MinNumeric: types.Int64{Value: 0}, + MinUpper: types.Int64{Value: 0}, + MinLower: types.Int64{Value: 0}, + MinSpecial: types.Int64{Value: 0}, + OverrideSpecial: types.String{Value: "!#$%\u0026*()-_=+[]{}\u003c\u003e:?"}, + Result: types.String{Value: "DZy_3*tnonj%Q%Yx"}, + } + + actual := passwordModelV2{} + diags := resp.State.Get(context.Background(), &actual) + if diags.HasError() { + t.Errorf("error getting state: %v", diags) + } + + err := bcrypt.CompareHashAndPassword([]byte(actual.BcryptHash.Value), []byte(actual.Result.Value)) + if err != nil { + t.Errorf("unexpected bcrypt comparison error: %s", err) + } + + // Setting actual.BcryptHash to zero value to allow direct comparison of expected and actual. + actual.BcryptHash = types.String{} + + if !cmp.Equal(expected, actual) { + t.Errorf("expected: %+v, got: %+v", expected, actual) } } -func TestResourcePasswordStateUpgradeV0(t *testing.T) { - cases := []struct { - name string - stateV0 map[string]interface{} - err error - expectedStateV1 map[string]interface{} - }{ - { - name: "result is not string", - stateV0: map[string]interface{}{"result": 0}, - err: errors.New("resource password state upgrade failed, result is not a string: int"), +func TestUpgradePasswordStateV1toV2(t *testing.T) { + raw := tftypes.NewValue(tftypes.Object{}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "none"), + "keepers": tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, nil), + "length": tftypes.NewValue(tftypes.Number, 16), + "lower": tftypes.NewValue(tftypes.Bool, true), + "min_lower": tftypes.NewValue(tftypes.Number, 0), + "min_numeric": tftypes.NewValue(tftypes.Number, 0), + "min_special": tftypes.NewValue(tftypes.Number, 0), + "min_upper": tftypes.NewValue(tftypes.Number, 0), + "number": tftypes.NewValue(tftypes.Bool, true), + "override_special": tftypes.NewValue(tftypes.String, "!#$%\u0026*()-_=+[]{}\u003c\u003e:?"), + "result": tftypes.NewValue(tftypes.String, "DZy_3*tnonj%Q%Yx"), + "special": tftypes.NewValue(tftypes.Bool, true), + "upper": tftypes.NewValue(tftypes.Bool, true), + "bcrypt_hash": tftypes.NewValue(tftypes.String, "bcrypt_hash"), + }) + + req := tfsdk.UpgradeResourceStateRequest{ + State: &tfsdk.State{ + Raw: raw, + Schema: passwordSchemaV1(), }, - { - name: "success", - stateV0: map[string]interface{}{"result": "abc123"}, - expectedStateV1: map[string]interface{}{"result": "abc123", "bcrypt_hash": "123"}, + } + + resp := &tfsdk.UpgradeResourceStateResponse{ + State: tfsdk.State{ + Schema: passwordSchemaV2(), }, } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - actualStateV1, err := resourcePasswordStateUpgradeV0(context.Background(), c.stateV0, nil) + upgradePasswordStateV1toV2(context.Background(), req, resp) - if c.err != nil { - // Check error msg - if !cmp.Equal(c.err.Error(), err.Error()) { - t.Errorf("expected: %q, got: %q", c.err.Error(), err) - } - // Check actualStateV1 is nil - if !cmp.Equal(c.expectedStateV1, actualStateV1) { - t.Errorf("expected: %+v, got: %+v", c.expectedStateV1, err) - } - } else { - if err != nil { - t.Errorf("err should be nil, actual: %v", err) - } + expected := passwordModelV2{ + ID: types.String{Value: "none"}, + Keepers: types.Map{Null: true, ElemType: types.StringType}, + Length: types.Int64{Value: 16}, + Special: types.Bool{Value: true}, + Upper: types.Bool{Value: true}, + Lower: types.Bool{Value: true}, + Numeric: types.Bool{Value: true}, + MinNumeric: types.Int64{Value: 0}, + MinUpper: types.Int64{Value: 0}, + MinLower: types.Int64{Value: 0}, + MinSpecial: types.Int64{Value: 0}, + OverrideSpecial: types.String{Value: "!#$%\u0026*()-_=+[]{}\u003c\u003e:?"}, + BcryptHash: types.String{Value: "bcrypt_hash"}, + Result: types.String{Value: "DZy_3*tnonj%Q%Yx"}, + } - // Compare bcrypt_hash with plaintext equivalent to verify match - bcryptHash := actualStateV1["bcrypt_hash"] - err := bcrypt.CompareHashAndPassword([]byte(bcryptHash.(string)), []byte(c.stateV0["result"].(string))) - if err != nil { - t.Error("hash and password do not match") - } + actual := passwordModelV2{} + diags := resp.State.Get(context.Background(), &actual) + if diags.HasError() { + t.Errorf("error getting state: %v", diags) + } - // Delete bcrypt_hash from actualStateV1 and expectedStateV1 so can compare - delete(actualStateV1, "bcrypt_hash") - delete(c.expectedStateV1, "bcrypt_hash") - if !cmp.Equal(actualStateV1, c.expectedStateV1) { - t.Errorf("expected: %v, got: %v", c.expectedStateV1, actualStateV1) - } - } - }) + if !cmp.Equal(expected, actual) { + t.Errorf("expected: %+v, got: %+v", expected, actual) } } diff --git a/internal/provider/resource_pet.go b/internal/provider/resource_pet.go index 0d29f33d..2e0ba2e7 100644 --- a/internal/provider/resource_pet.go +++ b/internal/provider/resource_pet.go @@ -6,80 +6,140 @@ import ( "strings" petname "github.com/dustinkirkland/golang-petname" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/terraform-providers/terraform-provider-random/internal/planmodifiers" ) -func resourcePet() *schema.Resource { - // This is necessary to ensure each call to petname is properly randomised: - // the library uses `rand.Intn()` and does NOT seed `rand.Seed()` by default, - // so this call takes care of that. - petname.NonDeterministicMode() +var _ tfsdk.ResourceType = (*petResourceType)(nil) - return &schema.Resource{ +type petResourceType struct{} + +func (r *petResourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{ Description: "The resource `random_pet` generates random pet names that are intended to be used as " + "unique identifiers for other resources.\n" + "\n" + "This resource can be used in conjunction with resources that have the `create_before_destroy` " + "lifecycle flag set, to avoid conflicts with unique names during the brief period where both the old " + "and new resources exist concurrently.", - CreateContext: CreatePet, - ReadContext: schema.NoopContext, - DeleteContext: RemoveResourceFromState, - - Schema: map[string]*schema.Schema{ + Attributes: map[string]tfsdk.Attribute{ "keepers": { Description: "Arbitrary map of values that, when changed, will trigger recreation of " + "resource. See [the main provider documentation](../index.html) for more information.", - Type: schema.TypeMap, + Type: types.MapType{ + ElemType: types.StringType, + }, Optional: true, - ForceNew: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, }, - "length": { Description: "The length (in words) of the pet name. Defaults to 2", - Type: schema.TypeInt, + Type: types.Int64Type, Optional: true, - Default: 2, - ForceNew: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 2}), + planmodifiers.RequiresReplace(), + }, }, - "prefix": { - Description: "A string to prefix the name with.", - Type: schema.TypeString, - Optional: true, - ForceNew: true, + Description: "A string to prefix the name with.", + Type: types.StringType, + Optional: true, + PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, }, - "separator": { Description: "The character to separate words in the pet name. Defaults to \"-\"", - Type: schema.TypeString, + Type: types.StringType, Optional: true, - Default: "-", - ForceNew: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.String{Value: "-"}), + planmodifiers.RequiresReplace(), + }, }, - "id": { - Description: "The random pet name", - Type: schema.TypeString, + Description: "The random pet name.", + Type: types.StringType, Computed: true, }, }, - } + }, nil } -func CreatePet(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - length := d.Get("length").(int) - separator := d.Get("separator").(string) - prefix := d.Get("prefix").(string) +func (r *petResourceType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return &petResource{}, nil +} + +var _ tfsdk.Resource = (*petResource)(nil) + +type petResource struct{} + +func (r *petResource) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { + // This is necessary to ensure each call to petname is properly randomised: + // the library uses `rand.Intn()` and does NOT seed `rand.Seed()` by default, + // so this call takes care of that. + petname.NonDeterministicMode() + + var plan petModelV0 - pet := strings.ToLower(petname.Generate(length, separator)) + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + length := plan.Length.Value + separator := plan.Separator.Value + prefix := plan.Prefix.Value + + pet := strings.ToLower(petname.Generate(int(length), separator)) + + pn := petModelV0{ + Keepers: plan.Keepers, + Length: types.Int64{Value: length}, + Separator: types.String{Value: separator}, + } if prefix != "" { pet = fmt.Sprintf("%s%s%s", prefix, separator, pet) + pn.Prefix.Value = prefix + } else { + pn.Prefix.Null = true } - d.SetId(pet) + pn.ID.Value = pet + + diags = resp.State.Set(ctx, pn) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read does not need to perform any operations as the state in ReadResourceResponse is already populated. +func (r *petResource) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { +} + +// Update is intentionally left blank as all required and optional attributes force replacement of the resource +// through the RequiresReplace AttributePlanModifier. +func (r *petResource) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { +} + +// Delete does not need to explicitly call resp.State.RemoveResource() as this is automatically handled by the +// [framework](https://github.com/hashicorp/terraform-plugin-framework/pull/301). +func (r *petResource) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { +} - return nil +type petModelV0 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + Length types.Int64 `tfsdk:"length"` + Prefix types.String `tfsdk:"prefix"` + Separator types.String `tfsdk:"separator"` } diff --git a/internal/provider/resource_pet_test.go b/internal/provider/resource_pet_test.go index 0f838ff5..3a5fc832 100644 --- a/internal/provider/resource_pet_test.go +++ b/internal/provider/resource_pet_test.go @@ -7,110 +7,115 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -func TestAccResourcePet_basic(t *testing.T) { +func TestAccResourcePet(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testAccResourcePet_basic, + Config: `resource "random_pet" "pet_1" { + }`, Check: resource.ComposeTestCheckFunc( - testAccResourcePetLength("random_pet.pet_1", "-", 2), + resource.TestCheckResourceAttrWith("random_pet.pet_1", "id", testCheckPetLen("-", 2)), ), }, }, }) } -func TestAccResourcePet_length(t *testing.T) { +func TestAccResourcePet_Length(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testAccResourcePet_length, + Config: `resource "random_pet" "pet_1" { + length = 4 + }`, Check: resource.ComposeTestCheckFunc( - testAccResourcePetLength("random_pet.pet_1", "-", 4), + resource.TestCheckResourceAttrWith("random_pet.pet_1", "id", testCheckPetLen("-", 4)), ), }, }, }) } -func TestAccResourcePet_prefix(t *testing.T) { +func TestAccResourcePet_Prefix(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testAccResourcePet_prefix, + Config: `resource "random_pet" "pet_1" { + prefix = "consul" + }`, Check: resource.ComposeTestCheckFunc( - testAccResourcePetLength("random_pet.pet_1", "-", 3), - resource.TestMatchResourceAttr( - "random_pet.pet_1", "id", regexp.MustCompile("^consul-")), + resource.TestCheckResourceAttrWith("random_pet.pet_1", "id", testCheckPetLen("-", 3)), + resource.TestMatchResourceAttr("random_pet.pet_1", "id", regexp.MustCompile("^consul-")), ), }, }, }) } -func TestAccResourcePet_separator(t *testing.T) { +func TestAccResourcePet_Separator(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testAccResourcePet_separator, + Config: `resource "random_pet" "pet_1" { + length = 3 + separator = "_" + }`, Check: resource.ComposeTestCheckFunc( - testAccResourcePetLength("random_pet.pet_1", "_", 3), + resource.TestCheckResourceAttrWith("random_pet.pet_1", "id", testCheckPetLen("_", 3)), ), }, }, }) } -// nolint:unparam -func testAccResourcePetLength(id string, separator string, length int) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[id] - if !ok { - return fmt.Errorf("Not found: %s", id) - } - if rs.Primary.ID == "" { - return fmt.Errorf("No ID is set") - } +func TestAccResourcePet_UpgradeFromVersion3_3_2(t *testing.T) { + resource.Test(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ExternalProviders: providerVersion332(), + Config: `resource "random_pet" "pet_1" { + prefix = "consul" + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrWith("random_pet.pet_1", "id", testCheckPetLen("-", 3)), + resource.TestMatchResourceAttr("random_pet.pet_1", "id", regexp.MustCompile("^consul-")), + ), + }, + { + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Config: `resource "random_pet" "pet_1" { + prefix = "consul" + }`, + PlanOnly: true, + }, + { + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Config: `resource "random_pet" "pet_1" { + prefix = "consul" + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrWith("random_pet.pet_1", "id", testCheckPetLen("-", 3)), + resource.TestMatchResourceAttr("random_pet.pet_1", "id", regexp.MustCompile("^consul-")), + ), + }, + }, + }) +} + +func testCheckPetLen(separator string, expectedLen int) func(input string) error { + return func(input string) error { + petNameParts := strings.Split(input, separator) - petParts := strings.Split(rs.Primary.ID, separator) - if len(petParts) != length { - return fmt.Errorf("Length does not match") + if len(petNameParts) != expectedLen { + return fmt.Errorf("expected length %d, actual length %d", expectedLen, len(petNameParts)) } return nil } } - -const testAccResourcePet_basic = ` -resource "random_pet" "pet_1" { -} -` - -const testAccResourcePet_length = ` -resource "random_pet" "pet_1" { - length = 4 -} -` -const testAccResourcePet_prefix = ` -resource "random_pet" "pet_1" { - prefix = "consul" -} -` - -const testAccResourcePet_separator = ` -resource "random_pet" "pet_1" { - length = 3 - separator = "_" -} -` diff --git a/internal/provider/resource_shuffle.go b/internal/provider/resource_shuffle.go index ccf04932..433cc0d1 100644 --- a/internal/provider/resource_shuffle.go +++ b/internal/provider/resource_shuffle.go @@ -3,27 +3,34 @@ package provider import ( "context" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/terraform-providers/terraform-provider-random/internal/random" ) -func resourceShuffle() *schema.Resource { - return &schema.Resource{ +var _ tfsdk.ResourceType = (*shuffleResourceType)(nil) + +type shuffleResourceType struct{} + +func (r *shuffleResourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{ Description: "The resource `random_shuffle` generates a random permutation of a list of strings " + "given as an argument.", - CreateContext: CreateShuffle, - ReadContext: schema.NoopContext, - DeleteContext: RemoveResourceFromState, - - Schema: map[string]*schema.Schema{ + Attributes: map[string]tfsdk.Attribute{ "keepers": { Description: "Arbitrary map of values that, when changed, will trigger recreation of " + "resource. See [the main provider documentation](../index.html) for more information.", - Type: schema.TypeMap, + Type: types.MapType{ + ElemType: types.StringType, + }, Optional: true, - ForceNew: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, }, - "seed": { Description: "Arbitrary string with which to seed the random number generator, in order to " + "produce less-volatile permutations of the list.\n" + @@ -31,83 +38,143 @@ func resourceShuffle() *schema.Resource { "**Important:** Even with an identical seed, it is not guaranteed that the same permutation " + "will be produced across different versions of Terraform. This argument causes the " + "result to be *less volatile*, but not fixed for all time.", - Type: schema.TypeString, + Type: types.StringType, Optional: true, - ForceNew: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, }, - "input": { Description: "The list of strings to shuffle.", - Type: schema.TypeList, - Required: true, - ForceNew: true, - Elem: &schema.Schema{ - Type: schema.TypeString, + Type: types.ListType{ + ElemType: types.StringType, + }, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), }, }, - "result_count": { Description: "The number of results to return. Defaults to the number of items in the " + "`input` list. If fewer items are requested, some elements will be excluded from the " + "result. If more items are requested, items will be repeated in the result but not more " + "frequently than the number of items in the input list.", - Type: schema.TypeInt, + Type: types.Int64Type, Optional: true, - ForceNew: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, }, - "result": { Description: "Random permutation of the list of strings given in `input`.", - Type: schema.TypeList, - Computed: true, - Elem: &schema.Schema{ - Type: schema.TypeString, + Type: types.ListType{ + ElemType: types.StringType, }, + Computed: true, }, - "id": { Description: "A static value used internally by Terraform, this should not be referenced in configurations.", - Type: schema.TypeString, + Type: types.StringType, Computed: true, }, }, - } + }, nil +} + +func (r *shuffleResourceType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return &shuffleResource{}, nil } -func CreateShuffle(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - input := d.Get("input").([]interface{}) - seed := d.Get("seed").(string) +var _ tfsdk.Resource = (*shuffleResource)(nil) + +type shuffleResource struct{} + +func (r *shuffleResource) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { + var plan shuffleModelV0 + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + input := plan.Input + seed := plan.Seed.Value + resultCount := plan.ResultCount.Value - resultCount := d.Get("result_count").(int) if resultCount == 0 { - resultCount = len(input) + resultCount = int64(len(input.Elems)) } - result := make([]interface{}, 0, resultCount) - if len(input) > 0 { - rand := NewRand(seed) + result := make([]attr.Value, 0, resultCount) + + if len(input.Elems) > 0 { + rand := random.NewRand(seed) // Keep producing permutations until we fill our result Batches: for { - perm := rand.Perm(len(input)) + perm := rand.Perm(len(input.Elems)) for _, i := range perm { - result = append(result, input[i]) + result = append(result, input.Elems[i]) - if len(result) >= resultCount { + if int64(len(result)) >= resultCount { break Batches } } } + } + s := shuffleModelV0{ + ID: types.String{Value: "-"}, + Keepers: plan.Keepers, + Input: plan.Input, + Result: types.List{ + Unknown: false, + Null: false, + Elems: result, + ElemType: types.StringType, + }, } - d.SetId("-") + if plan.Seed.Null { + s.Seed.Null = true + } else { + s.Seed.Value = seed + } - if err := d.Set("result", result); err != nil { - return diag.Errorf("error setting result: %s", err) + if plan.ResultCount.Null { + s.ResultCount.Null = true + } else { + s.ResultCount.Value = resultCount } - return nil + diags = resp.State.Set(ctx, s) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read does not need to perform any operations as the state in ReadResourceResponse is already populated. +func (r *shuffleResource) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { +} + +// Update is intentionally left blank as all required and optional attributes force replacement of the resource +// through the RequiresReplace AttributePlanModifier. +func (r *shuffleResource) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { +} + +// Delete does not need to explicitly call resp.State.RemoveResource() as this is automatically handled by the +// [framework](https://github.com/hashicorp/terraform-plugin-framework/pull/301). +func (r *shuffleResource) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { +} + +type shuffleModelV0 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + Seed types.String `tfsdk:"seed"` + Input types.List `tfsdk:"input"` + ResultCount types.Int64 `tfsdk:"result_count"` + Result types.List `tfsdk:"result"` } diff --git a/internal/provider/resource_shuffle_test.go b/internal/provider/resource_shuffle_test.go index f796a006..a5670e5d 100644 --- a/internal/provider/resource_shuffle_test.go +++ b/internal/provider/resource_shuffle_test.go @@ -2,11 +2,9 @@ package provider import ( "fmt" - "strconv" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) // These results are current as of Go 1.6. The Go @@ -18,161 +16,167 @@ import ( // document them when they arise, but the docs for this // resource specifically warn that results are not // guaranteed consistent across Terraform releases. -func TestAccResourceShuffleDefault(t *testing.T) { +func TestAccResourceShuffle(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testAccResourceShuffleConfigDefault, + Config: `resource "random_shuffle" "default_length" { + input = ["a", "b", "c", "d", "e"] + seed = "-" + }`, Check: resource.ComposeTestCheckFunc( - testAccResourceShuffleCheck( - "random_shuffle.default_length", - []string{"a", "c", "b", "e", "d"}, - ), + resource.TestCheckResourceAttrWith("random_shuffle.default_length", "result.#", testAccResourceShuffleCheckLength("5")), + resource.TestCheckResourceAttr("random_shuffle.default_length", "result.0", "a"), + resource.TestCheckResourceAttr("random_shuffle.default_length", "result.1", "c"), + resource.TestCheckResourceAttr("random_shuffle.default_length", "result.2", "b"), + resource.TestCheckResourceAttr("random_shuffle.default_length", "result.3", "e"), + resource.TestCheckResourceAttr("random_shuffle.default_length", "result.4", "d"), ), }, }, }) } -func TestAccResourceShuffleShorter(t *testing.T) { +func TestAccResourceShuffle_Shorter(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testAccResourceShuffleConfigShorter, + Config: `resource "random_shuffle" "shorter_length" { + input = ["a", "b", "c", "d", "e"] + seed = "-" + result_count = 3 + }`, Check: resource.ComposeTestCheckFunc( - testAccResourceShuffleCheck( - "random_shuffle.shorter_length", - []string{"a", "c", "b"}, - ), + resource.TestCheckResourceAttrWith("random_shuffle.shorter_length", "result.#", testAccResourceShuffleCheckLength("3")), + resource.TestCheckResourceAttr("random_shuffle.shorter_length", "result.0", "a"), + resource.TestCheckResourceAttr("random_shuffle.shorter_length", "result.1", "c"), + resource.TestCheckResourceAttr("random_shuffle.shorter_length", "result.2", "b"), ), }, }, }) } -func TestAccResourceShuffleLonger(t *testing.T) { +func TestAccResourceShuffle_Longer(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testAccResourceShuffleConfigLonger, + Config: `resource "random_shuffle" "longer_length" { + input = ["a", "b", "c", "d", "e"] + seed = "-" + result_count = 12 + }`, Check: resource.ComposeTestCheckFunc( - testAccResourceShuffleCheck( - "random_shuffle.longer_length", - []string{"a", "c", "b", "e", "d", "a", "e", "d", "c", "b", "a", "b"}, - ), + resource.TestCheckResourceAttrWith("random_shuffle.longer_length", "result.#", testAccResourceShuffleCheckLength("12")), + resource.TestCheckResourceAttr("random_shuffle.longer_length", "result.0", "a"), + resource.TestCheckResourceAttr("random_shuffle.longer_length", "result.1", "c"), + resource.TestCheckResourceAttr("random_shuffle.longer_length", "result.2", "b"), + resource.TestCheckResourceAttr("random_shuffle.longer_length", "result.3", "e"), + resource.TestCheckResourceAttr("random_shuffle.longer_length", "result.4", "d"), + resource.TestCheckResourceAttr("random_shuffle.longer_length", "result.5", "a"), + resource.TestCheckResourceAttr("random_shuffle.longer_length", "result.6", "e"), + resource.TestCheckResourceAttr("random_shuffle.longer_length", "result.7", "d"), + resource.TestCheckResourceAttr("random_shuffle.longer_length", "result.8", "c"), + resource.TestCheckResourceAttr("random_shuffle.longer_length", "result.9", "b"), + resource.TestCheckResourceAttr("random_shuffle.longer_length", "result.10", "a"), + resource.TestCheckResourceAttr("random_shuffle.longer_length", "result.11", "b"), ), }, }, }) } -func TestAccResourceShuffleEmpty(t *testing.T) { +func TestAccResourceShuffle_Empty(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testAccResourceShuffleConfigEmpty, + Config: `resource "random_shuffle" "empty_length" { + input = [] + seed = "-" + result_count = 12 + }`, Check: resource.ComposeTestCheckFunc( - testAccResourceShuffleCheck( - "random_shuffle.empty_length", - []string{}, - ), + resource.TestCheckResourceAttrWith("random_shuffle.empty_length", "result.#", testAccResourceShuffleCheckLength("0")), ), }, }, }) } -func TestAccResourceShuffleOne(t *testing.T) { +func TestAccResourceShuffle_One(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testAccResourceShuffleConfigOne, + Config: `resource "random_shuffle" "one_length" { + input = ["a"] + seed = "-" + result_count = 1 + }`, Check: resource.ComposeTestCheckFunc( - testAccResourceShuffleCheck( - "random_shuffle.one_length", - []string{"a"}, - ), + resource.TestCheckResourceAttrWith("random_shuffle.one_length", "result.#", testAccResourceShuffleCheckLength("1")), + resource.TestCheckResourceAttr("random_shuffle.one_length", "result.0", "a"), ), }, }, }) } -func testAccResourceShuffleCheck(id string, wants []string) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[id] - if !ok { - return fmt.Errorf("Not found: %s", id) - } - if rs.Primary.ID == "" { - return fmt.Errorf("No ID is set") - } - - attrs := rs.Primary.Attributes - - gotLen := attrs["result.#"] - wantLen := strconv.Itoa(len(wants)) - if gotLen != wantLen { - return fmt.Errorf("got %s result items; want %s", gotLen, wantLen) - } +func TestAccResourceShuffle_UpgradeFromVersion3_3_2(t *testing.T) { + resource.Test(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ExternalProviders: providerVersion332(), + Config: `resource "random_shuffle" "default_length" { + input = ["a", "b", "c", "d", "e"] + seed = "-" + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrWith("random_shuffle.default_length", "result.#", testAccResourceShuffleCheckLength("5")), + resource.TestCheckResourceAttr("random_shuffle.default_length", "result.0", "a"), + resource.TestCheckResourceAttr("random_shuffle.default_length", "result.1", "c"), + resource.TestCheckResourceAttr("random_shuffle.default_length", "result.2", "b"), + resource.TestCheckResourceAttr("random_shuffle.default_length", "result.3", "e"), + resource.TestCheckResourceAttr("random_shuffle.default_length", "result.4", "d"), + ), + }, + { + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Config: `resource "random_shuffle" "default_length" { + input = ["a", "b", "c", "d", "e"] + seed = "-" + }`, + PlanOnly: true, + }, + { + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Config: `resource "random_shuffle" "default_length" { + input = ["a", "b", "c", "d", "e"] + seed = "-" + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrWith("random_shuffle.default_length", "result.#", testAccResourceShuffleCheckLength("5")), + resource.TestCheckResourceAttr("random_shuffle.default_length", "result.0", "a"), + resource.TestCheckResourceAttr("random_shuffle.default_length", "result.1", "c"), + resource.TestCheckResourceAttr("random_shuffle.default_length", "result.2", "b"), + resource.TestCheckResourceAttr("random_shuffle.default_length", "result.3", "e"), + resource.TestCheckResourceAttr("random_shuffle.default_length", "result.4", "d"), + ), + }, + }, + }) +} - for i, want := range wants { - key := fmt.Sprintf("result.%d", i) - if got := attrs[key]; got != want { - return fmt.Errorf("index %d is %q; want %q", i, got, want) - } +func testAccResourceShuffleCheckLength(expectedLength string) func(input string) error { + return func(input string) error { + if input != expectedLength { + return fmt.Errorf("got length %s; expected length %s", input, expectedLength) } return nil } } - -const ( - testAccResourceShuffleConfigDefault = ` -resource "random_shuffle" "default_length" { - input = ["a", "b", "c", "d", "e"] - seed = "-" -}` - - testAccResourceShuffleConfigShorter = ` -resource "random_shuffle" "shorter_length" { - input = ["a", "b", "c", "d", "e"] - seed = "-" - result_count = 3 -} -` - - testAccResourceShuffleConfigLonger = ` -resource "random_shuffle" "longer_length" { - input = ["a", "b", "c", "d", "e"] - seed = "-" - result_count = 12 -} -` - - testAccResourceShuffleConfigEmpty = ` -resource "random_shuffle" "empty_length" { - input = [] - seed = "-" - result_count = 12 -} -` - - testAccResourceShuffleConfigOne = ` -resource "random_shuffle" "one_length" { - input = ["a"] - seed = "-" - result_count = 1 -} -` -) diff --git a/internal/provider/resource_string.go b/internal/provider/resource_string.go index 735eb899..74028153 100644 --- a/internal/provider/resource_string.go +++ b/internal/provider/resource_string.go @@ -2,22 +2,26 @@ package provider import ( "context" - "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/terraform-providers/terraform-provider-random/internal/diagnostics" + "github.com/terraform-providers/terraform-provider-random/internal/planmodifiers" + "github.com/terraform-providers/terraform-provider-random/internal/random" + "github.com/terraform-providers/terraform-provider-random/internal/validators" ) -// resourceString and resourcePassword both use the same set of CustomizeDiffFunc(s) in order to handle the deprecation -// of the `number` attribute and the simultaneous addition of the `numeric` attribute. planDefaultIfAllNull handles -// ensuring that both `number` and `numeric` default to `true` when they are both absent from config. -// planSyncIfChange handles keeping number and numeric in-sync when either one has been changed. -func resourceString() *schema.Resource { - customizeDiffFuncs := planDefaultIfAllNull(true, "number", "numeric") - customizeDiffFuncs = append(customizeDiffFuncs, planSyncIfChange("number", "numeric")) - customizeDiffFuncs = append(customizeDiffFuncs, planSyncIfChange("numeric", "number")) +var _ tfsdk.ResourceType = (*stringResourceType)(nil) + +type stringResourceType struct{} - return &schema.Resource{ +func (r stringResourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{ + Version: 2, Description: "The resource `random_string` generates a random permutation of alphanumeric " + "characters and optionally special characters.\n" + "\n" + @@ -26,61 +30,472 @@ func resourceString() *schema.Resource { "Historically this resource's intended usage has been ambiguous as the original example used " + "it in a password. For backwards compatibility it will continue to exist. For unique ids please " + "use [random_id](id.html), for sensitive random values please use [random_password](password.html).", - CreateContext: createStringFunc(false), - ReadContext: readNil, - DeleteContext: RemoveResourceFromState, - // MigrateState is deprecated but the implementation is being left in place as per the - // [SDK documentation](https://github.com/hashicorp/terraform-plugin-sdk/blob/main/helper/schema/resource.go#L91). - MigrateState: resourceRandomStringMigrateState, - SchemaVersion: 2, - Schema: stringSchemaV2(), - Importer: &schema.ResourceImporter{ - StateContext: importStringFunc, - }, - StateUpgraders: []schema.StateUpgrader{ - { - Version: 1, - Type: resourceStringV1().CoreConfigSchema().ImpliedType(), - Upgrade: resourcePasswordStringStateUpgradeV1, + Attributes: map[string]tfsdk.Attribute{ + "keepers": { + Description: "Arbitrary map of values that, when changed, will trigger recreation of " + + "resource. See [the main provider documentation](../index.html) for more information.", + Type: types.MapType{ + ElemType: types.StringType, + }, + Optional: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, + }, + + "length": { + Description: "The length of the string desired. The minimum value for length is 1 and, length " + + "must also be >= (`min_upper` + `min_lower` + `min_numeric` + `min_special`).", + Type: types.Int64Type, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, + Validators: []tfsdk.AttributeValidator{ + int64validator.AtLeast(1), + validators.NewIntIsAtLeastSumOfValidator( + tftypes.NewAttributePath().WithAttributeName("min_upper"), + tftypes.NewAttributePath().WithAttributeName("min_lower"), + tftypes.NewAttributePath().WithAttributeName("min_numeric"), + tftypes.NewAttributePath().WithAttributeName("min_special"), + ), + }, + }, + + "special": { + Description: "Include special characters in the result. These are `!@#$%&*()-_=+[]{}<>:?`. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "upper": { + Description: "Include uppercase alphabet characters in the result. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "lower": { + Description: "Include lowercase alphabet characters in the result. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "numeric": { + Description: "Include numeric characters in the result. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_numeric": { + Description: "Minimum number of numeric characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_upper": { + Description: "Minimum number of uppercase alphabet characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_lower": { + Description: "Minimum number of lowercase alphabet characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_special": { + Description: "Minimum number of special characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "override_special": { + Description: "Supply your own list of special characters to use for string generation. This " + + "overrides the default character list in the special argument. The `special` argument must " + + "still be set to true for any overwritten characters to be used in generation.", + Type: types.StringType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, + }, + + "result": { + Description: "The generated random string.", + Type: types.StringType, + Computed: true, + }, + + "id": { + Description: "The generated random string.", + Computed: true, + Type: types.StringType, }, }, - CustomizeDiff: customdiff.All( - customizeDiffFuncs..., - ), - } + }, nil +} + +func (r stringResourceType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return &stringResource{}, nil } -func importStringFunc(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - for k, v := range stringSchemaV2() { - if v.Default == nil { - continue - } - if err := d.Set(k, v.Default); err != nil { - return nil, fmt.Errorf("error setting %s: %w", k, err) - } +var ( + _ tfsdk.Resource = (*stringResource)(nil) + _ tfsdk.ResourceWithImportState = (*stringResource)(nil) + _ tfsdk.ResourceWithUpgradeState = (*stringResource)(nil) +) + +type stringResource struct{} + +func (r *stringResource) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { + var plan stringModelV2 + + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } - for _, key := range []string{"number", "numeric"} { - if err := d.Set(key, true); err != nil { - return nil, fmt.Errorf("error setting %s: %w", key, err) - } + params := random.StringParams{ + Length: plan.Length.Value, + Upper: plan.Upper.Value, + MinUpper: plan.MinUpper.Value, + Lower: plan.Lower.Value, + MinLower: plan.MinLower.Value, + Numeric: plan.Numeric.Value, + MinNumeric: plan.MinNumeric.Value, + Special: plan.Special.Value, + MinSpecial: plan.MinSpecial.Value, + OverrideSpecial: plan.OverrideSpecial.Value, } - val := d.Id() + result, err := random.CreateString(params) + if err != nil { + resp.Diagnostics.Append(diagnostics.RandomReadError(err.Error())...) + return + } - if err := d.Set("result", val); err != nil { - return nil, fmt.Errorf("error setting result: %w", err) + state := stringModelV2{ + ID: types.String{Value: string(result)}, + Keepers: plan.Keepers, + Length: types.Int64{Value: plan.Length.Value}, + Special: types.Bool{Value: plan.Special.Value}, + Upper: types.Bool{Value: plan.Upper.Value}, + Lower: types.Bool{Value: plan.Lower.Value}, + Numeric: types.Bool{Value: plan.Numeric.Value}, + MinNumeric: types.Int64{Value: plan.MinNumeric.Value}, + MinUpper: types.Int64{Value: plan.MinUpper.Value}, + MinLower: types.Int64{Value: plan.MinLower.Value}, + MinSpecial: types.Int64{Value: plan.MinSpecial.Value}, + OverrideSpecial: types.String{Value: plan.OverrideSpecial.Value}, + Result: types.String{Value: string(result)}, } - if err := d.Set("length", len(val)); err != nil { - return nil, fmt.Errorf("error setting length: %w", err) + diags = resp.State.Set(ctx, state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } +} - return []*schema.ResourceData{d}, nil +// Read does not need to perform any operations as the state in ReadResourceResponse is already populated. +func (r *stringResource) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { } -func resourceStringV1() *schema.Resource { - return &schema.Resource{ - Schema: stringSchemaV1(), +// Update is intentionally left blank as all required and optional attributes force replacement of the resource +// through the RequiresReplace AttributePlanModifier. +func (r *stringResource) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { +} + +// Delete does not need to explicitly call resp.State.RemoveResource() as this is automatically handled by the +// [framework](https://github.com/hashicorp/terraform-plugin-framework/pull/301). +func (r *stringResource) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { +} + +func (r *stringResource) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { + id := req.ID + + state := stringModelV2{ + ID: types.String{Value: id}, + Result: types.String{Value: id}, + Length: types.Int64{Value: int64(len(id))}, + Special: types.Bool{Value: true}, + Upper: types.Bool{Value: true}, + Lower: types.Bool{Value: true}, + Numeric: types.Bool{Value: true}, + MinSpecial: types.Int64{Value: 0}, + MinUpper: types.Int64{Value: 0}, + MinLower: types.Int64{Value: 0}, + MinNumeric: types.Int64{Value: 0}, + } + + state.Keepers.ElemType = types.StringType + + diags := resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } } + +func (r *stringResource) UpgradeState(context.Context) map[int64]tfsdk.ResourceStateUpgrader { + schemaV1 := tfsdk.Schema{ + Version: 1, + Description: "The resource `random_string` generates a random permutation of alphanumeric " + + "characters and optionally special characters.\n" + + "\n" + + "This resource *does* use a cryptographic random number generator.\n" + + "\n" + + "Historically this resource's intended usage has been ambiguous as the original example used " + + "it in a password. For backwards compatibility it will continue to exist. For unique ids please " + + "use [random_id](id.html), for sensitive random values please use [random_password](password.html).", + Attributes: map[string]tfsdk.Attribute{ + "keepers": { + Description: "Arbitrary map of values that, when changed, will trigger recreation of " + + "resource. See [the main provider documentation](../index.html) for more information.", + Type: types.MapType{ + ElemType: types.StringType, + }, + Optional: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, + }, + + "length": { + Description: "The length of the string desired. The minimum value for length is 1 and, length " + + "must also be >= (`min_upper` + `min_lower` + `min_numeric` + `min_special`).", + Type: types.Int64Type, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, + Validators: []tfsdk.AttributeValidator{ + int64validator.AtLeast(1), + validators.NewIntIsAtLeastSumOfValidator( + tftypes.NewAttributePath().WithAttributeName("min_upper"), + tftypes.NewAttributePath().WithAttributeName("min_lower"), + tftypes.NewAttributePath().WithAttributeName("min_numeric"), + tftypes.NewAttributePath().WithAttributeName("min_special"), + ), + }, + }, + + "special": { + Description: "Include special characters in the result. These are `!@#$%&*()-_=+[]{}<>:?`. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "upper": { + Description: "Include uppercase alphabet characters in the result. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "lower": { + Description: "Include lowercase alphabet characters in the result. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "number": { + Description: "Include numeric characters in the result. Default value is `true`.", + Type: types.BoolType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Bool{Value: true}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_numeric": { + Description: "Minimum number of numeric characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_upper": { + Description: "Minimum number of uppercase alphabet characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_lower": { + Description: "Minimum number of lowercase alphabet characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "min_special": { + Description: "Minimum number of special characters in the result. Default value is `0`.", + Type: types.Int64Type, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + planmodifiers.DefaultValue(types.Int64{Value: 0}), + planmodifiers.RequiresReplace(), + }, + }, + + "override_special": { + Description: "Supply your own list of special characters to use for string generation. This " + + "overrides the default character list in the special argument. The `special` argument must " + + "still be set to true for any overwritten characters to be used in generation.", + Type: types.StringType, + Optional: true, + Computed: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, + }, + + "result": { + Description: "The generated random string.", + Type: types.StringType, + Computed: true, + }, + + "id": { + Description: "The generated random string.", + Computed: true, + Type: types.StringType, + }, + }, + } + + return map[int64]tfsdk.ResourceStateUpgrader{ + 1: { + PriorSchema: &schemaV1, + StateUpgrader: upgradeStringStateV1toV2, + }, + } +} + +func upgradeStringStateV1toV2(ctx context.Context, req tfsdk.UpgradeResourceStateRequest, resp *tfsdk.UpgradeResourceStateResponse) { + type modelV1 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + Length types.Int64 `tfsdk:"length"` + Special types.Bool `tfsdk:"special"` + Upper types.Bool `tfsdk:"upper"` + Lower types.Bool `tfsdk:"lower"` + Number types.Bool `tfsdk:"number"` + MinNumeric types.Int64 `tfsdk:"min_numeric"` + MinUpper types.Int64 `tfsdk:"min_upper"` + MinLower types.Int64 `tfsdk:"min_lower"` + MinSpecial types.Int64 `tfsdk:"min_special"` + OverrideSpecial types.String `tfsdk:"override_special"` + Result types.String `tfsdk:"result"` + } + + var stringDataV1 modelV1 + + resp.Diagnostics.Append(req.State.Get(ctx, &stringDataV1)...) + if resp.Diagnostics.HasError() { + return + } + + stringDataV2 := stringModelV2{ + Keepers: stringDataV1.Keepers, + Length: stringDataV1.Length, + Special: stringDataV1.Special, + Upper: stringDataV1.Upper, + Lower: stringDataV1.Lower, + Numeric: stringDataV1.Number, + MinNumeric: stringDataV1.MinNumeric, + MinLower: stringDataV1.MinLower, + MinSpecial: stringDataV1.MinSpecial, + OverrideSpecial: stringDataV1.OverrideSpecial, + Result: stringDataV1.Result, + ID: stringDataV1.ID, + } + + diags := resp.State.Set(ctx, stringDataV2) + resp.Diagnostics.Append(diags...) +} + +type stringModelV2 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + Length types.Int64 `tfsdk:"length"` + Special types.Bool `tfsdk:"special"` + Upper types.Bool `tfsdk:"upper"` + Lower types.Bool `tfsdk:"lower"` + Numeric types.Bool `tfsdk:"numeric"` + MinNumeric types.Int64 `tfsdk:"min_numeric"` + MinUpper types.Int64 `tfsdk:"min_upper"` + MinLower types.Int64 `tfsdk:"min_lower"` + MinSpecial types.Int64 `tfsdk:"min_special"` + OverrideSpecial types.String `tfsdk:"override_special"` + Result types.String `tfsdk:"result"` +} diff --git a/internal/provider/resource_string_migration.go b/internal/provider/resource_string_migration.go deleted file mode 100644 index ad565c17..00000000 --- a/internal/provider/resource_string_migration.go +++ /dev/null @@ -1,50 +0,0 @@ -package provider - -import ( - "fmt" - "log" - - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" -) - -func resourceRandomStringMigrateState( - v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { - switch v { - case 0: - log.Println("[INFO] Found random string state v0; migrating to v1") - return migrateStringStateV0toV1(is) - default: - return is, fmt.Errorf("Unexpected schema version: %d", v) - } -} - -func redactAttributes(is *terraform.InstanceState) map[string]string { - redactedAttributes := make(map[string]string) - for k, v := range is.Attributes { - redactedAttributes[k] = v - if k == "id" || k == "result" { - redactedAttributes[k] = "" - } - } - return redactedAttributes -} - -func migrateStringStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { - if is.Empty() { - log.Println("[DEBUG] Empty InstanceState; nothing to migrate.") - return is, nil - } - - log.Printf("[DEBUG] Random String Attributes before Migration: %#v", redactAttributes(is)) - - keys := []string{"min_numeric", "min_upper", "min_lower", "min_special"} - for _, k := range keys { - if v := is.Attributes[k]; v == "" { - is.Attributes[k] = "0" - } - } - - log.Printf("[DEBUG] Random String Attributes after State Migration: %#v", redactAttributes(is)) - - return is, nil -} diff --git a/internal/provider/resource_string_migration_test.go b/internal/provider/resource_string_migration_test.go deleted file mode 100644 index e5172737..00000000 --- a/internal/provider/resource_string_migration_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package provider - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" -) - -func TestResourceStringMigrateState(t *testing.T) { - cases := map[string]struct { - StateVersion int - ID string - InputAttributes map[string]string - ExpectedAttributes map[string]string - Meta interface{} - }{ - "v0_1_simple": { - StateVersion: 0, - ID: "some_id", - InputAttributes: map[string]string{ - "result": "foo", - "id": "foo", - "length": "3", - }, - ExpectedAttributes: map[string]string{ - "result": "foo", - "id": "foo", - "length": "3", - "min_numeric": "0", - "min_special": "0", - "min_lower": "0", - "min_upper": "0", - }, - }, - "v0_1_special": { - StateVersion: 0, - ID: "some_id", - InputAttributes: map[string]string{ - "result": "foo", - "id": "foo", - "special": "false", - "length": "3", - "override_special": "!@", - }, - ExpectedAttributes: map[string]string{ - "result": "foo", - "id": "foo", - "special": "false", - "length": "3", - "override_special": "!@", - "min_numeric": "0", - "min_special": "0", - "min_lower": "0", - "min_upper": "0", - }, - }, - } - - for tn, tc := range cases { - is := &terraform.InstanceState{ - ID: tc.ID, - Attributes: tc.InputAttributes, - } - is, err := resourceRandomStringMigrateState(tc.StateVersion, is, tc.Meta) - - if err != nil { - t.Fatalf("bad: %s, err: %#v", tn, err) - } - - for k, v := range tc.ExpectedAttributes { - actual := is.Attributes[k] - if actual != v { - t.Fatalf("Bad Random String Migration for %q: %q\n\n expected: %q", k, actual, v) - } - } - } -} diff --git a/internal/provider/resource_string_test.go b/internal/provider/resource_string_test.go index fc2acaea..19fcdb36 100644 --- a/internal/provider/resource_string_test.go +++ b/internal/provider/resource_string_test.go @@ -6,24 +6,18 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -type customLens struct { - customLen int -} - func TestAccResourceString(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testAccResourceStringBasic, + Config: `resource "random_string" "basic" { + length = 12 + }`, Check: resource.ComposeTestCheckFunc( - testAccResourceStringCheck("random_string.basic", &customLens{ - customLen: 12, - }), + resource.TestCheckResourceAttrWith("random_string.basic", "result", testCheckLen(12)), ), }, { @@ -35,109 +29,60 @@ func TestAccResourceString(t *testing.T) { }) } -func TestAccResourceStringOverride(t *testing.T) { - resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, - Steps: []resource.TestStep{ - { - Config: testAccResourceStringOverride, - Check: resource.ComposeTestCheckFunc( - testAccResourceStringCheck("random_string.override", &customLens{ - customLen: 4, - }), - patternMatch("random_string.override", "!!!!"), - ), - }, - }, - }) -} - -func TestAccResourceStringMin(t *testing.T) { +func TestAccResourceString_Override(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testAccResourceStringMin, + Config: `resource "random_string" "override" { + length = 4 + override_special = "!" + lower = false + upper = false + numeric = false + }`, Check: resource.ComposeTestCheckFunc( - testAccResourceStringCheck("random_string.min", &customLens{ - customLen: 12, - }), - regexMatch("random_string.min", regexp.MustCompile(`([a-z])`), 2), - regexMatch("random_string.min", regexp.MustCompile(`([A-Z])`), 3), - regexMatch("random_string.min", regexp.MustCompile(`([0-9])`), 4), - regexMatch("random_string.min", regexp.MustCompile(`([!#@])`), 1), + resource.TestCheckResourceAttrWith("random_string.override", "result", testCheckLen(4)), + resource.TestCheckResourceAttr("random_string.override", "result", "!!!!"), ), }, }, }) } -func TestAccResourceString_UpdateNumberAndNumeric(t *testing.T) { - t.Parallel() +func TestAccResourceString_Min(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: `resource "random_string" "default" { - length = 12 - }`, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("random_string.default", "number", "true"), - resource.TestCheckResourceAttr("random_string.default", "numeric", "true"), - ), - }, - { - Config: `resource "random_string" "default" { - length = 12 - number = false - }`, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("random_string.default", "number", "false"), - resource.TestCheckResourceAttr("random_string.default", "numeric", "false"), - ), - }, - { - Config: `resource "random_string" "default" { + Config: `resource "random_string" "min" { length = 12 - numeric = true + override_special = "!#@" + min_lower = 2 + min_upper = 3 + min_special = 1 + min_numeric = 4 }`, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("random_string.default", "number", "true"), - resource.TestCheckResourceAttr("random_string.default", "numeric", "true"), - ), - }, - { - Config: `resource "random_string" "default" { - length = 12 - numeric = false - }`, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("random_string.default", "number", "false"), - resource.TestCheckResourceAttr("random_string.default", "numeric", "false"), - ), - }, - { - Config: `resource "random_string" "default" { - length = 12 - }`, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("random_string.default", "number", "true"), - resource.TestCheckResourceAttr("random_string.default", "numeric", "true"), + resource.TestCheckResourceAttrWith("random_string.min", "result", testCheckLen(12)), + resource.TestMatchResourceAttr("random_string.min", "result", regexp.MustCompile(`([a-z].*){2,}`)), + resource.TestMatchResourceAttr("random_string.min", "result", regexp.MustCompile(`([A-Z].*){3,}`)), + resource.TestMatchResourceAttr("random_string.min", "result", regexp.MustCompile(`([0-9].*){4,}`)), + resource.TestMatchResourceAttr("random_string.min", "result", regexp.MustCompile(`([!#@].*)`)), ), }, }, }) } -// TestAccResourceString_StateUpgraders covers the state upgrade from V1 to V2. -// This includes the addition of numeric attribute. -func TestAccResourceString_StateUpgraders(t *testing.T) { +// TestAccResourceString_StateUpgradeV1toV2 covers the state upgrade from V1 to V2. +// This includes the deprecation and removal of `number` and the addition of `numeric` attributes. +// v3.2.0 was used as this is the last version before `number` was deprecated and `numeric` attribute +// was added. +func TestAccResourceString_StateUpgradeV1toV2(t *testing.T) { t.Parallel() - v1Cases := []struct { + cases := []struct { name string configBeforeUpgrade string configDuringUpgrade string @@ -145,72 +90,79 @@ func TestAccResourceString_StateUpgraders(t *testing.T) { afterStateUpgrade []resource.TestCheckFunc }{ { - name: "%s number is absent", + name: "number is absent before numeric is absent during", configBeforeUpgrade: `resource "random_string" "default" { length = 12 }`, + configDuringUpgrade: `resource "random_string" "default" { + length = 12 + }`, beforeStateUpgrade: []resource.TestCheckFunc{ resource.TestCheckResourceAttr("random_string.default", "number", "true"), resource.TestCheckNoResourceAttr("random_string.default", "numeric"), }, afterStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("random_string.default", "number", "true"), - resource.TestCheckResourceAttrPair("random_string.default", "number", "random_string.default", "numeric"), + resource.TestCheckResourceAttr("random_string.default", "numeric", "true"), + resource.TestCheckNoResourceAttr("random_string.default", "number"), }, }, { - name: "%s number is absent then true", + name: "number is absent before numeric is true during", configBeforeUpgrade: `resource "random_string" "default" { length = 12 }`, configDuringUpgrade: `resource "random_string" "default" { length = 12 - number = true + numeric = true }`, beforeStateUpgrade: []resource.TestCheckFunc{ resource.TestCheckResourceAttr("random_string.default", "number", "true"), resource.TestCheckNoResourceAttr("random_string.default", "numeric"), }, afterStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("random_string.default", "number", "true"), - resource.TestCheckResourceAttrPair("random_string.default", "number", "random_string.default", "numeric"), + resource.TestCheckResourceAttr("random_string.default", "numeric", "true"), + resource.TestCheckNoResourceAttr("random_string.default", "number"), }, }, { - name: "%s number is absent then false", + name: "number is absent before numeric is false during", configBeforeUpgrade: `resource "random_string" "default" { length = 12 }`, configDuringUpgrade: `resource "random_string" "default" { length = 12 - number = false + numeric = false }`, beforeStateUpgrade: []resource.TestCheckFunc{ resource.TestCheckResourceAttr("random_string.default", "number", "true"), resource.TestCheckNoResourceAttr("random_string.default", "numeric"), }, afterStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("random_string.default", "number", "false"), - resource.TestCheckResourceAttrPair("random_string.default", "number", "random_string.default", "numeric"), + resource.TestCheckResourceAttr("random_string.default", "numeric", "false"), + resource.TestCheckNoResourceAttr("random_string.default", "number"), }, }, { - name: "%s number is true", + name: "number is true before numeric is true during", configBeforeUpgrade: `resource "random_string" "default" { length = 12 number = true }`, + configDuringUpgrade: `resource "random_string" "default" { + length = 12 + numeric = true + }`, beforeStateUpgrade: []resource.TestCheckFunc{ resource.TestCheckResourceAttr("random_string.default", "number", "true"), resource.TestCheckNoResourceAttr("random_string.default", "numeric"), }, afterStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("random_string.default", "number", "true"), - resource.TestCheckResourceAttrPair("random_string.default", "number", "random_string.default", "numeric"), + resource.TestCheckResourceAttr("random_string.default", "numeric", "true"), + resource.TestCheckNoResourceAttr("random_string.default", "number"), }, }, { - name: "%s number is true then absent", + name: "number is true before numeric is absent during", configBeforeUpgrade: `resource "random_string" "default" { length = 12 number = true @@ -223,46 +175,50 @@ func TestAccResourceString_StateUpgraders(t *testing.T) { resource.TestCheckNoResourceAttr("random_string.default", "numeric"), }, afterStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("random_string.default", "number", "true"), - resource.TestCheckResourceAttrPair("random_string.default", "number", "random_string.default", "numeric"), + resource.TestCheckResourceAttr("random_string.default", "numeric", "true"), + resource.TestCheckNoResourceAttr("random_string.default", "number"), }, }, { - name: "%s number is true then false", + name: "number is true before numeric is false during", configBeforeUpgrade: `resource "random_string" "default" { length = 12 number = true }`, configDuringUpgrade: `resource "random_string" "default" { length = 12 - number = false + numeric = false }`, beforeStateUpgrade: []resource.TestCheckFunc{ resource.TestCheckResourceAttr("random_string.default", "number", "true"), resource.TestCheckNoResourceAttr("random_string.default", "numeric"), }, afterStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("random_string.default", "number", "false"), - resource.TestCheckResourceAttrPair("random_string.default", "number", "random_string.default", "numeric"), + resource.TestCheckResourceAttr("random_string.default", "numeric", "false"), + resource.TestCheckNoResourceAttr("random_string.default", "number"), }, }, { - name: "%s number is false", + name: "number is false before numeric is false during", configBeforeUpgrade: `resource "random_string" "default" { length = 12 number = false }`, + configDuringUpgrade: `resource "random_string" "default" { + length = 12 + numeric = false + }`, beforeStateUpgrade: []resource.TestCheckFunc{ resource.TestCheckResourceAttr("random_string.default", "number", "false"), resource.TestCheckNoResourceAttr("random_string.default", "numeric"), }, afterStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("random_string.default", "number", "false"), - resource.TestCheckResourceAttrPair("random_string.default", "number", "random_string.default", "numeric"), + resource.TestCheckResourceAttr("random_string.default", "numeric", "false"), + resource.TestCheckNoResourceAttr("random_string.default", "number"), }, }, { - name: "%s number is false then absent", + name: "number is false before numeric is absent during", configBeforeUpgrade: `resource "random_string" "default" { length = 12 number = false @@ -275,174 +231,161 @@ func TestAccResourceString_StateUpgraders(t *testing.T) { resource.TestCheckNoResourceAttr("random_string.default", "numeric"), }, afterStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("random_string.default", "number", "true"), - resource.TestCheckResourceAttrPair("random_string.default", "number", "random_string.default", "numeric"), + resource.TestCheckResourceAttr("random_string.default", "numeric", "true"), + resource.TestCheckNoResourceAttr("random_string.default", "number"), }, }, { - name: "%s number is false then true", + name: "number is false before numeric is true during", configBeforeUpgrade: `resource "random_string" "default" { length = 12 number = false }`, configDuringUpgrade: `resource "random_string" "default" { length = 12 - number = true + numeric = true }`, beforeStateUpgrade: []resource.TestCheckFunc{ resource.TestCheckResourceAttr("random_string.default", "number", "false"), resource.TestCheckNoResourceAttr("random_string.default", "numeric"), }, afterStateUpgrade: []resource.TestCheckFunc{ - resource.TestCheckResourceAttr("random_string.default", "number", "true"), - resource.TestCheckResourceAttrPair("random_string.default", "number", "random_string.default", "numeric"), + resource.TestCheckResourceAttr("random_string.default", "numeric", "true"), + resource.TestCheckNoResourceAttr("random_string.default", "number"), }, }, } - cases := map[string][]struct { - name string - configBeforeUpgrade string - configDuringUpgrade string - beforeStateUpgrade []resource.TestCheckFunc - afterStateUpgrade []resource.TestCheckFunc - }{ - "3.2.0": v1Cases, - } - - for providerVersion, v := range cases { - for _, c := range v { - name := fmt.Sprintf(c.name, providerVersion) - t.Run(name, func(t *testing.T) { - if c.configDuringUpgrade == "" { - c.configDuringUpgrade = c.configBeforeUpgrade - } - - resource.UnitTest(t, resource.TestCase{ - Steps: []resource.TestStep{ - { - ExternalProviders: map[string]resource.ExternalProvider{"random": { - VersionConstraint: providerVersion, - Source: "hashicorp/random", - }}, - Config: c.configBeforeUpgrade, - Check: resource.ComposeTestCheckFunc(c.beforeStateUpgrade...), - }, - { - ProviderFactories: testAccProviders, - Config: c.configDuringUpgrade, - Check: resource.ComposeTestCheckFunc(c.afterStateUpgrade...), - }, + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{"random": { + VersionConstraint: "3.2.0", + Source: "hashicorp/random", + }}, + Config: c.configBeforeUpgrade, + Check: resource.ComposeTestCheckFunc(c.beforeStateUpgrade...), }, - }) + { + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Config: c.configDuringUpgrade, + Check: resource.ComposeTestCheckFunc(c.afterStateUpgrade...), + }, + }, }) - } + }) } } -func TestAccResourceStringErrors(t *testing.T) { +func TestAccResourceString_LengthErrors(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testAccResourceStringInvalidConfig, - ExpectError: regexp.MustCompile(`.*length \(2\) must be >= min_upper \+ min_lower \+ min_numeric \+ min_special \(3\)`), + Config: `resource "random_string" "invalid_length" { + length = 2 + min_lower = 3 + }`, + ExpectError: regexp.MustCompile(`.*Attribute "length" \(2\) cannot be less than min_upper \+ min_lower \+\nmin_numeric \+ min_special \(3\).`), }, { - Config: testAccResourceStringLengthTooShortConfig, - ExpectError: regexp.MustCompile(`.*expected length to be at least \(1\), got 0`), + Config: `resource "random_string" "invalid_length" { + length = 0 + }`, + ExpectError: regexp.MustCompile(`.*Value must be at least 1, got: 0`), }, }, }) } -const ( - testAccResourceStringBasic = ` -resource "random_string" "basic" { - length = 12 -}` - testAccResourceStringOverride = ` -resource "random_string" "override" { -length = 4 -override_special = "!" -lower = false -upper = false -number = false -} -` - testAccResourceStringMin = ` -resource "random_string" "min" { -length = 12 -override_special = "!#@" -min_lower = 2 -min_upper = 3 -min_special = 1 -min_numeric = 4 -}` - testAccResourceStringInvalidConfig = ` -resource "random_string" "invalid_length" { - length = 2 - min_lower = 3 -}` - testAccResourceStringLengthTooShortConfig = ` -resource "random_string" "invalid_length" { - length = 0 -}` -) - -func testAccResourceStringCheck(id string, want *customLens) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[id] - if !ok { - return fmt.Errorf("Not found: %s", id) - } - if rs.Primary.ID == "" { - return fmt.Errorf("No ID is set") - } - - customStr := rs.Primary.Attributes["result"] - - if got, want := len(customStr), want.customLen; got != want { - return fmt.Errorf("custom string length is %d; want %d", got, want) - } - - return nil - } +func TestAccResourceString_UpgradeFromVersion3_3_2(t *testing.T) { + resource.Test(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ExternalProviders: providerVersion332(), + Config: `resource "random_string" "min" { + length = 12 + override_special = "!#@" + min_lower = 2 + min_upper = 3 + min_special = 1 + min_numeric = 4 + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrWith("random_string.min", "result", testCheckLen(12)), + resource.TestMatchResourceAttr("random_string.min", "result", regexp.MustCompile(`([a-z].*){2,}`)), + resource.TestMatchResourceAttr("random_string.min", "result", regexp.MustCompile(`([A-Z].*){3,}`)), + resource.TestMatchResourceAttr("random_string.min", "result", regexp.MustCompile(`([0-9].*){4,}`)), + resource.TestMatchResourceAttr("random_string.min", "result", regexp.MustCompile(`([!#@])`)), + resource.TestCheckResourceAttr("random_string.min", "special", "true"), + resource.TestCheckResourceAttr("random_string.min", "upper", "true"), + resource.TestCheckResourceAttr("random_string.min", "lower", "true"), + resource.TestCheckResourceAttr("random_string.min", "numeric", "true"), + resource.TestCheckResourceAttr("random_string.min", "min_special", "1"), + resource.TestCheckResourceAttr("random_string.min", "min_upper", "3"), + resource.TestCheckResourceAttr("random_string.min", "min_lower", "2"), + resource.TestCheckResourceAttr("random_string.min", "min_numeric", "4"), + ), + }, + { + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Config: `resource "random_string" "min" { + length = 12 + override_special = "!#@" + min_lower = 2 + min_upper = 3 + min_special = 1 + min_numeric = 4 + }`, + PlanOnly: true, + }, + { + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Config: `resource "random_string" "min" { + length = 12 + override_special = "!#@" + min_lower = 2 + min_upper = 3 + min_special = 1 + min_numeric = 4 + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrWith("random_string.min", "result", testCheckLen(12)), + resource.TestMatchResourceAttr("random_string.min", "result", regexp.MustCompile(`([a-z].*){2,}`)), + resource.TestMatchResourceAttr("random_string.min", "result", regexp.MustCompile(`([A-Z].*){3,}`)), + resource.TestMatchResourceAttr("random_string.min", "result", regexp.MustCompile(`([0-9].*){4,}`)), + resource.TestMatchResourceAttr("random_string.min", "result", regexp.MustCompile(`([!#@])`)), + resource.TestCheckResourceAttr("random_string.min", "special", "true"), + resource.TestCheckResourceAttr("random_string.min", "upper", "true"), + resource.TestCheckResourceAttr("random_string.min", "lower", "true"), + resource.TestCheckResourceAttr("random_string.min", "numeric", "true"), + resource.TestCheckResourceAttr("random_string.min", "min_special", "1"), + resource.TestCheckResourceAttr("random_string.min", "min_upper", "3"), + resource.TestCheckResourceAttr("random_string.min", "min_lower", "2"), + resource.TestCheckResourceAttr("random_string.min", "min_numeric", "4"), + ), + }, + }, + }) } -func regexMatch(id string, exp *regexp.Regexp, requiredMatches int) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[id] - if !ok { - return fmt.Errorf("Not found: %s", id) - } - if rs.Primary.ID == "" { - return fmt.Errorf("No ID is set") - } - - customStr := rs.Primary.Attributes["result"] - - if matches := exp.FindAllStringSubmatchIndex(customStr, -1); len(matches) < requiredMatches { - return fmt.Errorf("custom string is %s; did not match %s", customStr, exp) +func testCheckLen(expectedLen int) func(input string) error { + return func(input string) error { + if len(input) != expectedLen { + return fmt.Errorf("expected length %d, actual length %d", expectedLen, len(input)) } return nil } } -func patternMatch(id string, want string) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[id] - if !ok { - return fmt.Errorf("Not found: %s", id) - } - if rs.Primary.ID == "" { - return fmt.Errorf("No ID is set") - } - customStr := rs.Primary.Attributes["result"] - if got, want := customStr, want; got != want { - return fmt.Errorf("custom string is %s; want %s", got, want) +//nolint:unparam +func testCheckMinLen(minLen int) func(input string) error { + return func(input string) error { + if len(input) < minLen { + return fmt.Errorf("minimum length %d, actual length %d", minLen, len(input)) } return nil diff --git a/internal/provider/resource_uuid.go b/internal/provider/resource_uuid.go index 06f423f2..b03f1e04 100644 --- a/internal/provider/resource_uuid.go +++ b/internal/provider/resource_uuid.go @@ -5,83 +5,147 @@ import ( "fmt" "github.com/hashicorp/go-uuid" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/terraform-providers/terraform-provider-random/internal/diagnostics" ) -func resourceUuid() *schema.Resource { - return &schema.Resource{ +var _ tfsdk.ResourceType = (*uuidResourceType)(nil) + +type uuidResourceType struct{} + +func (r *uuidResourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{ Description: "The resource `random_uuid` generates random uuid string that is intended to be " + "used as unique identifiers for other resources.\n" + "\n" + "This resource uses [hashicorp/go-uuid](https://github.com/hashicorp/go-uuid) to generate a " + "UUID-formatted string for use with services needed a unique string identifier.", - CreateContext: CreateUuid, - ReadContext: schema.NoopContext, - DeleteContext: RemoveResourceFromState, - Importer: &schema.ResourceImporter{ - StateContext: ImportUuid, - }, - - Schema: map[string]*schema.Schema{ + Attributes: map[string]tfsdk.Attribute{ "keepers": { Description: "Arbitrary map of values that, when changed, will trigger recreation of " + "resource. See [the main provider documentation](../index.html) for more information.", - Type: schema.TypeMap, + Type: types.MapType{ + ElemType: types.StringType, + }, Optional: true, - ForceNew: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, }, - "result": { Description: "The generated uuid presented in string format.", - Type: schema.TypeString, + Type: types.StringType, Computed: true, }, - "id": { Description: "The generated uuid presented in string format.", - Type: schema.TypeString, + Type: types.StringType, Computed: true, }, }, - } + }, nil +} + +func (r uuidResourceType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return &uuidResource{}, nil } -func CreateUuid(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - var diags diag.Diagnostics +var ( + _ tfsdk.Resource = (*uuidResource)(nil) + _ tfsdk.ResourceWithImportState = (*uuidResource)(nil) +) + +type uuidResource struct { +} +func (r *uuidResource) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { result, err := uuid.GenerateUUID() if err != nil { - return append(diags, diag.Errorf("error generating uuid: %s", err)...) + resp.Diagnostics.AddError( + "Create Random UUID error", + "There was an error during generation of a UUID.\n\n"+ + diagnostics.RetryMsg+ + fmt.Sprintf("Original Error: %s", err), + ) + return } - if err := d.Set("result", result); err != nil { - return append(diags, diag.Errorf("error setting result: %s", err)...) + var plan uuidModelV0 + + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + u := &uuidModelV0{ + ID: types.String{Value: result}, + Result: types.String{Value: result}, + Keepers: plan.Keepers, } - d.SetId(result) + diags = resp.State.Set(ctx, u) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read does not need to perform any operations as the state in ReadResourceResponse is already populated. +func (r *uuidResource) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { +} - return nil +// Update is intentionally left blank as all required and optional attributes force replacement of the resource +// through the RequiresReplace AttributePlanModifier. +func (r *uuidResource) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { } -func ImportUuid(_ context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - id := d.Id() +// Delete does not need to explicitly call resp.State.RemoveResource() as this is automatically handled by the +// [framework](https://github.com/hashicorp/terraform-plugin-framework/pull/301). +func (r *uuidResource) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { +} - bytes, err := uuid.ParseUUID(id) +func (r *uuidResource) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { + bytes, err := uuid.ParseUUID(req.ID) if err != nil { - return nil, fmt.Errorf("error parsing uuid bytes: %w", err) + resp.Diagnostics.AddError( + "Import Random UUID Error", + "There was an error during the parsing of the UUID.\n\n"+ + diagnostics.RetryMsg+ + fmt.Sprintf("Original Error: %s", err), + ) + return } result, err := uuid.FormatUUID(bytes) if err != nil { - return nil, fmt.Errorf("error formatting uuid bytes: %w", err) + resp.Diagnostics.AddError( + "Import Random UUID Error", + "There was an error during the formatting of the UUID.\n\n"+ + diagnostics.RetryMsg+ + fmt.Sprintf("Original Error: %s", err), + ) + return } - if err := d.Set("result", result); err != nil { - return nil, fmt.Errorf("error setting result: %w", err) - } + var state uuidModelV0 - d.SetId(result) + state.ID.Value = result + state.Result.Value = result + state.Keepers.ElemType = types.StringType + + diags := resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} - return []*schema.ResourceData{d}, nil +type uuidModelV0 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + Result types.String `tfsdk:"result"` } diff --git a/internal/provider/resource_uuid_test.go b/internal/provider/resource_uuid_test.go index 4df36c51..94ec0577 100644 --- a/internal/provider/resource_uuid_test.go +++ b/internal/provider/resource_uuid_test.go @@ -9,17 +9,13 @@ import ( func TestAccResourceUUID(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + ProtoV6ProviderFactories: protoV6ProviderFactories(), Steps: []resource.TestStep{ { - Config: testAccResourceUUIDConfig, + Config: `resource "random_uuid" "basic" { + }`, Check: resource.ComposeTestCheckFunc( - resource.TestMatchResourceAttr( - "random_uuid.basic", - "result", - regexp.MustCompile(`[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}`), - ), + resource.TestMatchResourceAttr("random_uuid.basic", "result", regexp.MustCompile(`[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}`)), ), }, { @@ -31,8 +27,31 @@ func TestAccResourceUUID(t *testing.T) { }) } -const ( - testAccResourceUUIDConfig = ` -resource "random_uuid" "basic" { } -` -) +func TestAccResourceUUID_UpgradeFromVersion3_3_2(t *testing.T) { + resource.Test(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ExternalProviders: providerVersion332(), + Config: `resource "random_uuid" "basic" { + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("random_uuid.basic", "result", regexp.MustCompile(`[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}`)), + ), + }, + { + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Config: `resource "random_uuid" "basic" { + }`, + PlanOnly: true, + }, + { + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Config: `resource "random_uuid" "basic" { + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("random_uuid.basic", "result", regexp.MustCompile(`[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}`)), + ), + }, + }, + }) +} diff --git a/internal/provider/string.go b/internal/provider/string.go deleted file mode 100644 index 232e9a6b..00000000 --- a/internal/provider/string.go +++ /dev/null @@ -1,395 +0,0 @@ -// Package provider string.go provides shared functionality between `resource_string` and `resource_password`. -// There is no intent to permanently couple their implementations. -// Over time, they could diverge, or one becomes deprecated. -package provider - -import ( - "context" - "crypto/rand" - "errors" - "fmt" - "math/big" - "sort" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" -) - -// passwordSchemaV2 uses passwordSchemaV1 to obtain the V1 version of the Schema key-value entries but requires that -// the numeric entry be configured and that the number entry be altered to include ConflictsWith. -func passwordSchemaV2() map[string]*schema.Schema { - passwordSchema := passwordSchemaV1() - - passwordSchema["number"] = &schema.Schema{ - Description: "Include numeric characters in the result. Default value is `true`. " + - "**NOTE**: This is deprecated, use `numeric` instead.", - Type: schema.TypeBool, - Optional: true, - Computed: true, - ForceNew: true, - ConflictsWith: []string{"numeric"}, - Deprecated: "Use numeric instead.", - } - - passwordSchema["numeric"] = &schema.Schema{ - Description: "Include numeric characters in the result. Default value is `true`.", - Type: schema.TypeBool, - Optional: true, - Computed: true, - ForceNew: true, - ConflictsWith: []string{"number"}, - } - - return passwordSchema -} - -// passwordSchemaV1 uses passwordSchemaV0 to obtain the V0 version of the Schema key-value entries but requires that -// the bcrypt_hash entry be configured. -func passwordSchemaV1() map[string]*schema.Schema { - passwordSchema := passwordSchemaV0() - passwordSchema["bcrypt_hash"] = &schema.Schema{ - Description: "A bcrypt hash of the generated random string.", - Type: schema.TypeString, - Computed: true, - Sensitive: true, - } - - return passwordSchema -} - -// passwordSchemaV0 uses passwordStringSchema to obtain the default Schema key-value entries but requires that the id -// description, result sensitive and bcrypt_hash entries be configured. -func passwordSchemaV0() map[string]*schema.Schema { - passwordSchema := passwordStringSchema() - passwordSchema["id"].Description = "A static value used internally by Terraform, this should not be referenced in configurations." - passwordSchema["result"].Sensitive = true - - return passwordSchema -} - -// stringSchemaV2 uses stringSchemaV1 to obtain the V1 version of the Schema key-value entries but requires that -// the numeric entry be configured and that the number entry be altered to include ConflictsWith. -func stringSchemaV2() map[string]*schema.Schema { - stringSchema := stringSchemaV1() - - stringSchema["number"] = &schema.Schema{ - Description: "Include numeric characters in the result. Default value is `true`. " + - "**NOTE**: This is deprecated, use `numeric` instead.", - Type: schema.TypeBool, - Optional: true, - Computed: true, - ForceNew: true, - ConflictsWith: []string{"numeric"}, - Deprecated: "Use numeric instead.", - } - - stringSchema["numeric"] = &schema.Schema{ - Description: "Include numeric characters in the result. Default value is `true`.", - Type: schema.TypeBool, - Optional: true, - Computed: true, - ForceNew: true, - ConflictsWith: []string{"number"}, - } - - return stringSchema -} - -// stringSchemaV1 uses passwordStringSchema to obtain the default Schema key-value entries but requires that the id -// description be configured. -func stringSchemaV1() map[string]*schema.Schema { - stringSchema := passwordStringSchema() - stringSchema["id"].Description = "The generated random string." - - return stringSchema -} - -// passwordStringSchema returns map[string]*schema.Schema with all keys and values that are common to both the -// password and string resources. -func passwordStringSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "keepers": { - Description: "Arbitrary map of values that, when changed, will trigger recreation of " + - "resource. See [the main provider documentation](../index.html) for more information.", - Type: schema.TypeMap, - Optional: true, - ForceNew: true, - }, - - "length": { - Description: "The length of the string desired. The minimum value for length is 1 and, length " + - "must also be >= (`min_upper` + `min_lower` + `min_numeric` + `min_special`).", - Type: schema.TypeInt, - Required: true, - ForceNew: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), - }, - - "special": { - Description: "Include special characters in the result. These are `!@#$%&*()-_=+[]{}<>:?`. Default value is `true`.", - Type: schema.TypeBool, - Optional: true, - Default: true, - ForceNew: true, - }, - - "upper": { - Description: "Include uppercase alphabet characters in the result. Default value is `true`.", - Type: schema.TypeBool, - Optional: true, - Default: true, - ForceNew: true, - }, - - "lower": { - Description: "Include lowercase alphabet characters in the result. Default value is `true`.", - Type: schema.TypeBool, - Optional: true, - Default: true, - ForceNew: true, - }, - - "number": { - Description: "Include numeric characters in the result. Default value is `true`.", - Type: schema.TypeBool, - Optional: true, - Default: true, - ForceNew: true, - }, - - "min_numeric": { - Description: "Minimum number of numeric characters in the result. Default value is `0`.", - Type: schema.TypeInt, - Optional: true, - Default: 0, - ForceNew: true, - }, - - "min_upper": { - Description: "Minimum number of uppercase alphabet characters in the result. Default value is `0`.", - Type: schema.TypeInt, - Optional: true, - Default: 0, - ForceNew: true, - }, - - "min_lower": { - Description: "Minimum number of lowercase alphabet characters in the result. Default value is `0`.", - Type: schema.TypeInt, - Optional: true, - Default: 0, - ForceNew: true, - }, - - "min_special": { - Description: "Minimum number of special characters in the result. Default value is `0`.", - Type: schema.TypeInt, - Optional: true, - Default: 0, - ForceNew: true, - }, - - "override_special": { - Description: "Supply your own list of special characters to use for string generation. This " + - "overrides the default character list in the special argument. The `special` argument must " + - "still be set to true for any overwritten characters to be used in generation.", - Type: schema.TypeString, - Optional: true, - ForceNew: true, - }, - - "result": { - Description: "The generated random string.", - Type: schema.TypeString, - Computed: true, - }, - - "id": { - Computed: true, - Type: schema.TypeString, - }, - } -} - -func createStringFunc(sensitive bool) func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - return func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { - const numChars = "0123456789" - const lowerChars = "abcdefghijklmnopqrstuvwxyz" - const upperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - var ( - specialChars = "!@#$%&*()-_=+[]{}<>:?" - diags diag.Diagnostics - ) - - length := d.Get("length").(int) - upper := d.Get("upper").(bool) - minUpper := d.Get("min_upper").(int) - lower := d.Get("lower").(bool) - minLower := d.Get("min_lower").(int) - number := d.Get("number").(bool) - numeric := d.Get("numeric").(bool) - minNumeric := d.Get("min_numeric").(int) - special := d.Get("special").(bool) - minSpecial := d.Get("min_special").(int) - overrideSpecial := d.Get("override_special").(string) - - if length < minUpper+minLower+minNumeric+minSpecial { - return append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("length (%d) must be >= min_upper + min_lower + min_numeric + min_special (%d)", length, minUpper+minLower+minNumeric+minSpecial), - }) - } - - if overrideSpecial != "" { - specialChars = overrideSpecial - } - - var chars = string("") - if upper { - chars += upperChars - } - if lower { - chars += lowerChars - } - if numeric { - chars += numChars - } - if special { - chars += specialChars - } - - minMapping := map[string]int{ - numChars: minNumeric, - lowerChars: minLower, - upperChars: minUpper, - specialChars: minSpecial, - } - var result = make([]byte, 0, length) - for k, v := range minMapping { - s, err := generateRandomBytes(&k, v) - if err != nil { - return append(diags, diag.Errorf("error generating random bytes: %s", err)...) - } - result = append(result, s...) - } - s, err := generateRandomBytes(&chars, length-len(result)) - if err != nil { - return append(diags, diag.Errorf("error generating random bytes: %s", err)...) - } - result = append(result, s...) - order := make([]byte, len(result)) - if _, err := rand.Read(order); err != nil { - return append(diags, diag.Errorf("error generating random bytes: %s", err)...) - } - sort.Slice(result, func(i, j int) bool { - return order[i] < order[j] - }) - - if err := d.Set("result", string(result)); err != nil { - return append(diags, diag.Errorf("error setting result: %s", err)...) - } - - if err := d.Set("number", number); err != nil { - return append(diags, diag.Errorf("error setting number: %s", err)...) - } - if err := d.Set("numeric", numeric); err != nil { - return append(diags, diag.Errorf("error setting numeric: %s", err)...) - } - - if sensitive { - d.SetId("none") - } else { - d.SetId(string(result)) - } - return nil - } -} - -func generateRandomBytes(charSet *string, length int) ([]byte, error) { - bytes := make([]byte, length) - setLen := big.NewInt(int64(len(*charSet))) - for i := range bytes { - idx, err := rand.Int(rand.Reader, setLen) - if err != nil { - return nil, err - } - bytes[i] = (*charSet)[idx.Int64()] - } - return bytes, nil -} - -func readNil(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - return nil -} - -func resourcePasswordStringStateUpgradeV1(_ context.Context, rawState map[string]interface{}, _ interface{}) (map[string]interface{}, error) { - if rawState == nil { - return nil, errors.New("state upgrade failed, state is nil") - } - - if number, ok := rawState["number"].(bool); ok { - rawState["numeric"] = number - } - - return rawState, nil -} - -// planDefaultIfAllNull handles ensuring that both `number` and `numeric` attributes default to `true` when neither are set -// in the config and, they had been previously set to `false`. This behaviour mimics setting `Default: true` on the -// attributes. Usage of `Default` is avoided as `Default` cannot be used with CustomizeDiffFunc(s) which are required in -// order to keep `number` and `numeric` in-sync (see planSyncIfChange). -func planDefaultIfAllNull(defaultVal interface{}, keys ...string) []schema.CustomizeDiffFunc { - var result []schema.CustomizeDiffFunc - - for _, key := range keys { - result = append(result, customdiff.IfValue( - key, - func(ctx context.Context, value, meta interface{}) bool { - return !value.(bool) - }, - func(_ context.Context, d *schema.ResourceDiff, _ interface{}) error { - vm := d.GetRawConfig().AsValueMap() - - number, ok := vm["number"] - if !ok { - return errors.New("number is absent from raw config") - } - - numeric, ok := vm["numeric"] - if !ok { - return errors.New("numeric is absent from raw config") - } - - if number.IsNull() && numeric.IsNull() { - err := d.SetNew("number", defaultVal) - if err != nil { - return err - } - err = d.SetNew("numeric", defaultVal) - if err != nil { - return err - } - } - return nil - }, - )) - } - - return result -} - -// planSyncIfChange handles keeping `number` and `numeric` in-sync. If either is changed the value of both is -// set to the new value of the attribute that has changed. -func planSyncIfChange(key, keyToSync string) func(context.Context, *schema.ResourceDiff, interface{}) error { - return customdiff.IfValueChange( - key, - func(ctx context.Context, oldValue, newValue, meta interface{}) bool { - return oldValue != newValue - }, - func(_ context.Context, d *schema.ResourceDiff, _ interface{}) error { - return d.SetNew(keyToSync, d.Get(key)) - }, - ) -} diff --git a/internal/provider/string_test.go b/internal/provider/string_test.go deleted file mode 100644 index a6231047..00000000 --- a/internal/provider/string_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package provider - -import ( - "context" - "errors" - "testing" - - "github.com/google/go-cmp/cmp" -) - -func TestResourcePasswordStringStateUpgradeV1(t *testing.T) { - cases := []struct { - name string - stateV1 map[string]interface{} - err error - expectedStateV2 map[string]interface{} - }{ - { - name: "raw state is nil", - stateV1: nil, - err: errors.New("state upgrade failed, state is nil"), - }, - { - name: "number is not bool, raw state unaltered", - stateV1: map[string]interface{}{"number": 0}, - expectedStateV2: map[string]interface{}{"number": 0}, - }, - { - name: "success", - stateV1: map[string]interface{}{"number": true}, - expectedStateV2: map[string]interface{}{"number": true, "numeric": true}, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - actualStateV2, err := resourcePasswordStringStateUpgradeV1(context.Background(), c.stateV1, nil) - - if c.err != nil { - if !cmp.Equal(c.err.Error(), err.Error()) { - t.Errorf("expected: %q, got: %q", c.err.Error(), err) - } - if !cmp.Equal(c.expectedStateV2, actualStateV2) { - t.Errorf("expected: %+v, got: %+v", c.expectedStateV2, err) - } - } else { - if err != nil { - t.Errorf("err should be nil, actual: %v", err) - } - - if !cmp.Equal(actualStateV2, c.expectedStateV2) { - t.Errorf("expected: %v, got: %v", c.expectedStateV2, actualStateV2) - } - } - }) - } -} diff --git a/internal/provider/seed.go b/internal/random/seed.go similarity index 96% rename from internal/provider/seed.go rename to internal/random/seed.go index 4d3c5265..b0bcf8bb 100644 --- a/internal/provider/seed.go +++ b/internal/random/seed.go @@ -1,4 +1,4 @@ -package provider +package random import ( "hash/crc64" diff --git a/internal/random/string.go b/internal/random/string.go new file mode 100644 index 00000000..f3494050 --- /dev/null +++ b/internal/random/string.go @@ -0,0 +1,94 @@ +package random + +import ( + "crypto/rand" + "math/big" + "sort" +) + +type StringParams struct { + Length int64 + Upper bool + MinUpper int64 + Lower bool + MinLower int64 + Numeric bool + MinNumeric int64 + Special bool + MinSpecial int64 + OverrideSpecial string +} + +func CreateString(input StringParams) ([]byte, error) { + const numChars = "0123456789" + const lowerChars = "abcdefghijklmnopqrstuvwxyz" + const upperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + var specialChars = "!@#$%&*()-_=+[]{}<>:?" + var result []byte + + if input.OverrideSpecial != "" { + specialChars = input.OverrideSpecial + } + + var chars = "" + if input.Upper { + chars += upperChars + } + if input.Lower { + chars += lowerChars + } + if input.Numeric { + chars += numChars + } + if input.Special { + chars += specialChars + } + + minMapping := map[string]int64{ + numChars: input.MinNumeric, + lowerChars: input.MinLower, + upperChars: input.MinUpper, + specialChars: input.MinSpecial, + } + + result = make([]byte, 0, input.Length) + + for k, v := range minMapping { + s, err := generateRandomBytes(&k, v) + if err != nil { + return nil, err + } + result = append(result, s...) + } + + s, err := generateRandomBytes(&chars, input.Length-int64(len(result))) + if err != nil { + return nil, err + } + + result = append(result, s...) + + order := make([]byte, len(result)) + if _, err := rand.Read(order); err != nil { + return nil, err + } + + sort.Slice(result, func(i, j int) bool { + return order[i] < order[j] + }) + + return result, nil +} + +func generateRandomBytes(charSet *string, length int64) ([]byte, error) { + bytes := make([]byte, length) + setLen := big.NewInt(int64(len(*charSet))) + for i := range bytes { + idx, err := rand.Int(rand.Reader, setLen) + if err != nil { + return nil, err + } + bytes[i] = (*charSet)[idx.Int64()] + } + return bytes, nil +} diff --git a/internal/validators/validators.go b/internal/validators/validators.go new file mode 100644 index 00000000..20f86eb4 --- /dev/null +++ b/internal/validators/validators.go @@ -0,0 +1,114 @@ +package validators + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// intIsAtLeastSumOfValidator checks that the value of the attribute in the configuration +// (i.e., AttributeConfig in ValidateAttributeRequest) is greater than or, equal to the sum of the values of the +// attributes in the slice of AttributePath. +// TODO: Remove once https://github.com/hashicorp/terraform-plugin-framework-validators/pull/29 is merged. +type intIsAtLeastSumOfValidator struct { + attributesToSum []*tftypes.AttributePath +} + +var _ tfsdk.AttributeValidator = (*intIsAtLeastSumOfValidator)(nil) + +func NewIntIsAtLeastSumOfValidator(attributePaths ...*tftypes.AttributePath) tfsdk.AttributeValidator { + return &intIsAtLeastSumOfValidator{attributePaths} +} + +func (av *intIsAtLeastSumOfValidator) Description(ctx context.Context) string { + return av.MarkdownDescription(ctx) +} + +func (av *intIsAtLeastSumOfValidator) MarkdownDescription(context.Context) string { + return fmt.Sprintf("Ensure that attribute has a value >= sum of: %q", av.attributesToSum) +} + +// Validate runs the following checks: +// 1. Determines if AttributeConfig can be reflected into types.Int64. +// 2. Checks that the AttributeConfig value is >= sum of values of the attributes defined in attributesToSum. +func (av *intIsAtLeastSumOfValidator) Validate(ctx context.Context, req tfsdk.ValidateAttributeRequest, resp *tfsdk.ValidateAttributeResponse) { + tflog.Debug(ctx, "Validating that attribute has a value at least equal to the attributes to sum", map[string]interface{}{ + "attribute": attrPathToString(req.AttributePath), + "attributesToSum": av.attributesToSum, + }) + + // TODO: Remove once attr.Value interface includes IsNull. + attribConfigValue, err := req.AttributeConfig.ToTerraformValue(ctx) + if err != nil { + resp.Diagnostics.AddError( + "Int at least sum of validator failed", + fmt.Sprintf("Unable to convert attribute config (%s) to terraform value: %s", req.AttributeConfig.Type(ctx).String(), err), + ) + return + } + + if attribConfigValue.IsNull() || !attribConfigValue.IsKnown() { + return + } + + var attrib types.Int64 + + resp.Diagnostics.Append(tfsdk.ValueAs(ctx, req.AttributeConfig, &attrib)...) + if resp.Diagnostics.HasError() { + return + } + + var sumOfAttribs int64 + var attributesToSumPaths []string + + for _, path := range av.attributesToSum { + var attribToSum types.Int64 + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path, &attribToSum)...) + if resp.Diagnostics.HasError() { + return + } + + sumOfAttribs += attribToSum.Value + attributesToSumPaths = append(attributesToSumPaths, attrPathToString(path)) + } + + if attrib.Value < sumOfAttribs { + attribPath := attrPathToString(req.AttributePath) + + resp.Diagnostics.AddAttributeError( + req.AttributePath, + fmt.Sprintf("Attribute %q is less than summed attributes.", attribPath), + fmt.Sprintf("Attribute %q (%d) cannot be less than %s (%d).", attribPath, attrib.Value, strings.Join(attributesToSumPaths, " + "), sumOfAttribs), + ) + } +} + +// attrPathToString takes all the tftypes.AttributePathStep in a tftypes.AttributePath and concatenates them, +// using `.` as separator. +// +// This should be used only when trying to "print out" a tftypes.AttributePath in a log or an error message. +func attrPathToString(a *tftypes.AttributePath) string { + var res strings.Builder + for pos, step := range a.Steps() { + if pos != 0 { + res.WriteString(".") + } + switch v := step.(type) { + case tftypes.AttributeName: + res.WriteString(string(v)) + case tftypes.ElementKeyString: + res.WriteString(string(v)) + case tftypes.ElementKeyInt: + res.WriteString(strconv.FormatInt(int64(v), 10)) + case tftypes.ElementKeyValue: + res.WriteString(tftypes.Value(v).String()) + } + } + return res.String() +} diff --git a/internal/validators/validators_test.go b/internal/validators/validators_test.go new file mode 100644 index 00000000..58d18e0e --- /dev/null +++ b/internal/validators/validators_test.go @@ -0,0 +1,147 @@ +package validators + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestIsAtLeastSumOfValidator_Validate(t *testing.T) { + t.Parallel() + + req := tfsdk.ValidateAttributeRequest{ + AttributePath: tftypes.NewAttributePath().WithAttributeName("length"), + AttributeConfig: types.Int64{Value: 16}, + Config: tfsdk.Config{ + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "min_upper": { + Type: types.Int64Type, + }, + + "min_lower": { + Type: types.Int64Type, + }, + }, + }, + }, + } + + cases := []struct { + name string + reqAttribConfig attr.Value + reqConfigRaw tftypes.Value + attributesToSum []*tftypes.AttributePath + expectDiag bool + expectedValidatorDiags diag.Diagnostics + }{ + { + name: "attribute wrong type", + reqAttribConfig: types.String{Value: "16"}, + expectDiag: true, + }, + { + "attribute less than sum of attribute", + types.Int64{Value: 16}, + tftypes.NewValue(tftypes.Object{}, map[string]tftypes.Value{ + "min_upper": tftypes.NewValue(tftypes.Number, 17), + }), + []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("min_upper"), + }, + true, + diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath().WithAttributeName("length"), + `Attribute "length" is less than summed attributes.`, + `Attribute "length" (16) cannot be less than min_upper (17).`, + ), + }, + }, + { + "attribute less than sum of attributes", + types.Int64{Value: 16}, + tftypes.NewValue(tftypes.Object{}, map[string]tftypes.Value{ + "min_upper": tftypes.NewValue(tftypes.Number, 10), + "min_lower": tftypes.NewValue(tftypes.Number, 12), + }), + []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("min_upper"), + tftypes.NewAttributePath().WithAttributeName("min_lower"), + }, + true, + diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath().WithAttributeName("length"), + `Attribute "length" is less than summed attributes.`, + `Attribute "length" (16) cannot be less than min_upper + min_lower (22).`, + ), + }, + }, + { + "a summed attribute is of invalid type", + types.Int64{Value: 16}, + tftypes.NewValue(tftypes.Object{}, map[string]tftypes.Value{ + "min_upper": tftypes.NewValue(tftypes.String, "17"), + }), + []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("min_upper"), + }, + true, + diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + tftypes.NewAttributePath().WithAttributeName("min_upper"), + `Int64 Type Validation Error`, + `An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. Please report the following to the provider developer: + +Expected Number value, received tftypes.Value with value: tftypes.String<"17">`, + ), + }, + }, + { + name: "attribute equal to sum of attributes", + reqAttribConfig: types.Int64{Value: 16}, + reqConfigRaw: tftypes.NewValue(tftypes.Object{}, map[string]tftypes.Value{ + "min_upper": tftypes.NewValue(tftypes.Number, 8), + "min_lower": tftypes.NewValue(tftypes.Number, 8), + }), + attributesToSum: []*tftypes.AttributePath{ + tftypes.NewAttributePath().WithAttributeName("min_upper"), + tftypes.NewAttributePath().WithAttributeName("min_lower"), + }, + expectDiag: false, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + req.AttributeConfig = c.reqAttribConfig + req.Config.Raw = c.reqConfigRaw + resp := tfsdk.ValidateAttributeResponse{ + Diagnostics: diag.Diagnostics{}, + } + + validator := NewIntIsAtLeastSumOfValidator(c.attributesToSum...) + validator.Validate(context.Background(), req, &resp) + + if c.expectDiag { + if len(resp.Diagnostics) != 1 { + t.Errorf("expecting resp diags len: 1, actual resp diags len: %d", len(resp.Diagnostics)) + } + } + + // Only test the contents of diags that are explicitly under the control of the validator. + if c.expectedValidatorDiags != nil { + if !cmp.Equal(c.expectedValidatorDiags, resp.Diagnostics) { + t.Errorf("expecting resp diags: %s, actual resp diags: %s", c.expectedValidatorDiags, resp.Diagnostics) + } + } + }) + } +} diff --git a/main.go b/main.go index 6575fd60..196115a4 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,11 @@ package main import ( - "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" + "context" + "flag" + "log" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/terraform-providers/terraform-provider-random/internal/provider" ) @@ -17,5 +21,16 @@ import ( //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs func main() { - plugin.Serve(&plugin.ServeOpts{ProviderFunc: provider.New}) + var debug bool + + flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") + flag.Parse() + + err := providerserver.Serve(context.Background(), provider.New, providerserver.ServeOpts{ + Address: "registry.terraform.io/hashicorp/random", + Debug: debug, + }) + if err != nil { + log.Fatal(err) + } } diff --git a/terraform-registry-manifest.json b/terraform-registry-manifest.json index a8286e38..6e86c621 100644 --- a/terraform-registry-manifest.json +++ b/terraform-registry-manifest.json @@ -1,6 +1,6 @@ { "version": 1, "metadata": { - "protocol_versions": ["5.0"] + "protocol_versions": ["6.0"] } } \ No newline at end of file