diff --git a/README.md b/README.md index fefe4191..076bcbea 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,20 @@ It's published on the [Terraform registry](https://registry.terraform.io/provide ## Requirements - [Terraform](https://www.terraform.io/downloads.html) >= 1.0 - - [Go](https://golang.org/doc/install) 1.21 (to build the provider plugin) + - [Go](https://golang.org/doc/install) 1.24 (to build the provider plugin) + +## Limitations + +Due to limited testing capacities, the following features are not tested/stable yet: + +* External Schemas + * Hive Database + * RDS Postgres Database + * RDS MySQL Database + * Redshift Database +* Temporary Credentials Cluster Identifier +* Temporary Credentials Assume Role +* Datashares ## Building The Provider diff --git a/docs/resources/group.md b/docs/resources/group.md index 8a8763d8..206ac7fc 100644 --- a/docs/resources/group.md +++ b/docs/resources/group.md @@ -41,6 +41,8 @@ resource "redshift_group" "staff" { Import is supported using the following syntax: +The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: + ```shell # Import group with grosysid: SELECT grosysid FROM pg_group WHERE groname = 'mygroup' diff --git a/docs/resources/group_membership.md b/docs/resources/group_membership.md new file mode 100644 index 00000000..b33ef59e --- /dev/null +++ b/docs/resources/group_membership.md @@ -0,0 +1,25 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "redshift_group_membership Resource - terraform-provider-redshift" +subcategory: "" +description: |- + Manages Redshift group memberships. Allows either to exclusively manage group memberships or to add members to an existing group. Note: this resource conflicts with the users attribute of the redshift_group resource +--- + +# redshift_group_membership (Resource) + +Manages Redshift group memberships. Allows either to exclusively manage group memberships or to add members to an existing group. Note: this resource conflicts with the `users` attribute of the `redshift_group` resource + + + + +## Schema + +### Required + +- `name` (String) Name of the user group. +- `users` (Set of String) List of the user names to add to the group. Note: this resource does not check whether the specified users exist. + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/docs/resources/schema.md b/docs/resources/schema.md index a179a8c2..7ef8beca 100644 --- a/docs/resources/schema.md +++ b/docs/resources/schema.md @@ -279,6 +279,8 @@ Optional: Import is supported using the following syntax: +The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: + ```shell # Import schema with oid: SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = 'myschema'; diff --git a/docs/resources/user.md b/docs/resources/user.md index 42c9a037..946a7c17 100644 --- a/docs/resources/user.md +++ b/docs/resources/user.md @@ -50,6 +50,8 @@ resource "redshift_user" "user_with_unrestricted_syslog" { Import is supported using the following syntax: +The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: + ```shell # Import user with usesysid: SELECT usesysid FROM pg_user_info WHERE usename = 'mememe' diff --git a/examples/resources/redshift_group_membership/resource.tf b/examples/resources/redshift_group_membership/resource.tf new file mode 100644 index 00000000..0b4e7b1f --- /dev/null +++ b/examples/resources/redshift_group_membership/resource.tf @@ -0,0 +1,4 @@ +resource "redshift_group_membership" "simple" { + name = "some_group_name" + users = ["user1", "user2"] +} diff --git a/go.mod b/go.mod index dc0ef2cb..29d4133a 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,20 @@ module github.com/dbsystel/terraform-provider-redshift -go 1.23.7 +go 1.24 toolchain go1.24.5 require ( - github.com/aws/aws-sdk-go-v2 v1.37.0 - github.com/aws/aws-sdk-go-v2/config v1.30.1 - github.com/aws/aws-sdk-go-v2/credentials v1.18.1 - github.com/aws/aws-sdk-go-v2/service/redshift v1.55.0 - github.com/aws/aws-sdk-go-v2/service/sts v1.35.0 + github.com/aws/aws-sdk-go-v2 v1.38.0 + github.com/aws/aws-sdk-go-v2/config v1.31.0 + github.com/aws/aws-sdk-go-v2/credentials v1.18.4 + github.com/aws/aws-sdk-go-v2/service/redshift v1.57.0 + github.com/aws/aws-sdk-go-v2/service/sts v1.37.0 github.com/hashicorp/terraform-plugin-docs v0.22.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 github.com/lib/pq v1.10.9 - golang.org/x/net v0.42.0 + github.com/mmichaelb/redshift-data-sql-driver v0.4.0 + golang.org/x/net v0.43.0 ) require ( @@ -23,22 +24,23 @@ require ( github.com/Masterminds/semver/v3 v3.2.0 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect - github.com/agext/levenshtein v1.2.2 // indirect + github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/armon/go-radix v1.0.0 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.0 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.0 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.0 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.26.0 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.31.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3 // indirect + github.com/aws/aws-sdk-go-v2/service/redshiftdata v1.36.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.28.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0 // indirect github.com/aws/smithy-go v1.22.5 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect - github.com/fatih/color v1.16.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect @@ -49,20 +51,20 @@ require ( github.com/hashicorp/go-cty v1.5.0 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.6.3 // indirect + github.com/hashicorp/go-plugin v1.7.0 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/hc-install v0.9.2 // indirect - github.com/hashicorp/hcl/v2 v2.23.0 // indirect + github.com/hashicorp/hcl/v2 v2.24.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.23.0 // indirect github.com/hashicorp/terraform-json v0.25.0 // indirect - github.com/hashicorp/terraform-plugin-go v0.27.0 // indirect + github.com/hashicorp/terraform-plugin-go v0.28.0 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect - github.com/hashicorp/terraform-registry-address v0.2.5 // indirect + github.com/hashicorp/terraform-registry-address v0.3.0 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect - github.com/hashicorp/yamux v0.1.1 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.15 // indirect github.com/mattn/go-colorable v0.1.14 // indirect @@ -70,10 +72,10 @@ require ( github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect - github.com/mitchellh/go-wordwrap v1.0.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // 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.2.0 // indirect github.com/posener/complete v1.2.3 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/spf13/cast v1.5.0 // indirect @@ -84,17 +86,17 @@ require ( github.com/yuin/goldmark-meta v1.1.0 // indirect github.com/zclconf/go-cty v1.16.3 // indirect go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect - golang.org/x/crypto v0.40.0 // indirect + golang.org/x/crypto v0.41.0 // indirect golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect - golang.org/x/mod v0.25.0 // indirect + golang.org/x/mod v0.27.0 // indirect golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/text v0.27.0 // indirect - golang.org/x/tools v0.34.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/tools v0.36.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect - google.golang.org/grpc v1.72.1 // indirect - google.golang.org/protobuf v1.36.6 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect + google.golang.org/grpc v1.74.2 // indirect + google.golang.org/protobuf v1.36.7 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5f17c9eb..d01c2097 100644 --- a/go.sum +++ b/go.sum @@ -16,37 +16,43 @@ github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNx github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aws/aws-sdk-go-v2 v1.37.0 h1:YtCOESR/pN4j5oA7cVHSfOwIcuh/KwHC4DOSXFbv5F0= -github.com/aws/aws-sdk-go-v2 v1.37.0/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= -github.com/aws/aws-sdk-go-v2/config v1.30.1 h1:sHL8g/+9tcZATeV2tEkEfxZeaNokDtKsSjGMGHD49qA= -github.com/aws/aws-sdk-go-v2/config v1.30.1/go.mod h1:wkibEyFfxXRyTSzRU4bbF5IUsSXyE4xQ4ZjkGmi5tFo= -github.com/aws/aws-sdk-go-v2/credentials v1.18.1 h1:E55xvOqlX7CvB66Z7rSM9usCrFU1ryUIUHqiXsEzVoE= -github.com/aws/aws-sdk-go-v2/credentials v1.18.1/go.mod h1:iobSQfR5MkvILxssGOvi/P1jjOhrRzfTiCPCzku0vx4= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.0 h1:9sBTeKQwAvmJUWKIACIoiFSnxxl+sS++YDfr17/ngq0= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.0/go.mod h1:LW9/PxQD1SYFC7pnWcgqPhoyZprhjEdg5hBK6qYPLW8= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.0 h1:H2iZoqW/v2Jnrh1FnU725Bq6KJ0k2uP63yH+DcY+HUI= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.0/go.mod h1:L0FqLbwMXHvNC/7crWV1iIxUlOKYZUE8KuTIA+TozAI= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.0 h1:EDped/rNzAhFPhVY0sDGbtD16OKqksfA8OjF/kLEgw8= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.0/go.mod h1:uUI335jvzpZRPpjYx6ODc/wg1qH+NnoSTK/FwVeK0C0= +github.com/aws/aws-sdk-go-v2 v1.38.0 h1:UCRQ5mlqcFk9HJDIqENSLR3wiG1VTWlyUfLDEvY7RxU= +github.com/aws/aws-sdk-go-v2 v1.38.0/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= +github.com/aws/aws-sdk-go-v2/config v1.31.0 h1:9yH0xiY5fUnVNLRWO0AtayqwU1ndriZdN78LlhruJR4= +github.com/aws/aws-sdk-go-v2/config v1.31.0/go.mod h1:VeV3K72nXnhbe4EuxxhzsDc/ByrCSlZwUnWH52Nde/I= +github.com/aws/aws-sdk-go-v2/credentials v1.18.4 h1:IPd0Algf1b+Qy9BcDp0sCUcIWdCQPSzDoMK3a8pcbUM= +github.com/aws/aws-sdk-go-v2/credentials v1.18.4/go.mod h1:nwg78FjH2qvsRM1EVZlX9WuGUJOL5od+0qvm0adEzHk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3 h1:GicIdnekoJsjq9wqnvyi2elW6CGMSYKhdozE7/Svh78= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3/go.mod h1:R7BIi6WNC5mc1kfRM7XM/VHC3uRWkjc396sfabq4iOo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3 h1:o9RnO+YZ4X+kt5Z7Nvcishlz0nksIt2PIzDglLMP0vA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3/go.mod h1:+6aLJzOG1fvMOyzIySYjOFjcguGvVRL68R+uoRencN4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3 h1:joyyUFhiTQQmVK6ImzNU9TQSNRNeD9kOklqTzyk5v6s= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3/go.mod h1:+vNIyZQP3b3B1tSLI0lxvrU9cfM7gpdRXMFfm67ZcPc= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.0 h1:eRhU3Sh8dGbaniI6B+I48XJMrTPRkK4DKo+vqIxziOU= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.0/go.mod h1:paNLV18DZ6FnWE/bd06RIKPDIFpjuvCkGKWTG/GDBeM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3 h1:ieRzyHXypu5ByllM7Sp4hC5f/1Fy5wqxqY0yB85hC7s= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3/go.mod h1:O5ROz8jHiOAKAwx179v+7sHMhfobFVi6nZt8DEyiYoM= github.com/aws/aws-sdk-go-v2/service/redshift v1.55.0 h1:70T8EpAmUAmh1+iljlPu94NnUKATN9GedtKY0y9I4CY= github.com/aws/aws-sdk-go-v2/service/redshift v1.55.0/go.mod h1:ItDt61dKOBnzf5gY/kvu4UaDKNxdp8LntwS7PaaVpfU= -github.com/aws/aws-sdk-go-v2/service/sso v1.26.0 h1:cuFWHH87GP1NBGXXfMicUbE7Oty5KpPxN6w4JpmuxYc= -github.com/aws/aws-sdk-go-v2/service/sso v1.26.0/go.mod h1:aJBemdlbCKyOXEXdXBqS7E+8S9XTDcOTaoOjtng54hA= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.31.0 h1:t2va+wewPOYIqC6XyJ4MGjiGKkczMAPsgq5W4FtL9ME= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.31.0/go.mod h1:ExCTcqYqN0hYYRsDlBVU8+68grqlWdgX9/nZJwQW4aY= -github.com/aws/aws-sdk-go-v2/service/sts v1.35.0 h1:FD9agdG4CeOGS3ORLByJk56YIXDS7mxFpmZyCtpqExc= -github.com/aws/aws-sdk-go-v2/service/sts v1.35.0/go.mod h1:NDzDPbBF1xtSTZUMuZx0w3hIfWzcL7X2AQ0Tr9becIQ= +github.com/aws/aws-sdk-go-v2/service/redshift v1.57.0 h1:gFNE53MstNSex5n2AeuqDeO9y6YrAEq5r9ohIo0Q1S4= +github.com/aws/aws-sdk-go-v2/service/redshift v1.57.0/go.mod h1:royODzFrVBRoek5vd76xF7WnwhMGjDj9ZdYcg7Hj8Es= +github.com/aws/aws-sdk-go-v2/service/redshiftdata v1.36.0 h1:m900Kua81M38+2mViQR0WyXuVJHXfHhBMZE2KEOKCxg= +github.com/aws/aws-sdk-go-v2/service/redshiftdata v1.36.0/go.mod h1:fKbyyPpRvNxxd3FC8IllvIVic0l4C/IGKIcU7lb5tfI= +github.com/aws/aws-sdk-go-v2/service/sso v1.28.0 h1:Mc/MKBf2m4VynyJkABoVEN+QzkfLqGj0aiJuEe7cMeM= +github.com/aws/aws-sdk-go-v2/service/sso v1.28.0/go.mod h1:iS5OmxEcN4QIPXARGhavH7S8kETNL11kym6jhoS7IUQ= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0 h1:6csaS/aJmqZQbKhi1EyEMM7yBW653Wy/B9hnBofW+sw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0/go.mod h1:59qHWaY5B+Rs7HGTuVGaC32m0rdpQ68N8QCN3khYiqs= +github.com/aws/aws-sdk-go-v2/service/sts v1.37.0 h1:MG9VFW43M4A8BYeAfaJJZWrroinxeTi2r3+SnmLQfSA= +github.com/aws/aws-sdk-go-v2/service/sts v1.37.0/go.mod h1:JdeBDPgpJfuS6rU/hNglmOigKhyEZtBmbraLE4GK1J8= github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= @@ -55,6 +61,7 @@ github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= @@ -67,6 +74,8 @@ github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FM github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -77,6 +86,7 @@ github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= @@ -114,6 +124,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg= github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -125,6 +137,8 @@ github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+O github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I= github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= +github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I= @@ -135,16 +149,22 @@ github.com/hashicorp/terraform-plugin-docs v0.22.0 h1:fwIDStbFel1PPNkM+mDPnpB4ef github.com/hashicorp/terraform-plugin-docs v0.22.0/go.mod h1:55DJVyZ7BNK4t/lANcQ1YpemRuS6KsvIO1BbGA+xzGE= github.com/hashicorp/terraform-plugin-go v0.27.0 h1:ujykws/fWIdsi6oTUT5Or4ukvEan4aN9lY+LOxVP8EE= github.com/hashicorp/terraform-plugin-go v0.27.0/go.mod h1:FDa2Bb3uumkTGSkTFpWSOwWJDwA7bf3vdP3ltLDTH6o= +github.com/hashicorp/terraform-plugin-go v0.28.0 h1:zJmu2UDwhVN0J+J20RE5huiF3XXlTYVIleaevHZgKPA= +github.com/hashicorp/terraform-plugin-go v0.28.0/go.mod h1:FDa2Bb3uumkTGSkTFpWSOwWJDwA7bf3vdP3ltLDTH6o= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 h1:NFPMacTrY/IdcIcnUB+7hsore1ZaRWU9cnB6jFoBnIM= github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0/go.mod h1:QYmYnLfsosrxjCnGY1p9c7Zj6n9thnEE+7RObeYs3fA= github.com/hashicorp/terraform-registry-address v0.2.5 h1:2GTftHqmUhVOeuu9CW3kwDkRe4pcBDq0uuK5VJngU1M= github.com/hashicorp/terraform-registry-address v0.2.5/go.mod h1:PpzXWINwB5kuVS5CA7m1+eO2f1jKb5ZDIxrOPfpnGkg= +github.com/hashicorp/terraform-registry-address v0.3.0 h1:HMpK3nqaGFPS9VmgRXrJL/dzHNdheGVKk5k7VlFxzCo= +github.com/hashicorp/terraform-registry-address v0.3.0/go.mod h1:jRGCMiLaY9zii3GLC7hqpSnwhfnCN5yzvY0hh4iCGbM= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= @@ -154,6 +174,7 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -182,13 +203,21 @@ github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJ github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mmichaelb/redshift-data-sql-driver v0.0.3 h1:xOEYPczRHpbwduJSfjk/EqKCiSkw2nk1AciHsRFzYLk= +github.com/mmichaelb/redshift-data-sql-driver v0.0.3/go.mod h1:0g7EsDHiEl9MQd7D6eOJOlruhuV6ie/9XxprwdOpchU= +github.com/mmichaelb/redshift-data-sql-driver v0.4.0 h1:5vl8KRQqJTm2WsAgjF/0zUL7eMmNce3YtU3F6P6CMWc= +github.com/mmichaelb/redshift-data-sql-driver v0.4.0/go.mod h1:D1e55nmxuOialwOsqTruJ3EaKCfCL/zopZGj5uGMv/8= 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.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= +github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -212,8 +241,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf 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.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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= @@ -238,24 +267,33 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -263,6 +301,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -282,11 +322,14 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -294,11 +337,15 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -306,12 +353,18 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a h1:tPE/Kp+x9dMSwUm/uM0JKK0IfdiJkwAbSMSeZBXXJXc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo= google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/redshift/acc_test.go b/redshift/acc_test.go index 99cd7091..86ee8c8a 100644 --- a/redshift/acc_test.go +++ b/redshift/acc_test.go @@ -7,6 +7,8 @@ import ( "os" "strings" "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" ) // Get the value of an environment variable, or skip the @@ -25,3 +27,7 @@ func tfArray(s []string) string { tokens := strings.Split(semiformat, " ") return strings.Join(tokens, ",") } + +func generateRandomObjectName(prefix string) string { + return strings.ReplaceAll(acctest.RandomWithPrefix(prefix), "-", "_") +} diff --git a/redshift/config.go b/redshift/config.go index 49e37e5b..7b017001 100644 --- a/redshift/config.go +++ b/redshift/config.go @@ -3,11 +3,9 @@ package redshift import ( "database/sql" "fmt" - "net/url" - "strings" "sync" - _ "github.com/lib/pq" + _ "github.com/mmichaelb/redshift-data-sql-driver" ) var ( @@ -15,25 +13,35 @@ var ( dbRegistry = make(map[string]*DBConnection, 1) ) -// Config - provider config type Config struct { - Host string - Username string - Password string - Port int - Database string - SSLMode string - MaxConns int + DriverName string + ConnStr string + Database string + MaxConns int serverlessCheckMutex *sync.Mutex isServerless bool checkedForServerless bool + + usernameRetrievalMutex *sync.Mutex + retrievedUsername string +} + +func NewConfig(driverName, connStr, database string, maxConns int) *Config { + return &Config{ + DriverName: driverName, + ConnStr: connStr, + Database: database, + MaxConns: maxConns, + + serverlessCheckMutex: &sync.Mutex{}, + usernameRetrievalMutex: &sync.Mutex{}, + } } // Client struct holding connection string type Client struct { - config Config - databaseName string + config Config db *sql.DB } @@ -45,10 +53,9 @@ type DBConnection struct { } // NewClient returns client config for the specified database. -func (c *Config) NewClient(database string) *Client { +func (c *Config) NewClient() *Client { return &Client{ - config: *c, - databaseName: database, + config: *c, } } @@ -64,9 +71,10 @@ func (c *Config) IsServerless(db *DBConnection) (bool, error) { c.checkedForServerless = true - _, err := db.Query("SELECT 1 FROM SYS_SERVERLESS_USAGE") + rows, err := db.Query("SELECT 1 FROM SYS_SERVERLESS_USAGE") // No error means we have accessed the view and are running Redshift Serverless if err == nil { + defer rows.Close() c.isServerless = true return true, nil } @@ -80,6 +88,27 @@ func (c *Config) IsServerless(db *DBConnection) (bool, error) { return false, err } +func (c *Config) GetUsername(db *DBConnection) (string, error) { + if c.retrievedUsername != "" { + return c.retrievedUsername, nil + } + c.usernameRetrievalMutex.Lock() + defer c.usernameRetrievalMutex.Unlock() + if c.retrievedUsername != "" { + return c.retrievedUsername, nil + } + row := db.QueryRow("SELECT current_user;") + if row.Err() != nil { + return "", fmt.Errorf("error retrieving current user: %w", row.Err()) + } + var username string + if err := row.Scan(&username); err != nil { + return "", fmt.Errorf("error scanning current user: %w", err) + } + c.retrievedUsername = username + return c.retrievedUsername, nil +} + // Connect returns a copy to an sql.Open()'ed database connection wrapped in a DBConnection struct. // Callers must return their database resources. Use of QueryRow() or Exec() is encouraged. // Query() must have their rows.Close()'ed. @@ -87,17 +116,19 @@ func (c *Client) Connect() (*DBConnection, error) { dbRegistryLock.Lock() defer dbRegistryLock.Unlock() - dsn := c.config.connStr(c.databaseName) + dsn := c.config.ConnStr + driverName := c.config.DriverName conn, found := dbRegistry[dsn] - if !found { - db, err := sql.Open(proxyDriverName, dsn) + + if !found || conn.Ping() != nil { + db, err := sql.Open(driverName, dsn) if err != nil { - return nil, fmt.Errorf("error connecting to PostgreSQL server %q: %w", c.config.Host, err) + return nil, fmt.Errorf("error creating Redshift driver instance (driver: %q, dsn: %q): %w", driverName, dsn, err) } // We don't want to retain connection // So when we connect on a specific database which might be managed by terraform, - // we don't keep opened connection in case of the db has to be dopped in the plan. + // we don't keep opened connection in case of the db has to be dropped in the plan. db.SetMaxIdleConns(0) db.SetMaxOpenConns(c.config.MaxConns) @@ -105,62 +136,16 @@ func (c *Client) Connect() (*DBConnection, error) { db, c, } - dbRegistry[dsn] = conn - } - - return conn, nil -} - -func (c *Config) connStr(database string) string { - connStr := fmt.Sprintf( - "postgres://%s:%s@%s:%d/%s?%s", - url.QueryEscape(c.Username), - url.QueryEscape(c.Password), - c.Host, - c.Port, - database, - strings.Join(c.connParams(), "&"), - ) - - return connStr -} - -func (c *Config) connParams() []string { - params := map[string]string{} - - params["sslmode"] = c.SSLMode - params["connect_timeout"] = "180" - - var paramsArray []string - for key, value := range params { - paramsArray = append(paramsArray, fmt.Sprintf("%s=%s", key, url.QueryEscape(value))) - } - - return paramsArray -} -// Client instantiates a new Redshift client. -func (c *Config) Client() (*Client, error) { - - conninfo := fmt.Sprintf("sslmode=%v user=%v password=%v host=%v port=%v dbname=%v", - c.SSLMode, - c.Username, - c.Password, - c.Host, - c.Port, - c.Database) - - db, err := sql.Open(proxyDriverName, conninfo) - if err != nil { - return nil, err - } + _, err = c.config.GetUsername(conn) + if err != nil { + return nil, fmt.Errorf("error retrieving username from Redshift database (driver: %q, dsn: %q): %w", driverName, dsn, err) + } - client := Client{ - config: *c, - db: db, + dbRegistry[dsn] = conn } - return &client, nil + return conn, nil } func (c *Client) Close() { diff --git a/redshift/config_data_api.go b/redshift/config_data_api.go new file mode 100644 index 00000000..9bb58810 --- /dev/null +++ b/redshift/config_data_api.go @@ -0,0 +1,33 @@ +package redshift + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const redshiftDataDriverName = "redshift-data" + +func NewDataApiConfig(workgroupName, database, awsRegion string, maxConns int) *Config { + connStr := buildConnStrFromDataApiConfig(workgroupName, database, awsRegion) + return NewConfig(redshiftDataDriverName, connStr, database, maxConns) +} + +func buildConnStrFromDataApiConfig(workgroupName, database, awsRegion string) string { + return fmt.Sprintf( + "workgroup(%s)/%s?region=%s&transactionMode=non-transactional&requestMode=blocking", + workgroupName, database, awsRegion, + ) +} + +func getConfigFromDataApiResourceData(d *schema.ResourceData, database string) (*Config, error) { + workgroupName := d.Get("data_api.0.workgroup_name").(string) + if workgroupName == "" { + return nil, fmt.Errorf(`attribute "workgroup_name" is required in data_api configuration`) + } + region := d.Get("data_api.0.region").(string) + if region == "" { + return nil, fmt.Errorf(`attribute "region" is required in data_api configuration`) + } + return NewDataApiConfig(workgroupName, database, region, 1), nil +} diff --git a/redshift/config_pq_proxy.go b/redshift/config_pq_proxy.go new file mode 100644 index 00000000..e9ff5a1a --- /dev/null +++ b/redshift/config_pq_proxy.go @@ -0,0 +1,167 @@ +package redshift + +import ( + "context" + "fmt" + "log" + "net/url" + "sort" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" + "github.com/aws/aws-sdk-go-v2/service/redshift" + "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + _ "github.com/lib/pq" +) + +type temporaryCredentialsResolverFunc func(username string, d *schema.ResourceData) (string, string, error) + +func NewPqConfig(host, database, username, password string, port int, sslMode string, maxConns int) *Config { + connStr := buildConnStrFromPqConfig(host, database, username, password, port, sslMode, maxConns) + return NewConfig(proxyDriverName, connStr, database, maxConns) +} + +func buildConnStrFromPqConfig(host, database, username, password string, port int, sslMode string, maxConns int) string { + params := map[string]string{} + + params["sslmode"] = sslMode + params["connect_timeout"] = "180" + + var paramsArray []string + for key, value := range params { + paramsArray = append(paramsArray, fmt.Sprintf("%s=%s", key, url.QueryEscape(value))) + } + sort.Strings(paramsArray) + + return fmt.Sprintf( + "postgres://%s:%s@%s:%d/%s?%s", + url.QueryEscape(username), + url.QueryEscape(password), + host, + port, + database, + strings.Join(paramsArray, "&"), + ) +} +func getRequiredResourceDataValue[V int | string](d *schema.ResourceData, path string) (V, error) { + valueRaw, valuePresent := d.GetOk(path) + if !valuePresent { + var emptyValue V + return emptyValue, fmt.Errorf("attribute %q is required in pq configuration", path) + } + return valueRaw.(V), nil +} + +func getConfigFromPqResourceData(d *schema.ResourceData, database string, maxConnections int, temporaryCredentialsResolver temporaryCredentialsResolverFunc) (*Config, error) { + var err error + var host, username, password, sslMode string + var port int + if host, err = getRequiredResourceDataValue[string](d, "host"); err != nil { + return nil, err + } + if username, err = getRequiredResourceDataValue[string](d, "username"); err != nil { + return nil, err + } + if port, err = getRequiredResourceDataValue[int](d, "port"); err != nil { + return nil, err + } + if sslMode, err = getRequiredResourceDataValue[string](d, "sslmode"); err != nil { + return nil, err + } + _, useTemporaryCredentials := d.GetOk("temporary_credentials") + if useTemporaryCredentials { + log.Println("[DEBUG] using temporary credentials authentication") + username, password, err = temporaryCredentialsResolver(username, d) + if err != nil { + return nil, fmt.Errorf("failed to resolve temporary credentials: %w", err) + } + log.Printf("[DEBUG] got temporary credentials with username %s\n", username) + } else { + log.Println("[DEBUG] using password authentication") + if password, err = getRequiredResourceDataValue[string](d, "password"); err != nil { + return nil, err + } + } + return NewPqConfig(host, database, username, password, port, sslMode, maxConnections), nil +} + +// temporaryCredentials gets temporary credentials using GetClusterCredentials +func temporaryCredentials(username string, d *schema.ResourceData) (string, string, error) { + sdkClient, err := redshiftSdkClient(d) + if err != nil { + return "", "", err + } + clusterIdentifier, clusterIdentifierIsSet := d.GetOk("temporary_credentials.0.cluster_identifier") + if !clusterIdentifierIsSet { + return "", "", fmt.Errorf("temporary_credentials not configured") + } + input := &redshift.GetClusterCredentialsInput{ + ClusterIdentifier: aws.String(clusterIdentifier.(string)), + DbName: aws.String(d.Get("database").(string)), + DbUser: aws.String(username), + } + if autoCreateUser, ok := d.GetOk("temporary_credentials.0.auto_create_user"); ok { + input.AutoCreate = aws.Bool(autoCreateUser.(bool)) + } + if dbGroups, ok := d.GetOk("temporary_credentials.0.db_groups"); ok { + if dbGroups != nil { + dbGroupsList := dbGroups.(*schema.Set).List() + if len(dbGroupsList) > 0 { + var groups []string + for _, group := range dbGroupsList { + if group.(string) != "" { + groups = append(groups, group.(string)) + } + } + input.DbGroups = groups + } + } + } + if durationSeconds, ok := d.GetOk("temporary_credentials.0.duration_seconds"); ok { + duration := durationSeconds.(int) + if duration > 0 { + input.DurationSeconds = aws.Int32(int32(duration)) + } + } + log.Println("[DEBUG] making GetClusterCredentials request") + response, err := sdkClient.GetClusterCredentials(context.TODO(), input) + if err != nil { + return "", "", err + } + return aws.ToString(response.DbUser), aws.ToString(response.DbPassword), nil +} + +func redshiftSdkClient(d *schema.ResourceData) (*redshift.Client, error) { + cfg, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + return nil, err + } + + if region := d.Get("temporary_credentials.0.region").(string); region != "" { + cfg.Region = region + } + + if _, ok := d.GetOk("temporary_credentials.0.assume_role"); ok { + var parsedRoleArn string + if roleArn, ok := d.GetOk("temporary_credentials.0.assume_role.0.arn"); ok { + parsedRoleArn = roleArn.(string) + } + log.Printf("[DEBUG] Assuming role provided in configuration: [%s]", parsedRoleArn) + opts := func(options *stscreds.AssumeRoleOptions) { + options.Duration = time.Duration(defaultTemporaryCredentialsAssumeRoleDurationInSeconds) * time.Second + if externalID, ok := d.GetOk("temporary_credentials.0.assume_role.0.external_id"); ok { + options.ExternalID = aws.String(externalID.(string)) + } + if sessionName, ok := d.GetOk("temporary_credentials.0.assume_role.0.session_name"); ok { + options.RoleSessionName = sessionName.(string) + } + } + stsClient := sts.NewFromConfig(cfg) + cfg.Credentials = stscreds.NewAssumeRoleProvider(stsClient, parsedRoleArn, opts) + } + return redshift.NewFromConfig(cfg), nil +} diff --git a/redshift/data_source_redshift_group.go b/redshift/data_source_redshift_group.go index 170c6dbf..7f5cab2c 100644 --- a/redshift/data_source_redshift_group.go +++ b/redshift/data_source_redshift_group.go @@ -1,12 +1,12 @@ package redshift import ( + "fmt" "regexp" "strings" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - "github.com/lib/pq" ) func dataSourceRedshiftGroup() *schema.Resource { @@ -43,10 +43,31 @@ func dataSourceRedshiftGroupRead(db *DBConnection, d *schema.ResourceData) error groupUsers []string ) - sql := `SELECT ARRAY(SELECT u.usename FROM pg_user_info u, pg_group g WHERE g.groname = $1 AND u.usesysid = ANY(g.grolist)) AS members, grosysid FROM pg_group WHERE groname = $1` - if err := db.QueryRow(sql, d.Get(groupNameAttr).(string)).Scan(pq.Array(&groupUsers), &groupId); err != nil { + groupName := d.Get(groupNameAttr).(string) + + query := `SELECT u.usename, g.grosysid FROM pg_user_info u, pg_group g WHERE g.groname = $1 AND u.usesysid = ANY(g.grolist);` + rows, err := db.Query(query, groupName) + if err != nil { return err } + defer rows.Close() + for rows.Next() { + if err = rows.Err(); err != nil { + return fmt.Errorf("could not read group members for group name %q: %w", groupName, err) + } + var userName string + if err := rows.Scan(&userName, &groupId); err != nil { + return fmt.Errorf("could not read group members for group name %q: %w", groupName, err) + } + groupUsers = append(groupUsers, userName) + } + if len(groupUsers) == 0 { + // no users found so the group id could not be fetched, we have to query for the name + query = `SELECT grosysid FROM pg_group WHERE groname = $1;` + if err := db.QueryRow(query, groupName).Scan(&groupId); err != nil { + return err + } + } d.SetId(groupId) d.Set(groupUsersAttr, groupUsers) diff --git a/redshift/data_source_redshift_schema.go b/redshift/data_source_redshift_schema.go index cbc31a56..c36e4db8 100644 --- a/redshift/data_source_redshift_schema.go +++ b/redshift/data_source_redshift_schema.go @@ -276,7 +276,7 @@ func dataSourceRedshiftSchemaRead(db *DBConnection, d *schema.ResourceData) erro LEFT JOIN pg_user_info ON (svv_all_schemas.database_name = $1 AND pg_user_info.usesysid = svv_all_schemas.schema_owner) WHERE svv_all_schemas.database_name = $1 - AND svv_all_schemas.schema_name = $2`, db.client.databaseName, d.Get(schemaNameAttr).(string)).Scan(&schemaId, &schemaOwner, &schemaType) + AND svv_all_schemas.schema_name = $2`, db.client.config.Database, d.Get(schemaNameAttr).(string)).Scan(&schemaId, &schemaOwner, &schemaType) if err != nil { return err } diff --git a/redshift/helpers.go b/redshift/helpers.go index c8c15c0b..fa727735 100644 --- a/redshift/helpers.go +++ b/redshift/helpers.go @@ -26,13 +26,8 @@ const ( pgErrorCodeInsufficientPrivileges = "42501" ) -// startTransaction starts a new DB transaction on the specified database. -// If the database is specified and different from the one configured in the provider, -// it will create a new connection pool if needed. -func startTransaction(client *Client, database string) (*sql.Tx, error) { - if database != "" && database != client.databaseName { - client = client.config.NewClient(database) - } +// startTransaction starts a new DB transaction using the provided client. +func startTransaction(client *Client) (*sql.Tx, error) { db, err := client.Connect() if err != nil { return nil, err diff --git a/redshift/provider.go b/redshift/provider.go index 0afe4107..73b7296a 100644 --- a/redshift/provider.go +++ b/redshift/provider.go @@ -2,17 +2,11 @@ package redshift import ( "context" - "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "log" "regexp" - "time" - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/config" - "github.com/aws/aws-sdk-go-v2/credentials/stscreds" - "github.com/aws/aws-sdk-go-v2/service/redshift" - "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) @@ -26,16 +20,18 @@ func Provider() *schema.Provider { return &schema.Provider{ Schema: map[string]*schema.Schema{ "host": { - Type: schema.TypeString, - Description: "Name of Redshift server address to connect to.", - Required: true, - DefaultFunc: schema.EnvDefaultFunc("REDSHIFT_HOST", ""), + Type: schema.TypeString, + Description: "Name of Redshift server address to connect to.", + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("REDSHIFT_HOST", ""), + ConflictsWith: []string{"data_api"}, }, "username": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("REDSHIFT_USER", "root"), - Description: "Redshift user name to connect as.", + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("REDSHIFT_USER", "root"), + Description: "Redshift user name to connect as.", + ConflictsWith: []string{"data_api"}, }, "password": { Type: schema.TypeString, @@ -45,13 +41,15 @@ func Provider() *schema.Provider { Sensitive: true, ConflictsWith: []string{ "temporary_credentials", + "data_api", }, }, "port": { - Type: schema.TypeInt, - Description: "The Redshift port number to connect to at the server host.", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("REDSHIFT_PORT", 5439), + Type: schema.TypeInt, + Description: "The Redshift port number to connect to at the server host.", + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("REDSHIFT_PORT", 5439), + ConflictsWith: []string{"data_api"}, }, "sslmode": { Type: schema.TypeString, @@ -64,6 +62,7 @@ func Provider() *schema.Provider { "verify-ca", "verify-full", }, false), + ConflictsWith: []string{"data_api"}, }, "database": { Type: schema.TypeString, @@ -72,11 +71,48 @@ func Provider() *schema.Provider { DefaultFunc: schema.EnvDefaultFunc("REDSHIFT_DATABASE", "redshift"), }, "max_connections": { - Type: schema.TypeInt, - Optional: true, - Default: defaultProviderMaxOpenConnections, - Description: "Maximum number of connections to establish to the database. Zero means unlimited.", - ValidateFunc: validation.IntAtLeast(-1), + Type: schema.TypeInt, + Optional: true, + Default: defaultProviderMaxOpenConnections, + Description: "Maximum number of connections to establish to the database. Zero means unlimited.", + ValidateFunc: validation.IntAtLeast(-1), + ConflictsWith: []string{"data_api"}, + }, + "data_api": { + Type: schema.TypeList, + Optional: true, + Description: "Configuration for using the Redshift Data API. This can only be used for serverless Redshift clusters.", + MaxItems: 1, + ConflictsWith: []string{ + "host", + "username", + "password", + "port", + "sslmode", + "max_connections", + "temporary_credentials", + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "workgroup_name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the Redshift Serverless workgroup to connect to.", + DefaultFunc: schema.EnvDefaultFunc("REDSHIFT_DATA_API_SERVERLESS_WORKGROUP_NAME", nil), + // https://docs.aws.amazon.com/redshift-serverless/latest/APIReference/API_Workgroup.html#:~:text=Required%3A%20No-,workgroupName,-The%20name%20of + ValidateFunc: validation.All( + validation.StringLenBetween(3, 64), + validation.StringMatch(regexp.MustCompile("[a-z0-9-]+"), "must be lowercase alphanumeric or hyphen characters"), + ), + }, + "region": { + Type: schema.TypeString, + Required: true, + Description: "The AWS region where the Redshift Serverless workgroup is located. If not specified, the region will be determined from the AWS SDK configuration.", + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"AWS_REGION", "AWS_DEFAULT_REGION"}, nil), + }, + }, + }, }, "temporary_credentials": { Type: schema.TypeList, @@ -85,6 +121,7 @@ func Provider() *schema.Provider { MaxItems: 1, ConflictsWith: []string{ "password", + "data_api", }, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -130,6 +167,7 @@ func Provider() *schema.Provider { ResourcesMap: map[string]*schema.Resource{ "redshift_user": redshiftUser(), "redshift_group": redshiftGroup(), + "redshift_group_membership": redshiftGroupMembership(), "redshift_schema": redshiftSchema(), "redshift_default_privileges": redshiftDefaultPrivileges(), "redshift_grant": redshiftGrant(), @@ -149,118 +187,25 @@ func Provider() *schema.Provider { } func providerConfigure(_ context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { - username, password, err := resolveCredentials(d) + cfg, err := getConfigFromResourceData(d, temporaryCredentials) if err != nil { return nil, diag.FromErr(err) } - cfg := Config{ - Host: d.Get("host").(string), - Port: d.Get("port").(int), - Username: username, - Password: password, - Database: d.Get("database").(string), - SSLMode: d.Get("sslmode").(string), - MaxConns: d.Get("max_connections").(int), - } log.Println("[DEBUG] creating database client") - client := cfg.NewClient(d.Get("database").(string)) + client := cfg.NewClient() log.Println("[DEBUG] created database client") return client, nil } -func resolveCredentials(d *schema.ResourceData) (string, string, error) { - username, ok := d.GetOk("username") - if (!ok) || username == nil { - return "", "", fmt.Errorf("username is required") - } - if _, useTemporaryCredentials := d.GetOk("temporary_credentials"); useTemporaryCredentials { - log.Println("[DEBUG] using temporary credentials authentication") - dbUser, dbPassword, err := temporaryCredentials(username.(string), d) - log.Printf("[DEBUG] got temporary credentials with username %s\n", dbUser) - return dbUser, dbPassword, err - } - - password, _ := d.GetOk("password") - log.Println("[DEBUG] using password authentication") - return username.(string), password.(string), nil -} - -// temporaryCredentials gets temporary credentials using GetClusterCredentials -func temporaryCredentials(username string, d *schema.ResourceData) (string, string, error) { - sdkClient, err := redshiftSdkClient(d) - if err != nil { - return "", "", err - } - clusterIdentifier, clusterIdentifierIsSet := d.GetOk("temporary_credentials.0.cluster_identifier") - if !clusterIdentifierIsSet { - return "", "", fmt.Errorf("temporary_credentials not configured") - } - input := &redshift.GetClusterCredentialsInput{ - ClusterIdentifier: aws.String(clusterIdentifier.(string)), - DbName: aws.String(d.Get("database").(string)), - DbUser: aws.String(username), - } - if autoCreateUser, ok := d.GetOk("temporary_credentials.0.auto_create_user"); ok { - input.AutoCreate = aws.Bool(autoCreateUser.(bool)) - } - if dbGroups, ok := d.GetOk("temporary_credentials.0.db_groups"); ok { - if dbGroups != nil { - dbGroupsList := dbGroups.(*schema.Set).List() - if len(dbGroupsList) > 0 { - var groups []string - for _, group := range dbGroupsList { - if group.(string) != "" { - groups = append(groups, group.(string)) - } - } - input.DbGroups = groups - } - } - } - if durationSeconds, ok := d.GetOk("temporary_credentials.0.duration_seconds"); ok { - duration := durationSeconds.(int) - if duration > 0 { - input.DurationSeconds = aws.Int32(int32(duration)) - } - } - log.Println("[DEBUG] making GetClusterCredentials request") - response, err := sdkClient.GetClusterCredentials(context.TODO(), input) - if err != nil { - return "", "", err - } - return aws.ToString(response.DbUser), aws.ToString(response.DbPassword), nil -} - -func redshiftSdkClient(d *schema.ResourceData) (*redshift.Client, error) { - cfg, err := config.LoadDefaultConfig(context.TODO()) - if err != nil { - return nil, err - } - - if region := d.Get("temporary_credentials.0.region").(string); region != "" { - cfg.Region = region - } - - if _, ok := d.GetOk("temporary_credentials.0.assume_role"); ok { - var parsedRoleArn string - if roleArn, ok := d.GetOk("temporary_credentials.0.assume_role.0.arn"); ok { - parsedRoleArn = roleArn.(string) - } - log.Printf("[DEBUG] Assuming role provided in configuration: [%s]", parsedRoleArn) - opts := func(options *stscreds.AssumeRoleOptions) { - options.Duration = time.Duration(defaultTemporaryCredentialsAssumeRoleDurationInSeconds) * time.Second - if externalID, ok := d.GetOk("temporary_credentials.0.assume_role.0.external_id"); ok { - options.ExternalID = aws.String(externalID.(string)) - } - if sessionName, ok := d.GetOk("temporary_credentials.0.assume_role.0.session_name"); ok { - options.RoleSessionName = sessionName.(string) - } - } - stsClient := sts.NewFromConfig(cfg) - cfg.Credentials = stscreds.NewAssumeRoleProvider(stsClient, parsedRoleArn, opts) - } - return redshift.NewFromConfig(cfg), nil +func getConfigFromResourceData(d *schema.ResourceData, temporaryCredentialsResolver temporaryCredentialsResolverFunc) (*Config, error) { + database := d.Get("database").(string) + maxConnections := d.Get("max_connections").(int) + _, useDataApi := d.GetOk("data_api.0") + if useDataApi { + return getConfigFromDataApiResourceData(d, database) + } + return getConfigFromPqResourceData(d, database, maxConnections, temporaryCredentialsResolver) } func assumeRoleSchema() *schema.Schema { diff --git a/redshift/provider_test.go b/redshift/provider_test.go index ab4fbe7a..b04b6e83 100644 --- a/redshift/provider_test.go +++ b/redshift/provider_test.go @@ -2,6 +2,8 @@ package redshift import ( "context" + "fmt" + "log" "os" "strings" "testing" @@ -10,6 +12,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + redshiftdatasqldriver "github.com/mmichaelb/redshift-data-sql-driver" ) var ( @@ -42,6 +45,9 @@ func testAccPreCheck(t *testing.T) { if v := os.Getenv("REDSHIFT_USER"); v == "" { t.Fatal("REDSHIFT_USER must be set for acceptance tests") } + if v := os.Getenv("REDSHIFT_TEST_ACC_DEBUG_REDSHIFT_DATA"); v != "" { + redshiftdatasqldriver.SetDebugLogger(log.New(os.Stdout, "[redshift-data][debug]", log.Ldate|log.Ltime|log.Lshortfile)) + } } func initTemporaryCredentialsProvider(t *testing.T, provider *schema.Provider) { @@ -122,6 +128,22 @@ func TestAccRedshiftTemporaryCredentialsAssumeRole(t *testing.T) { defer db.Close() } +func TestAccRedshiftDataApiServerlessConnect(t *testing.T) { + _ = getEnvOrSkip("REDSHIFT_DATA_API_SERVERLESS_WORKGROUP_NAME", t) + defer unsetAndSetEnvVars("REDSHIFT_HOST")() + provider := Provider() + provider.Configure(context.Background(), terraform.NewResourceConfigRaw(map[string]interface{}{})) + client, ok := provider.Meta().(*Client) + if !ok { + t.Fatal("Unable to initialize client") + } + db, err := client.Connect() + if err != nil { + t.Fatalf("Unable to connect to database: %s", err) + } + defer db.Close() +} + func prepareRedshiftTemporaryCredentialsTestCases(t *testing.T, provider *schema.Provider) { redshiftPassword := os.Getenv("REDSHIFT_PASSWORD") defer os.Setenv("REDSHIFT_PASSWORD", redshiftPassword) @@ -132,3 +154,157 @@ func prepareRedshiftTemporaryCredentialsTestCases(t *testing.T, provider *schema os.Setenv("REDSHIFT_USER", username) initTemporaryCredentialsProvider(t, provider) } + +func Test_getConfigFromResourceData(t *testing.T) { + defer unsetAndSetEnvVars("AWS_REGION", "AWS_DEFAULT_REGION")() + type args struct { + d *schema.ResourceData + } + const tempUsername, tempPassword = "temp-user", "temp-password" + fakeTemporaryCredentialsResolver := func(username string, d *schema.ResourceData) (string, string, error) { + return tempUsername, tempPassword, nil + } + tests := []struct { + name string + args args + want *Config + wantErr bool + }{ + { + "Data API config", + args{ + d: schema.TestResourceDataRaw(t, Provider().Schema, map[string]interface{}{ + "database": "some-database", + "data_api": []interface{}{ + map[string]interface{}{ + "workgroup_name": "some-workgroup", + "region": "us-west-2", + }, + }, + }), + }, + &Config{ + DriverName: redshiftDataDriverName, + ConnStr: "workgroup(some-workgroup)/some-database?region=us-west-2&transactionMode=non-transactional&requestMode=blocking", + Database: "some-database", + MaxConns: 1, + }, + false, + }, + { + "Data API config missing region", + args{ + d: schema.TestResourceDataRaw(t, Provider().Schema, map[string]interface{}{ + "database": "some-database", + "data_api": []interface{}{ + map[string]interface{}{ + "workgroup_name": "some-workgroup", + }, + }, + }), + }, + nil, + true, + }, + { + "PQ config", + args{ + d: schema.TestResourceDataRaw(t, Provider().Schema, map[string]interface{}{ + "username": "some-user", + "password": "some-pw", + "host": "some-host", + "port": 4122, + "database": "some-database", + "sslmode": "require", + "max_connections": 10, + }), + }, + &Config{ + DriverName: "postgresql-proxy", + ConnStr: "postgres://some-user:some-pw@some-host:4122/some-database?connect_timeout=180&sslmode=require", + Database: "some-database", + MaxConns: 10, + }, + false, + }, + { + "PQ config - fake temporary credentials", + args{ + d: schema.TestResourceDataRaw(t, Provider().Schema, map[string]interface{}{ + "username": "some-user", + "host": "some-host", + "port": 4122, + "database": "some-database", + "sslmode": "require", + "temporary_credentials": []interface{}{ + map[string]interface{}{ + "cluster_identifier": "some-cluster", + }, + }, + }), + }, + &Config{ + DriverName: "postgresql-proxy", + ConnStr: fmt.Sprintf("postgres://%s:%s@some-host:4122/some-database?connect_timeout=180&sslmode=require", tempUsername, tempPassword), + Database: "some-database", + MaxConns: 20, + }, + false, + }, + { + "PQ config - missing host", + args{ + d: schema.TestResourceDataRaw(t, Provider().Schema, map[string]interface{}{ + "username": "some-user", + "port": 4122, + "database": "some-database", + "sslmode": "require", + }), + }, + nil, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getConfigFromResourceData(tt.args.d, fakeTemporaryCredentialsResolver) + if (err != nil) != tt.wantErr { + t.Errorf("getConfigFromResourceData() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantErr { + return + } + if tt.want.ConnStr != got.ConnStr { + t.Errorf("getConfigFromResourceData() ConnStr = %q, want %q", got.ConnStr, tt.want.ConnStr) + } + if tt.want.DriverName != got.DriverName { + t.Errorf("getConfigFromResourceData() DriverName = %q, want %q", got.DriverName, tt.want.DriverName) + } + if tt.want.MaxConns != got.MaxConns { + t.Errorf("getConfigFromResourceData() MaxConns = %d, want %d", got.MaxConns, tt.want.MaxConns) + } + if tt.want.Database != got.Database { + t.Errorf("getConfigFromResourceData() Database = %d, want %d", got.MaxConns, tt.want.MaxConns) + } + }) + } +} + +func unsetAndSetEnvVars(envName ...string) func() { + envValues := make(map[string]string) + for _, env := range envName { + envValue := os.Getenv(env) + if envValue != "" { + envValues[env] = envValue + os.Unsetenv(env) + } + } + return func() { + for key, value := range envValues { + if err := os.Setenv(key, value); err != nil { + fmt.Printf("Failed to set environment variable %s: %v\n", key, err) + } + } + } +} diff --git a/redshift/resource_redshift_database.go b/redshift/resource_redshift_database.go index b40692c3..c4a65f07 100644 --- a/redshift/resource_redshift_database.go +++ b/redshift/resource_redshift_database.go @@ -140,7 +140,7 @@ func resourceRedshiftDatabaseCreateFromDatashare(db *DBConnection, d *schema.Res // CREATE DATABASE isn't allowed to run inside a transaction, however ALTER DATABASE // can be - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } @@ -251,7 +251,7 @@ WHERE pg_database_info.datid = $1 } func resourceRedshiftDatabaseUpdate(db *DBConnection, d *schema.ResourceData) error { - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } diff --git a/redshift/resource_redshift_datashare.go b/redshift/resource_redshift_datashare.go index 2a053dcd..0d15506f 100644 --- a/redshift/resource_redshift_datashare.go +++ b/redshift/resource_redshift_datashare.go @@ -97,7 +97,7 @@ such as RA3. } func resourceRedshiftDatashareCreate(db *DBConnection, d *schema.ResourceData) error { - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } @@ -242,7 +242,7 @@ func resourceRedshiftDatashareRead(db *DBConnection, d *schema.ResourceData) err var shareName, owner, producerAccount, producerNamespace, created string var publicAccessible bool - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } @@ -313,7 +313,7 @@ func readDatashareSchemas(tx *sql.Tx, shareName string, d *schema.ResourceData) } func resourceRedshiftDatashareUpdate(db *DBConnection, d *schema.ResourceData) error { - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } @@ -405,7 +405,7 @@ func setDatashareSchemas(tx *sql.Tx, d *schema.ResourceData) error { } func resourceRedshiftDatashareDelete(db *DBConnection, d *schema.ResourceData) error { - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } diff --git a/redshift/resource_redshift_default_privileges.go b/redshift/resource_redshift_default_privileges.go index 86ac1f7a..03ce0dbf 100644 --- a/redshift/resource_redshift_default_privileges.go +++ b/redshift/resource_redshift_default_privileges.go @@ -99,7 +99,7 @@ func redshiftDefaultPrivileges() *schema.Resource { func resourceRedshiftDefaultPrivilegesDelete(db *DBConnection, d *schema.ResourceData) error { revokeAlterDefaultQuery := createAlterDefaultsRevokeQuery(d) - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } @@ -125,7 +125,7 @@ func resourceRedshiftDefaultPrivilegesCreate(db *DBConnection, d *schema.Resourc return fmt.Errorf(`invalid privileges list %+v for object type %q`, privileges, objectType) } - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } @@ -162,7 +162,7 @@ func resourceRedshiftDefaultPrivilegesReadImpl(db *DBConnection, d *schema.Resou schemaName, schemaNameSet := d.GetOk(defaultPrivilegesSchemaAttr) ownerName := d.Get(defaultPrivilegesOwnerAttr).(string) - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } diff --git a/redshift/resource_redshift_default_privileges_test.go b/redshift/resource_redshift_default_privileges_test.go index 805f2e8a..e5b73910 100644 --- a/redshift/resource_redshift_default_privileges_test.go +++ b/redshift/resource_redshift_default_privileges_test.go @@ -4,6 +4,7 @@ import ( "database/sql" "errors" "fmt" + "os" "regexp" "strings" "testing" @@ -22,6 +23,7 @@ func TestAccRedshiftDefaultPrivileges_Basic(t *testing.T) { strings.ReplaceAll(acctest.RandomWithPrefix("tf_acc_user"), "-", "_"), strings.ReplaceAll(acctest.RandomWithPrefix("tf_acc_user@tf_acc_domain.tld"), "-", "_"), } + rootUsername := getRootUsername() for i, groupName := range groupNames { userName := userNames[i] @@ -37,18 +39,18 @@ func TestAccRedshiftDefaultPrivileges_Basic(t *testing.T) { resource "redshift_default_privileges" "group" { group = redshift_group.group.name - owner = "root" + owner = %[3]q object_type = "table" privileges = ["select", "update", "insert", "delete", "drop", "references", "rule", "trigger"] } resource "redshift_default_privileges" "user" { user = redshift_user.user.name - owner = "root" + owner = %[3]q object_type = "table" privileges = ["select", "update", "insert", "delete", "drop", "references", "rule", "trigger"] } - `, groupName, userName) + `, groupName, userName, rootUsername) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: testAccProviders, @@ -57,7 +59,7 @@ func TestAccRedshiftDefaultPrivileges_Basic(t *testing.T) { { Config: config, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("redshift_default_privileges.group", "id", fmt.Sprintf("gn:%s_noschema_on:root_ot:table", groupName)), + resource.TestCheckResourceAttr("redshift_default_privileges.group", "id", fmt.Sprintf("gn:%s_noschema_on:%s_ot:table", groupName, rootUsername)), resource.TestCheckResourceAttr("redshift_default_privileges.group", "group", groupName), resource.TestCheckResourceAttr("redshift_default_privileges.group", "object_type", "table"), resource.TestCheckResourceAttr("redshift_default_privileges.group", "privileges.#", "8"), @@ -70,7 +72,7 @@ func TestAccRedshiftDefaultPrivileges_Basic(t *testing.T) { resource.TestCheckTypeSetElemAttr("redshift_default_privileges.group", "privileges.*", "rule"), resource.TestCheckTypeSetElemAttr("redshift_default_privileges.group", "privileges.*", "trigger"), - resource.TestCheckResourceAttr("redshift_default_privileges.user", "id", fmt.Sprintf("un:%s_noschema_on:root_ot:table", userName)), + resource.TestCheckResourceAttr("redshift_default_privileges.user", "id", fmt.Sprintf("un:%s_noschema_on:%s_ot:table", userName, rootUsername)), resource.TestCheckResourceAttr("redshift_default_privileges.user", "user", userName), resource.TestCheckResourceAttr("redshift_default_privileges.user", "object_type", "table"), resource.TestCheckResourceAttr("redshift_default_privileges.user", "privileges.#", "8"), @@ -98,6 +100,7 @@ func TestAccRedshiftDefaultPrivileges_UpdateToRevoke(t *testing.T) { strings.ReplaceAll(acctest.RandomWithPrefix("tf_acc_user"), "-", "_"), strings.ReplaceAll(acctest.RandomWithPrefix("tf_acc_user@tf_acc_domain.tld"), "-", "_"), } + rootUsername := getRootUsername() for i, groupName := range groupNames { userName := userNames[i] @@ -113,18 +116,18 @@ func TestAccRedshiftDefaultPrivileges_UpdateToRevoke(t *testing.T) { resource "redshift_default_privileges" "group" { group = redshift_group.group.name - owner = "root" + owner = %[3]q object_type = "table" privileges = ["select", "update", "insert", "delete", "drop", "references", "rule", "trigger"] } resource "redshift_default_privileges" "user" { user = redshift_user.user.name - owner = "root" + owner = %[3]q object_type = "table" privileges = ["select", "update", "insert", "delete", "drop", "references", "rule", "trigger"] } - `, groupName, userName) + `, groupName, userName, rootUsername) configUpdated := fmt.Sprintf(` resource "redshift_group" "group" { @@ -138,18 +141,18 @@ func TestAccRedshiftDefaultPrivileges_UpdateToRevoke(t *testing.T) { resource "redshift_default_privileges" "group" { group = redshift_group.group.name - owner = "root" + owner = %[3]q object_type = "table" privileges = [] } resource "redshift_default_privileges" "user" { user = redshift_user.user.name - owner = "root" + owner = %[3]q object_type = "table" privileges = [] } - `, groupName, userName) + `, groupName, userName, rootUsername) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: testAccProviders, @@ -158,7 +161,7 @@ func TestAccRedshiftDefaultPrivileges_UpdateToRevoke(t *testing.T) { { Config: configInitial, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("redshift_default_privileges.group", "id", fmt.Sprintf("gn:%s_noschema_on:root_ot:table", groupName)), + resource.TestCheckResourceAttr("redshift_default_privileges.group", "id", fmt.Sprintf("gn:%s_noschema_on:%s_ot:table", groupName, rootUsername)), resource.TestCheckResourceAttr("redshift_default_privileges.group", "group", groupName), resource.TestCheckResourceAttr("redshift_default_privileges.group", "object_type", "table"), resource.TestCheckResourceAttr("redshift_default_privileges.group", "privileges.#", "8"), @@ -171,7 +174,7 @@ func TestAccRedshiftDefaultPrivileges_UpdateToRevoke(t *testing.T) { resource.TestCheckTypeSetElemAttr("redshift_default_privileges.group", "privileges.*", "rule"), resource.TestCheckTypeSetElemAttr("redshift_default_privileges.group", "privileges.*", "trigger"), - resource.TestCheckResourceAttr("redshift_default_privileges.user", "id", fmt.Sprintf("un:%s_noschema_on:root_ot:table", userName)), + resource.TestCheckResourceAttr("redshift_default_privileges.user", "id", fmt.Sprintf("un:%s_noschema_on:%s_ot:table", userName, rootUsername)), resource.TestCheckResourceAttr("redshift_default_privileges.user", "user", userName), resource.TestCheckResourceAttr("redshift_default_privileges.user", "object_type", "table"), resource.TestCheckResourceAttr("redshift_default_privileges.user", "privileges.#", "8"), @@ -188,12 +191,12 @@ func TestAccRedshiftDefaultPrivileges_UpdateToRevoke(t *testing.T) { { Config: configUpdated, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("redshift_default_privileges.group", "id", fmt.Sprintf("gn:%s_noschema_on:root_ot:table", groupName)), + resource.TestCheckResourceAttr("redshift_default_privileges.group", "id", fmt.Sprintf("gn:%s_noschema_on:%s_ot:table", groupName, rootUsername)), resource.TestCheckResourceAttr("redshift_default_privileges.group", "group", groupName), resource.TestCheckResourceAttr("redshift_default_privileges.group", "object_type", "table"), resource.TestCheckResourceAttr("redshift_default_privileges.group", "privileges.#", "0"), - resource.TestCheckResourceAttr("redshift_default_privileges.user", "id", fmt.Sprintf("un:%s_noschema_on:root_ot:table", userName)), + resource.TestCheckResourceAttr("redshift_default_privileges.user", "id", fmt.Sprintf("un:%s_noschema_on:%s_ot:table", userName, rootUsername)), resource.TestCheckResourceAttr("redshift_default_privileges.user", "user", userName), resource.TestCheckResourceAttr("redshift_default_privileges.user", "object_type", "table"), resource.TestCheckResourceAttr("redshift_default_privileges.user", "privileges.#", "0"), @@ -205,16 +208,17 @@ func TestAccRedshiftDefaultPrivileges_UpdateToRevoke(t *testing.T) { } func TestAccRedshiftDefaultPrivileges_BothUserGroupError(t *testing.T) { - config := ` + rootUsername := getRootUsername() + config := fmt.Sprintf(` resource "redshift_default_privileges" "both" { user = "test_user" group = "test_group" - owner = "root" + owner = %[1]q object_type = "table" privileges = [] } -` +`, rootUsername) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: testAccProviders, @@ -228,13 +232,14 @@ resource "redshift_default_privileges" "both" { } func TestAccRedshiftDefaultPrivileges_NoUserGroupError(t *testing.T) { - config := ` + rootUsername := getRootUsername() + config := fmt.Sprintf(` resource "redshift_default_privileges" "none" { - owner = "root" + owner = %[1]q object_type = "table" privileges = [] } -` +`, rootUsername) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: testAccProviders, @@ -294,3 +299,11 @@ func checkDefACLExists(client *Client, schemaID, ownerID int, objectType, groupN return true, nil } + +func getRootUsername() string { + envRootUsername := os.Getenv("REDSHIFT_ROOT_USERNAME") + if envRootUsername == "" { + return "root" + } + return envRootUsername +} diff --git a/redshift/resource_redshift_grant.go b/redshift/resource_redshift_grant.go index f254d4c4..36871d4f 100644 --- a/redshift/resource_redshift_grant.go +++ b/redshift/resource_redshift_grant.go @@ -156,7 +156,7 @@ func resourceRedshiftGrantCreate(db *DBConnection, d *schema.ResourceData) error databaseName := getDatabaseName(db, d) - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } @@ -180,7 +180,7 @@ func resourceRedshiftGrantCreate(db *DBConnection, d *schema.ResourceData) error } func resourceRedshiftGrantDelete(db *DBConnection, d *schema.ResourceData) error { - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } @@ -544,6 +544,7 @@ func readCallableGrants(db *DBConnection, d *schema.ResourceData) error { if err != nil { return err } + defer rows.Close() contains := func(callables []string, objName string) bool { for _, callable := range callables { @@ -553,7 +554,6 @@ func readCallableGrants(db *DBConnection, d *schema.ResourceData) error { } return false } - defer rows.Close() privilegesSet := schema.NewSet(schema.HashString, nil) for rows.Next() { @@ -626,9 +626,9 @@ func readLanguageGrants(db *DBConnection, d *schema.ResourceData) error { if err != nil { return err } + defer rows.Close() objects := d.Get(grantObjectsAttr).(*schema.Set) - defer rows.Close() for rows.Next() { var objName string @@ -842,7 +842,7 @@ func createGrantsQuery(d *schema.ResourceData, databaseName string) string { } func getDatabaseName(db *DBConnection, d *schema.ResourceData) string { - databaseName := db.client.databaseName + databaseName := db.client.config.Database if database, ok := d.GetOk(grantDatabaseAttr); ok { databaseName = database.(string) } diff --git a/redshift/resource_redshift_grant_test.go b/redshift/resource_redshift_grant_test.go index 71b422f1..10a2e8df 100644 --- a/redshift/resource_redshift_grant_test.go +++ b/redshift/resource_redshift_grant_test.go @@ -25,6 +25,10 @@ resource "redshift_grant" "public" { schema = %[1]q object_type = "schema" privileges = ["create", "usage"] + + depends_on = [ + redshift_schema.test + ] } # Add user with different privileges to see if we do not catch them by accident @@ -37,6 +41,10 @@ resource "redshift_grant" "user" { schema = %[1]q object_type = "schema" privileges = ["usage"] + + depends_on = [ + redshift_schema.test + ] } `, schemaName, userName) diff --git a/redshift/resource_redshift_group.go b/redshift/resource_redshift_group.go index 9cf70df1..a3eb4fea 100644 --- a/redshift/resource_redshift_group.go +++ b/redshift/resource_redshift_group.go @@ -64,10 +64,29 @@ func resourceRedshiftGroupReadImpl(db *DBConnection, d *schema.ResourceData) err groupUsers []string ) - query := `SELECT ARRAY(SELECT u.usename FROM pg_user_info u, pg_group g WHERE g.grosysid = $1 AND u.usesysid = ANY(g.grolist)) AS members, groname FROM pg_group WHERE grosysid = $1` - if err := db.QueryRow(query, d.Id()).Scan(pq.Array(&groupUsers), &groupName); err != nil { + query := `SELECT groname, u.usename FROM pg_user_info u, pg_group g WHERE g.grosysid = $1 AND u.usesysid = ANY(g.grolist);` + rows, err := db.Query(query, d.Id()) + if err != nil { return err } + defer rows.Close() + for rows.Next() { + if err = rows.Err(); err != nil { + return fmt.Errorf("could not read group members for group id %q: %w", d.Id(), err) + } + var userName string + if err := rows.Scan(&groupName, &userName); err != nil { + return fmt.Errorf("could not read group members for group id %q: %w", d.Id(), err) + } + groupUsers = append(groupUsers, userName) + } + if len(groupUsers) == 0 { + // no users found so the group name could not be fetched, we have to query for the name + query = `SELECT groname FROM pg_group WHERE grosysid = $1;` + if err := db.QueryRow(query, d.Id()).Scan(&groupName); err != nil { + return err + } + } d.Set(groupNameAttr, groupName) d.Set(groupUsersAttr, groupUsers) @@ -78,7 +97,7 @@ func resourceRedshiftGroupReadImpl(db *DBConnection, d *schema.ResourceData) err func resourceRedshiftGroupCreate(db *DBConnection, d *schema.ResourceData) error { groupName := d.Get(groupNameAttr).(string) - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } @@ -117,7 +136,7 @@ func resourceRedshiftGroupCreate(db *DBConnection, d *schema.ResourceData) error func resourceRedshiftGroupDelete(db *DBConnection, d *schema.ResourceData) error { groupName := d.Get(groupNameAttr).(string) - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } @@ -151,7 +170,7 @@ func resourceRedshiftGroupDelete(db *DBConnection, d *schema.ResourceData) error } func resourceRedshiftGroupUpdate(db *DBConnection, d *schema.ResourceData) error { - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } diff --git a/redshift/resource_redshift_group_membership.go b/redshift/resource_redshift_group_membership.go new file mode 100644 index 00000000..31a343c1 --- /dev/null +++ b/redshift/resource_redshift_group_membership.go @@ -0,0 +1,204 @@ +package redshift + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/lib/pq" +) + +func redshiftGroupMembership() *schema.Resource { + return &schema.Resource{ + Description: fmt.Sprintf(` +Manages Redshift group memberships. Allows either to exclusively manage group memberships or to add members to an existing group. Note: this resource conflicts with the %s attribute of the %s resource +`, "`users`", "`redshift_group`"), + CreateContext: ResourceFunc(resourceRedshiftGroupMembershipCreate), + ReadContext: ResourceFunc(resourceRedshiftGroupMembershipRead), + UpdateContext: ResourceFunc(resourceRedshiftGroupMembershipUpdate), + DeleteContext: ResourceFunc(resourceRedshiftGroupMembershipDelete), + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + groupNameAttr: { + Type: schema.TypeString, + Required: true, + Description: "Name of the user group.", + ValidateFunc: validation.StringLenBetween(1, 127), + }, + groupUsersAttr: { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: "List of the user names to add to the group. Note: this resource does not check whether the specified users exist.", + }, + }, + } +} + +func resourceRedshiftGroupMembershipCreate(db *DBConnection, d *schema.ResourceData) error { + groupName := d.Get(groupNameAttr).(string) + userNames := parseUserNames(d.Get(groupUsersAttr)) + + if len(userNames) == 0 { + return fmt.Errorf("at least one user must be specified in %q", groupUsersAttr) + } + + if err := addUsersToGroup(db, groupName, userNames); err != nil { + return err + } + + return resourceRedshiftGroupMembershipRead(db, d) +} + +func addUsersToGroup(db *DBConnection, group string, userNames []string) error { + if len(userNames) == 0 { + return nil + } + userNamesParam := buildUserStringArray(userNames, false) + query := fmt.Sprintf("ALTER GROUP %s ADD USER %s;", pq.QuoteIdentifier(group), userNamesParam) + + if _, err := db.Exec(query); err != nil { + return fmt.Errorf("could not add users %s to group %q: %w", userNamesParam, group, err) + } + return nil + +} + +func resourceRedshiftGroupMembershipRead(db *DBConnection, d *schema.ResourceData) error { + groupName := d.Get(groupNameAttr).(string) + userNames := parseUserNames(d.Get(groupUsersAttr)) + + userNamesParam := buildUserStringArray(userNames, true) + + query := fmt.Sprintf( + `SELECT 1 FROM pg_group pgg JOIN pg_user pgu ON pgu.usesysid = ANY(pgg.grolist) WHERE pgg.groname = %s AND pgu.usename IN (%s);`, + pq.QuoteLiteral(groupName), userNamesParam, + ) + + rows, err := db.Query(query) + if err != nil { + return err + } + defer rows.Close() + if rows.Next() { + if err = rows.Err(); err != nil { + return fmt.Errorf("could not read group membership for group %q: %w", groupName, err) + } + d.SetId(generateGroupMembershipId(groupName, userNames)) + } else { + d.SetId("") + } + return nil +} + +func resourceRedshiftGroupMembershipUpdate(db *DBConnection, d *schema.ResourceData) error { + rawUserNamesOld, rawUserNamesNew := d.GetChange(groupUsersAttr) + oldUserNames := parseUserNames(rawUserNamesOld) + newUserNames := parseUserNames(rawUserNamesNew) + if len(newUserNames) == 0 { + return fmt.Errorf("at least one user must be specified in %q", groupUsersAttr) + } + if d.HasChange(groupNameAttr) { + if err := resourceRedshiftGroupMembershipDelete(db, d); err != nil { + return fmt.Errorf("error deleting group membership while updating the resource: %w", err) + } + if err := resourceRedshiftGroupMembershipCreate(db, d); err != nil { + return fmt.Errorf("error creating group membership while updating the resource: %w", err) + } + return nil + } + deletedUserNames, addedUserNames := calculateUserNamesDiff(oldUserNames, newUserNames) + if err := dropUsersFromGroup(db, d.Get(groupNameAttr).(string), deletedUserNames); err != nil { + return fmt.Errorf("error removing users from group while updating the resource: %w", err) + } + if err := addUsersToGroup(db, d.Get(groupNameAttr).(string), addedUserNames); err != nil { + return fmt.Errorf("error adding users to group while updating the resource: %w", err) + } + return resourceRedshiftGroupMembershipRead(db, d) +} + +func calculateUserNamesDiff(oldUserNames, newUserNames []string) (deletedUserNames, addedUserNames []string) { + deletedUserNames = make([]string, 0) + addedUserNames = make([]string, 0) + for _, oldUserName := range oldUserNames { + found := false + for _, newUserName := range newUserNames { + if oldUserName == newUserName { + found = true + break + } + } + if !found { + deletedUserNames = append(deletedUserNames, oldUserName) + } + } + for _, newUserName := range newUserNames { + found := false + for _, oldUserName := range oldUserNames { + if newUserName == oldUserName { + found = true + break + } + } + if !found { + addedUserNames = append(addedUserNames, newUserName) + } + } + return deletedUserNames, addedUserNames +} + +func resourceRedshiftGroupMembershipDelete(db *DBConnection, d *schema.ResourceData) error { + groupName := d.Get(groupNameAttr).(string) + userNames := parseUserNames(d.Get(groupUsersAttr)) + + return dropUsersFromGroup(db, groupName, userNames) +} + +func dropUsersFromGroup(db *DBConnection, groupName string, userNames []string) error { + if len(userNames) == 0 { + return nil + } + userNamesParam := buildUserStringArray(userNames, false) + query := fmt.Sprintf("ALTER GROUP %s DROP USER %s;", pq.QuoteIdentifier(groupName), userNamesParam) + + if _, err := db.Exec(query); err != nil { + return fmt.Errorf("could not remove users %s from group %q: %w", userNamesParam, groupName, err) + } + return nil +} + +func parseUserNames(rawUserNames interface{}) []string { + rawUserNamesTyped := rawUserNames.(*schema.Set).List() + userNames := make([]string, len(rawUserNamesTyped)) + for index, userNameRaw := range rawUserNamesTyped { + userNames[index] = userNameRaw.(string) + } + return userNames +} + +func buildUserStringArray(userNames []string, encodeAsLiteral bool) string { + var userNamesSafe []string + for _, userName := range userNames { + encodedUserName := strings.ToLower(userName) + if encodeAsLiteral { + encodedUserName = pq.QuoteLiteral(encodedUserName) + } else { + encodedUserName = pq.QuoteIdentifier(encodedUserName) + } + userNamesSafe = append(userNamesSafe, encodedUserName) + } + return strings.Join(userNamesSafe, ", ") +} + +func generateGroupMembershipId(groupName string, userNames []string) string { + var idBuilder strings.Builder + idBuilder.WriteString(groupName) + for _, userName := range userNames { + idBuilder.WriteString("_") + idBuilder.WriteString(strings.ToLower(userName)) + } + return idBuilder.String() +} diff --git a/redshift/resource_redshift_group_membership_test.go b/redshift/resource_redshift_group_membership_test.go new file mode 100644 index 00000000..a2b7b277 --- /dev/null +++ b/redshift/resource_redshift_group_membership_test.go @@ -0,0 +1,492 @@ +package redshift + +import ( + "database/sql" + "errors" + "fmt" + "reflect" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccRedshiftGroupMembership_Basic(t *testing.T) { + groupName := generateRandomObjectName("tf_acc_group_membership") + userName := generateRandomObjectName("tf_acc_group_membership_user") + config := fmt.Sprintf(` +resource "redshift_group" "simple" { + name = %[1]q + + lifecycle { + ignore_changes = [ + users + ] + } +} + +resource "redshift_user" "simple" { + name = %[2]q +} + +resource "redshift_group_membership" "simple" { + name = redshift_group.simple.name + users = [redshift_user.simple.name] +} +`, groupName, userName) + updateConfig := fmt.Sprintf(` +resource "redshift_group" "simple" { + name = %[1]q + + lifecycle { + ignore_changes = [ + users + ] + } +} + +resource "redshift_user" "simple" { + name = %[2]q +} +`, groupName, userName) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckRedshiftGroupMembershipDestroy, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("redshift_group_membership.simple", "name", groupName), + resource.TestCheckResourceAttr("redshift_group_membership.simple", "users.0", userName), + testAccCheckRedshiftGroupMembershipPresence(groupName, userName, true), + ), + }, + { + Config: updateConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckRedshiftGroupMembershipPresence(groupName, userName, false), + ), + }, + }, + }) +} + +func TestAccRedshiftGroupMembership_UserRemove(t *testing.T) { + groupName := generateRandomObjectName("tf_acc_group_membership") + userName := generateRandomObjectName("tf_acc_group_membership_user") + config1 := fmt.Sprintf(` +resource "redshift_group" "simple" { + name = %[1]q + + lifecycle { + ignore_changes = [ + users + ] + } +} + +resource "redshift_user" "simple" { + name = %[2]q +} +`, groupName, userName) + config2 := fmt.Sprintf(` +resource "redshift_group" "simple" { + name = %[1]q + + lifecycle { + ignore_changes = [ + users + ] + } +} + +resource "redshift_user" "simple" { + name = %[2]q +} + +resource "redshift_group_membership" "simple" { + name = redshift_group.simple.name + users = [%[2]q] +} +`, groupName, userName) + config3 := fmt.Sprintf(` +resource "redshift_group" "simple" { + name = %[1]q + + lifecycle { + ignore_changes = [ + users + ] + } +} + +resource "redshift_group_membership" "simple" { + name = redshift_group.simple.name + users = [%[2]q] +} +`, groupName, userName) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckRedshiftGroupMembershipDestroy, + Steps: []resource.TestStep{ + { + Config: config1, + }, + { + Config: config2, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("redshift_group_membership.simple", "name", groupName), + resource.TestCheckResourceAttr("redshift_group_membership.simple", "users.0", userName), + testAccCheckRedshiftGroupMembershipPresence(groupName, userName, true), + ), + }, + { + Config: config3, + ExpectError: regexp.MustCompile("(?s)After applying this test step and performing a `terraform refresh`, the plan was not empty.*redshift_group_membership.simple will be created"), + }, + }, + }) +} + +func TestAccRedshiftGroupMembership_Update(t *testing.T) { + groupName := generateRandomObjectName("tf_acc_group_membership") + newGroupName := generateRandomObjectName("tf_acc_group_membership") + userName1 := generateRandomObjectName("tf_acc_group_membership_user") + userName2 := generateRandomObjectName("tf_acc_group_membership_user") + userName3 := generateRandomObjectName("tf_acc_group_membership_user") + config1 := fmt.Sprintf(` +resource "redshift_group" "simple" { + name = %[1]q + + lifecycle { + ignore_changes = [ + users + ] + } +} + +resource "redshift_user" "simple" { + name = %[2]q +} + +resource "redshift_group_membership" "simple" { + name = redshift_group.simple.name + users = [redshift_user.simple.name] +} +`, groupName, userName1) + config2 := fmt.Sprintf(` +resource "redshift_group" "simple" { + name = %[1]q + + lifecycle { + ignore_changes = [ + users + ] + } +} + +resource "redshift_user" "simple" { + name = %[2]q +} + +resource "redshift_user" "also_simple" { + name = %[3]q +} + +resource "redshift_group_membership" "simple" { + name = redshift_group.simple.name + users = [redshift_user.simple.name, redshift_user.also_simple.name] +} +`, newGroupName, userName1, userName2) + config3 := fmt.Sprintf(` +resource "redshift_group" "simple" { + name = %[1]q + + lifecycle { + ignore_changes = [ + users + ] + } +} + +resource "redshift_user" "simple" { + name = %[2]q +} + +resource "redshift_user" "also_simple" { + name = %[3]q +} + +resource "redshift_user" "third_simple" { + name = %[4]q +} + +resource "redshift_group_membership" "simple" { + name = redshift_group.simple.name + users = [redshift_user.simple.name, redshift_user.third_simple.name] +} +`, newGroupName, userName1, userName2, userName3) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckRedshiftGroupMembershipDestroy, + Steps: []resource.TestStep{ + { + Config: config1, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("redshift_group_membership.simple", "name", groupName), + resource.TestCheckResourceAttr("redshift_group_membership.simple", "users.0", userName1), + testAccCheckRedshiftGroupMembershipPresence(groupName, userName1, true), + ), + }, + { + Config: config2, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("redshift_group_membership.simple", "name", newGroupName), + resource.TestCheckTypeSetElemAttr("redshift_group_membership.simple", "users.*", userName1), + resource.TestCheckTypeSetElemAttr("redshift_group_membership.simple", "users.*", userName2), + testAccCheckRedshiftGroupMembershipPresence(newGroupName, userName1, true), + testAccCheckRedshiftGroupMembershipPresence(newGroupName, userName2, true), + testAccCheckRedshiftGroupMembershipPresence(newGroupName, userName3, false), + ), + }, + { + Config: config3, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("redshift_group_membership.simple", "name", newGroupName), + resource.TestCheckTypeSetElemAttr("redshift_group_membership.simple", "users.*", userName1), + resource.TestCheckTypeSetElemAttr("redshift_group_membership.simple", "users.*", userName3), + testAccCheckRedshiftGroupMembershipPresence(newGroupName, userName1, true), + testAccCheckRedshiftGroupMembershipPresence(newGroupName, userName2, false), + testAccCheckRedshiftGroupMembershipPresence(newGroupName, userName3, true), + ), + }, + }, + }) +} + +func TestAccRedshiftGroupMembership_Invalid_EmptyGroupName(t *testing.T) { + groupName := generateRandomObjectName("tf_acc_group_membership") + userName := generateRandomObjectName("tf_acc_group_membership_user") + config1 := fmt.Sprintf(` +resource "redshift_group" "simple" { + name = %[1]q + + lifecycle { + ignore_changes = [ + users + ] + } +} + +resource "redshift_user" "simple" { + name = %[2]q +} + +resource "redshift_group_membership" "simple" { + name = "" + users = [redshift_user.simple.name] +} +`, groupName, userName) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckRedshiftGroupMembershipDestroy, + Steps: []resource.TestStep{ + { + Config: config1, + ExpectError: regexp.MustCompile("expected length of name to be in the range"), + }, + }, + }) +} + +func TestAccRedshiftGroupMembership_Invalid_EmptyUserList(t *testing.T) { + groupName := generateRandomObjectName("tf_acc_group_membership") + userName := generateRandomObjectName("tf_acc_group_membership_user") + config1 := fmt.Sprintf(` +resource "redshift_group" "simple" { + name = %[1]q + + lifecycle { + ignore_changes = [ + users + ] + } +} + +resource "redshift_user" "simple" { + name = %[2]q +} + +resource "redshift_group_membership" "simple" { + name = redshift_group.simple.name + users = [] +} +`, groupName, userName) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckRedshiftGroupMembershipDestroy, + Steps: []resource.TestStep{ + { + Config: config1, + ExpectError: regexp.MustCompile("at least one user must be specified in \"users\""), + }, + }, + }) +} + +func testAccCheckRedshiftGroupMembershipPresence(groupName, userName string, shouldBePresent bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*Client) + + exists, err := checkGroupMembershipExists(client, groupName, userName) + if err != nil { + return fmt.Errorf("error checking user: %w", err) + } + + if shouldBePresent && !exists { + return fmt.Errorf("user %s should be present in group %s, but it is not", userName, groupName) + } else if !shouldBePresent && exists { + return fmt.Errorf("user %s should not be present in group %s, but it is", userName, groupName) + } + + return nil + } +} + +func checkGroupMembershipExists(client *Client, groupName string, userNames ...string) (bool, error) { + db, err := client.Connect() + if err != nil { + return false, err + } + var _rez int + if len(userNames) == 0 { + return false, nil + } + userNamesParam := buildUserStringArray(userNames, true) + query := fmt.Sprintf(`SELECT 1 FROM pg_group pgg JOIN pg_user pgu ON pgu.usesysid = ANY(pgg.grolist) WHERE pgu.usename IN (%s) AND pgg.groname = $1`, userNamesParam) + err = db.QueryRow(query, groupName).Scan(&_rez) + + switch { + case errors.Is(err, sql.ErrNoRows): + return false, nil + case err != nil: + return false, fmt.Errorf("error reading info about group: %w", err) + } + + return true, nil +} + +func testAccCheckRedshiftGroupMembershipDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "redshift_group_membership" { + continue + } + + exists, err := checkGroupMembershipExists(client, rs.Primary.ID) + + if err != nil { + return fmt.Errorf("error checking role: %w", err) + } + + if exists { + return fmt.Errorf("group still exists after destroy") + } + } + + return nil +} + +func Test_calculateUserNamesDiff(t *testing.T) { + type args struct { + oldUserNames []string + newUserNames []string + } + tests := []struct { + name string + args args + wantDeletedUserNames []string + wantAddedUserNames []string + }{ + { + "no changes", + args{ + []string{ + "user1", + "user2", + }, + []string{ + "user1", + "user2", + }, + }, + []string{}, + []string{}, + }, + { + "add user", + args{ + []string{ + "user1", + "user2", + }, + []string{ + "user1", + "user2", + "user3", + }, + }, + []string{}, + []string{"user3"}, + }, + { + "remove user", + args{ + []string{ + "user1", + "user2", + "user3", + }, + []string{ + "user1", + "user2", + }, + }, + []string{"user3"}, + []string{}, + }, + { + "add and remove user", + args{ + []string{ + "user1", + "user2", + }, + []string{ + "user2", + "user3", + }, + }, + []string{"user1"}, + []string{"user3"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotDeletedUserNames, gotAddedUserNames := calculateUserNamesDiff(tt.args.oldUserNames, tt.args.newUserNames) + if !reflect.DeepEqual(gotDeletedUserNames, tt.wantDeletedUserNames) { + t.Errorf("calculateUserNamesDiff() gotDeletedUserNames = %v, want %v", gotDeletedUserNames, tt.wantDeletedUserNames) + } + if !reflect.DeepEqual(gotAddedUserNames, tt.wantAddedUserNames) { + t.Errorf("calculateUserNamesDiff() gotAddedUserNames = %v, want %v", gotAddedUserNames, tt.wantAddedUserNames) + } + }) + } +} diff --git a/redshift/resource_redshift_group_test.go b/redshift/resource_redshift_group_test.go index 9df44a52..22cada16 100644 --- a/redshift/resource_redshift_group_test.go +++ b/redshift/resource_redshift_group_test.go @@ -74,8 +74,8 @@ func TestAccRedshiftGroup_Update(t *testing.T) { } resource "redshift_user" "group_update_user3" { - name = %[3]q - } + name = %[3]q + } resource "redshift_group" "update_group" { name = %[4]q diff --git a/redshift/resource_redshift_schema.go b/redshift/resource_redshift_schema.go index 1a2f239e..bf1e6fd4 100644 --- a/redshift/resource_redshift_schema.go +++ b/redshift/resource_redshift_schema.go @@ -418,7 +418,7 @@ func resourceRedshiftSchemaReadImpl(db *DBConnection, d *schema.ResourceData) er LEFT JOIN pg_user_info ON (svv_all_schemas.database_name = $1 AND pg_user_info.usesysid = svv_all_schemas.schema_owner) WHERE svv_all_schemas.database_name = $1 - AND pg_namespace.oid = $2`, db.client.databaseName, d.Id()).Scan(&schemaName, &schemaOwner, &schemaType) + AND pg_namespace.oid = $2`, db.client.config.Database, d.Id()).Scan(&schemaName, &schemaOwner, &schemaType) if err != nil { return err } @@ -447,7 +447,7 @@ func resourceRedshiftSchemaReadLocal(db *DBConnection, d *schema.ResourceData) e FROM svv_redshift_schema_quota WHERE database_name = $1 AND schema_name = $2 - `, db.client.databaseName, d.Get(schemaNameAttr)).Scan(&schemaQuota) + `, db.client.config.Database, d.Get(schemaNameAttr)).Scan(&schemaQuota) } else { err = db.QueryRow(` SELECT @@ -567,7 +567,7 @@ func resourceRedshiftSchemaReadExternal(db *DBConnection, d *schema.ResourceData } func resourceRedshiftSchemaDelete(db *DBConnection, d *schema.ResourceData) error { - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } @@ -588,7 +588,7 @@ func resourceRedshiftSchemaDelete(db *DBConnection, d *schema.ResourceData) erro } func resourceRedshiftSchemaCreate(db *DBConnection, d *schema.ResourceData) error { - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } @@ -780,7 +780,7 @@ func getRedshiftConfigQueryPart(d *schema.ResourceData, sourceDbName string) str } func resourceRedshiftSchemaUpdate(db *DBConnection, d *schema.ResourceData) error { - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } diff --git a/redshift/resource_redshift_user.go b/redshift/resource_redshift_user.go index 8312a093..6701b629 100644 --- a/redshift/resource_redshift_user.go +++ b/redshift/resource_redshift_user.go @@ -140,7 +140,7 @@ Amazon Redshift user accounts can only be created and dropped by a database supe } func resourceRedshiftUserCreate(db *DBConnection, d *schema.ResourceData) error { - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } @@ -299,6 +299,12 @@ func resourceRedshiftUserReadImpl(db *DBConnection, d *schema.ResourceData) erro case err != nil: return fmt.Errorf("error reading User: %w", err) } + + userValidUntil, err = validateAndAdjustValidUntil(userValidUntil) + if err != nil { + return err + } + userConnLimitNumber := -1 if userConnLimit != "UNLIMITED" { if userConnLimitNumber, err = strconv.Atoi(userConnLimit); err != nil { @@ -322,12 +328,35 @@ func resourceRedshiftUserReadImpl(db *DBConnection, d *schema.ResourceData) erro return nil } +const redshiftDataApiInfinityDateString = "2038-01-19 03:14:04" + +var redshiftDataApiDatetimeRegexp = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$`) +var correctDatetimeRegexp = regexp.MustCompile(`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\+00$`) + +func validateAndAdjustValidUntil(validUntil string) (string, error) { + if validUntil == redshiftDataApiInfinityDateString { + // The Redshift Data API translates the `infinity` to a date in 2038 (see https://en.wikipedia.org/wiki/Year_2038_problem) + return "infinity", nil + } else if redshiftDataApiDatetimeRegexp.MatchString(validUntil) { + // The Redshift Data API returns the datetime without the timezone offset, so we need to add it + validUntil += "+00" + } + if !correctDatetimeRegexp.MatchString(validUntil) { + return "", fmt.Errorf(`received invalid date format for valid_until: %q, expected format is "YYYY-MM-DD HH:MM:SS+00"`, validUntil) + } + return validUntil, nil +} + func resourceRedshiftUserDelete(db *DBConnection, d *schema.ResourceData) error { useSysID := d.Id() userName := d.Get(userNameAttr).(string) - newOwnerName := permanentUsername(db.client.config.Username) + rawUsername, err := db.client.config.GetUsername(db) + if err != nil { + return fmt.Errorf("error retrieving username: %w", err) + } + newOwnerName := permanentUsername(rawUsername) - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } @@ -429,7 +458,7 @@ func resourceRedshiftUserDelete(db *DBConnection, d *schema.ResourceData) error } func resourceRedshiftUserUpdate(db *DBConnection, d *schema.ResourceData) error { - tx, err := startTransaction(db.client, "") + tx, err := startTransaction(db.client) if err != nil { return err } diff --git a/redshift/resource_redshift_user_test.go b/redshift/resource_redshift_user_test.go index 6883f540..170b7a04 100644 --- a/redshift/resource_redshift_user_test.go +++ b/redshift/resource_redshift_user_test.go @@ -5,37 +5,126 @@ import ( "database/sql" "errors" "fmt" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "os" "regexp" "strconv" "strings" "testing" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -func TestAccRedshiftUser_Basic(t *testing.T) { +const testAccRedshiftUserLoginConfig = ` +resource "redshift_user" "with_email" { + name = "John-and-Jane.doe@example.com" + password = "Foobarbaz1" +} + +resource "redshift_user" "with_hashed_password" { + name = "hashed_password" + password = "Foobarbaz3" +} +` + +const testAccRedshiftUserLoginUpdateConfig = ` +resource "redshift_user" "with_email" { + name = "John-and-Jane.doe@example.com" + password = "Foobarbaz1" +} + +resource "redshift_user" "with_hashed_password" { + name = "hashed_password" + password = "md5ad3b897bab2474bc7e408326cb18c42f" +} +` + +func TestAccRedshiftUser_Login(t *testing.T) { + if os.Getenv("REDSHIFT_TEST_ACC_SKIP_USER_LOGIN") != "" { + t.Skipf("Skipping user login test as REDSHIFT_TEST_ACC_SKIP_USER_LOGIN is set") + } resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: testAccProviders, CheckDestroy: testAccCheckRedshiftUserDestroy, Steps: []resource.TestStep{ { - Config: testAccRedshiftUserConfig, + Config: testAccRedshiftUserLoginConfig, Check: resource.ComposeTestCheckFunc( - testAccCheckRedshiftUserExists("user_simple"), - resource.TestCheckResourceAttr("redshift_user.simple", "name", "user_simple"), - testAccCheckRedshiftUserExists("John-and-Jane.doe@example.com"), resource.TestCheckResourceAttr("redshift_user.with_email", "name", "John-and-Jane.doe@example.com"), + resource.TestCheckResourceAttr("redshift_user.with_email", "password", "Foobarbaz1"), testAccCheckRedshiftUserCanLogin("John-and-Jane.doe@example.com", "Foobarbaz1"), testAccCheckRedshiftUserExists("hashed_password"), - testAccCheckRedshiftUserCanLogin("hashed_password", "Foobarbaz2"), + resource.TestCheckResourceAttr("redshift_user.with_hashed_password", "password", "Foobarbaz3"), + testAccCheckRedshiftUserCanLogin("hashed_password", "Foobarbaz3"), + ), + }, + { + Config: testAccRedshiftUserLoginUpdateConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckRedshiftUserExists("hashed_password"), + resource.TestCheckResourceAttr("redshift_user.with_hashed_password", "password", "md5ad3b897bab2474bc7e408326cb18c42f"), + testAccCheckRedshiftUserCanLogin("hashed_password", "Foobarbaz6"), + ), + }, + }, + }) +} + +const testAccRedshiftUserConfig = ` +resource "redshift_user" "simple" { + name = "user_simple" +} + +resource "redshift_user" "user_with_defaults" { + name = "user_defaults" + valid_until = "infinity" + superuser = false + create_database = false + connection_limit = -1 + password = "" +} + +resource "redshift_user" "user_with_create_database" { + name = "user_create_database" + create_database = true +} + +resource "redshift_user" "user_with_unrestricted_syslog" { + name = "user_syslog" + syslog_access = "UNRESTRICTED" +} + +resource "redshift_user" "user_superuser" { + name = "user_superuser" + superuser = true + password = "FooBarBaz123" +} + +resource "redshift_user" "user_timeout" { + name = "user_timeout" + password = "FooBarBaz123" + session_timeout = 60 +} +` + +func TestAccRedshiftUser_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckRedshiftUserDestroy, + Steps: []resource.TestStep{ + { + Config: testAccRedshiftUserConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckRedshiftUserExists("user_simple"), + resource.TestCheckResourceAttr("redshift_user.simple", "name", "user_simple"), testAccCheckRedshiftUserExists("user_defaults"), resource.TestCheckResourceAttr("redshift_user.user_with_defaults", "name", "user_defaults"), @@ -73,7 +162,6 @@ func TestAccRedshiftUser_Update(t *testing.T) { var configCreate = ` resource "redshift_user" "update_user" { name = "update_user" - password = "Foobarbaz1" valid_until = "2038-01-04 12:00:00+00" } ` @@ -82,16 +170,6 @@ resource "redshift_user" "update_user" { resource "redshift_user" "update_user" { name = "update_user2" connection_limit = 5 - password = "Foobarbaz5" - syslog_access = "UNRESTRICTED" - create_database = true -} -` - var configUpdate2 = ` -resource "redshift_user" "update_user" { - name = "update_user2" - connection_limit = 5 - password = "md508d5d11f1f947091b312fb36b25e621f" syslog_access = "UNRESTRICTED" create_database = true } @@ -107,7 +185,6 @@ resource "redshift_user" "update_user" { testAccCheckRedshiftUserExists("update_user"), resource.TestCheckResourceAttr("redshift_user.update_user", "name", "update_user"), resource.TestCheckResourceAttr("redshift_user.update_user", "connection_limit", "-1"), - resource.TestCheckResourceAttr("redshift_user.update_user", "password", "Foobarbaz1"), resource.TestCheckResourceAttr("redshift_user.update_user", "valid_until", "2038-01-04 12:00:00+00"), resource.TestCheckResourceAttr("redshift_user.update_user", "syslog_access", "RESTRICTED"), resource.TestCheckResourceAttr("redshift_user.update_user", "create_database", "false"), @@ -121,20 +198,11 @@ resource "redshift_user" "update_user" { "redshift_user.update_user", "name", "update_user2", ), resource.TestCheckResourceAttr("redshift_user.update_user", "connection_limit", "5"), - resource.TestCheckResourceAttr("redshift_user.update_user", "password", "Foobarbaz5"), resource.TestCheckResourceAttr("redshift_user.update_user", "valid_until", "infinity"), resource.TestCheckResourceAttr("redshift_user.update_user", "syslog_access", "UNRESTRICTED"), resource.TestCheckResourceAttr("redshift_user.update_user", "create_database", "true"), ), }, - { - Config: configUpdate2, - Check: resource.ComposeTestCheckFunc( - testAccCheckRedshiftUserExists("update_user2"), - testAccCheckRedshiftUserCanLogin("update_user2", "Foobarbaz6"), - resource.TestCheckResourceAttr("redshift_user.update_user", "password", "md508d5d11f1f947091b312fb36b25e621f"), - ), - }, // apply the first one again to check if all parameters roll back properly { Config: configCreate, @@ -142,7 +210,6 @@ resource "redshift_user" "update_user" { testAccCheckRedshiftUserExists("update_user"), resource.TestCheckResourceAttr("redshift_user.update_user", "name", "update_user"), resource.TestCheckResourceAttr("redshift_user.update_user", "connection_limit", "-1"), - resource.TestCheckResourceAttr("redshift_user.update_user", "password", "Foobarbaz1"), resource.TestCheckResourceAttr("redshift_user.update_user", "valid_until", "2038-01-04 12:00:00+00"), resource.TestCheckResourceAttr("redshift_user.update_user", "syslog_access", "RESTRICTED"), resource.TestCheckResourceAttr("redshift_user.update_user", "create_database", "false"), @@ -230,7 +297,7 @@ resource "redshift_user" "superuser" { Steps: []resource.TestStep{ { Config: config, - ExpectError: regexp.MustCompile("Users that are superusers must define a password."), + ExpectError: regexp.MustCompile("users that are superusers must define a password"), }, }, }) @@ -274,7 +341,7 @@ func TestAccRedshiftUser_SuperuserSyslogAccess(t *testing.T) { "(superuser) RESTRICTED syslog access": { isSuperuser: true, syslogAccess: defaultUserSyslogAccess, - expectError: regexp.MustCompile("Superusers must have syslog access set to UNRESTRICTED."), + expectError: regexp.MustCompile("superusers must have syslog access set to \"UNRESTRICTED\""), }, "(superuser) UNRESTRICTED syslog access": { isSuperuser: true, @@ -426,53 +493,6 @@ func checkUserExists(client *Client, user string) (bool, error) { return true, nil } -const testAccRedshiftUserConfig = ` -resource "redshift_user" "simple" { - name = "user_simple" -} - -resource "redshift_user" "with_email" { - name = "John-and-Jane.doe@example.com" - password = "Foobarbaz1" -} - -resource "redshift_user" "with_hashed_password" { - name = "hashed_password" - password = "md5ad3b897bab2474bc7e408326cb18c42f" -} - -resource "redshift_user" "user_with_defaults" { - name = "user_defaults" - valid_until = "infinity" - superuser = false - create_database = false - connection_limit = -1 - password = "" -} - -resource "redshift_user" "user_with_create_database" { - name = "user_create_database" - create_database = true -} - -resource "redshift_user" "user_with_unrestricted_syslog" { - name = "user_syslog" - syslog_access = "UNRESTRICTED" -} - -resource "redshift_user" "user_superuser" { - name = "user_superuser" - superuser = true - password = "FooBarBaz123" -} - -resource "redshift_user" "user_timeout" { - name = "user_timeout" - password = "FooBarBaz123" - session_timeout = 60 -} -` - func TestPermanentUsername(t *testing.T) { expected := "user" if result := permanentUsername(expected); result != expected { @@ -507,17 +527,10 @@ func testAccCheckRedshiftUserCanLogin(user string, password string) resource.Tes if !ok { sslMode = "require" } - config := &Config{ - Host: os.Getenv("REDSHIFT_HOST"), - Port: portNum, - Username: user, - Password: password, - Database: database, - SSLMode: sslMode, - MaxConns: defaultProviderMaxOpenConnections, - } + config := NewPqConfig(os.Getenv("REDSHIFT_HOST"), database, user, password, portNum, sslMode, + defaultProviderMaxOpenConnections) - client, err := config.Client() + client := config.NewClient() if err != nil { return fmt.Errorf("user is unable to login: %w", err) } @@ -525,3 +538,68 @@ func testAccCheckRedshiftUserCanLogin(user string, password string) resource.Tes return nil } } + +func Test_validateAndAdjustValidUntil(t *testing.T) { + type args struct { + validUntil string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "translate Redshift Data API infinity date", + args: args{ + validUntil: "2038-01-19 03:14:04", + }, + want: "infinity", + wantErr: false, + }, + { + name: "adds suffix to Redshift Data API datetime", + args: args{ + validUntil: "2025-08-06 17:22:56", + }, + want: "2025-08-06 17:22:56+00", + wantErr: false, + }, + { + name: "does not add suffix to correct datetime", + args: args{ + validUntil: "2025-08-06 17:22:56+00", + }, + want: "2025-08-06 17:22:56+00", + wantErr: false, + }, + { + name: "returns error for invalid timezone", + args: args{ + validUntil: "2025-08-06 17:22:56+01", + }, + want: "", + wantErr: true, + }, + { + name: "returns error for invalid datetime", + args: args{ + validUntil: "some none date", + }, + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := validateAndAdjustValidUntil(tt.args.validUntil) + if (err != nil) != tt.wantErr { + t.Errorf("validateAndAdjustValidUntil() error = %v, wantErr = %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("validateAndAdjustValidUntil() got = %v, want %v", got, tt.want) + } + }) + } +}