diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 2755e44..4d5635f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,37 +1,36 @@
name: release
on:
- push:
- tags:
- - 'v*'
+ push:
+ tags:
+ - "v*"
permissions:
- contents: write
+ contents: write
jobs:
- goreleaser:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- - name: Unshallow
- run: git fetch --prune --unshallow
- - name: Setup Go
- uses: actions/setup-go@v5
- with:
- go-version: 1.22.4
- - name: Import GPG Key
- id: import_gpg
- uses: crazy-max/ghaction-import-gpg@v6.2.0
- with:
- gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
- - name: Run GoReleaser
- uses: goreleaser/goreleaser-action@v6.0.0
- with:
- version: latest
- args: release --clean
- env:
- GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
- # GitHub sets this automatically
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-
+ goreleaser:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - name: Unshallow
+ run: git fetch --prune --unshallow
+ - name: Setup Go
+ uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
+ with:
+ go-version: "1.22"
+ - name: Import GPG Key
+ id: import_gpg
+ uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6.2.0
+ with:
+ gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
+ - name: Run GoReleaser
+ uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0
+ with:
+ version: latest
+ args: release --clean
+ env:
+ GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
+ # GitHub sets this automatically
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b348a86..e6885d3 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -24,31 +24,40 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-go@v5
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+
+ - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version-file: "go.mod"
cache: true
+
- run: go mod download
+
- run: go build -v .
+
- name: Run linters
- uses: golangci/golangci-lint-action@v6
+ uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
with:
version: latest
generate:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-go@v5
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+
+ - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version-file: "go.mod"
cache: true
+
- run: go generate ./...
+
- name: git diff
run: |
- git diff --compact-summary --exit-code || \
- (echo; echo "Unexpected difference in directories after code generation. Run 'go generate ./...' command and commit."; exit 1)
+ if [[ -n $(git ls-files --other --modified --exclude-standard) ]]; then
+ echo "Unexpected difference in directories after code generation. Run 'make gen' and include the output in the commit."
+ exit 1
+ fi
# Run acceptance tests in a matrix with Terraform CLI versions
test:
@@ -72,20 +81,24 @@ jobs:
- "1.8.*"
- "1.9.*"
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-go@v5
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+
+ - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version-file: "go.mod"
cache: true
- - uses: hashicorp/setup-terraform@v3
+
+ - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
with:
terraform_version: ${{ matrix.terraform }}
terraform_wrapper: false
+
- run: go mod download
- - env:
+
+ - run: go test -v -cover ./internal/provider/
+ env:
TF_ACC: "1"
CODER_ENTERPRISE_LICENSE: ${{ secrets.CODER_ENTERPRISE_LICENSE }}
- run: go test -v -cover ./internal/provider/
timeout-minutes: 10
lint:
@@ -93,20 +106,18 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- - name: Set up Go
- uses: actions/setup-go@v5
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+
+ - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
with:
go-version: "1.22"
id: go
- - uses: hashicorp/setup-terraform@v3
+ - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
with:
terraform_version: "1.9.*"
terraform_wrapper: false
- - name: Check out code into the Go module directory
- uses: actions/checkout@v4
-
- name: Get dependencies
run: |
go mod download
diff --git a/.golangci.yml b/.golangci.yml
index 223cf95..679a35a 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,5 +1,5 @@
-# Visit https://golangci-lint.run/ for usage documentation
-# and information on other useful linters
+# Visit https://golangci-lint.run/ for usage documentation and information on
+# other useful linters
issues:
max-per-linter: 0
max-same-issues: 0
@@ -24,4 +24,4 @@ linters:
- unconvert
- unparam
- unused
- - vet
\ No newline at end of file
+ - vet
diff --git a/Makefile b/Makefile
index 54a7a12..b1f903b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,12 @@
default: testacc
fmt:
+ go fmt ./...
terraform fmt -recursive
+vet:
+ go vet ./...
+
gen:
go generate ./...
diff --git a/docs/resources/license.md b/docs/resources/license.md
index 58c773c..ec29706 100644
--- a/docs/resources/license.md
+++ b/docs/resources/license.md
@@ -4,7 +4,7 @@ page_title: "coderd_license Resource - terraform-provider-coderd"
subcategory: ""
description: |-
A license for a Coder deployment.
- It's recommended to create multiple instances of this resource when updating a license. Modifying an existing license will cause the resource to be replaced, which may result in a brief unlicensed period.
+ It's recommended to set create_before_destroy https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle#create_before_destroy on license resources. Without setting this, Terraform will remove the old license before adding the updated license. This will result in a temporary disruption to your users; during which they may be unable to use features that require a license.
Terraform does not guarantee this resource will be created before other resources or attributes that require a licensed deployment. The depends_on meta-argument is instead recommended.
---
@@ -12,11 +12,21 @@ description: |-
A license for a Coder deployment.
-It's recommended to create multiple instances of this resource when updating a license. Modifying an existing license will cause the resource to be replaced, which may result in a brief unlicensed period.
+It's recommended to set [`create_before_destroy`](https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle#create_before_destroy) on license resources. Without setting this, Terraform will remove the old license before adding the updated license. This will result in a temporary disruption to your users; during which they may be unable to use features that require a license.
Terraform does not guarantee this resource will be created before other resources or attributes that require a licensed deployment. The `depends_on` meta-argument is instead recommended.
+## Example Usage
+```terraform
+resource "coderd_license" "license" {
+ license = "<…>"
+
+ lifecycle {
+ create_before_destroy = true
+ }
+}
+```
## Schema
diff --git a/docs/resources/organization.md b/docs/resources/organization.md
new file mode 100644
index 0000000..9556be4
--- /dev/null
+++ b/docs/resources/organization.md
@@ -0,0 +1,60 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "coderd_organization Resource - terraform-provider-coderd"
+subcategory: ""
+description: |-
+ An organization on the Coder deployment
+---
+
+# coderd_organization (Resource)
+
+An organization on the Coder deployment
+
+
+
+
+## Schema
+
+### Required
+
+- `name` (String) Name of the organization.
+
+### Optional
+
+- `description` (String)
+- `display_name` (String) Display name of the organization. Defaults to name.
+- `group_sync` (Block, Optional) (see [below for nested schema](#nestedblock--group_sync))
+- `icon` (String)
+- `role_sync` (Block, Optional) (see [below for nested schema](#nestedblock--role_sync))
+
+### Read-Only
+
+- `id` (String) Organization ID
+
+
+### Nested Schema for `group_sync`
+
+Optional:
+
+- `auto_create_missing` (Boolean) Controls whether groups will be created if they are missing.
+- `field` (String) The claim field that specifies what groups a user should be in.
+- `mapping` (Map of List of String) A map from OIDC group name to Coder group ID.
+- `regex_filter` (String) A regular expression that will be used to filter the groups returned by the OIDC provider. Any group not matched will be ignored.
+
+
+
+### Nested Schema for `role_sync`
+
+Optional:
+
+- `field` (String) The claim field that specifies what organization roles a user should be given.
+- `mapping` (Map of List of String) A map from OIDC group name to Coder organization role.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+# Organizations can be imported by their name
+terraform import coderd_organization.our_org our_org
+```
diff --git a/docs/resources/provisioner_key.md b/docs/resources/provisioner_key.md
new file mode 100644
index 0000000..d64b724
--- /dev/null
+++ b/docs/resources/provisioner_key.md
@@ -0,0 +1,29 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "coderd_provisioner_key Resource - terraform-provider-coderd"
+subcategory: ""
+description: |-
+ A provisioner key for a Coder deployment.
+---
+
+# coderd_provisioner_key (Resource)
+
+A provisioner key for a Coder deployment.
+
+
+
+
+## Schema
+
+### Required
+
+- `name` (String) The name of the key.
+- `organization_id` (String) The organization that provisioners connected with this key will be connected to.
+
+### Optional
+
+- `tags` (Map of String) The tags that provisioners connected with this key will accept jobs for.
+
+### Read-Only
+
+- `key` (String, Sensitive) The acquired provisioner key
diff --git a/examples/resources/coderd_license/resource.tf b/examples/resources/coderd_license/resource.tf
new file mode 100644
index 0000000..3a25e9c
--- /dev/null
+++ b/examples/resources/coderd_license/resource.tf
@@ -0,0 +1,7 @@
+resource "coderd_license" "license" {
+ license = "<…>"
+
+ lifecycle {
+ create_before_destroy = true
+ }
+}
diff --git a/examples/resources/coderd_organization/import.sh b/examples/resources/coderd_organization/import.sh
new file mode 100644
index 0000000..882dce6
--- /dev/null
+++ b/examples/resources/coderd_organization/import.sh
@@ -0,0 +1,2 @@
+# Organizations can be imported by their name
+terraform import coderd_organization.our_org our_org
diff --git a/go.mod b/go.mod
index c62b2b8..4c43f7c 100644
--- a/go.mod
+++ b/go.mod
@@ -8,14 +8,14 @@ require (
github.com/docker/docker v27.2.1+incompatible
github.com/docker/go-connections v0.5.0
github.com/google/uuid v1.6.0
- github.com/hashicorp/terraform-plugin-docs v0.19.4
+ github.com/hashicorp/terraform-plugin-docs v0.20.1
github.com/hashicorp/terraform-plugin-framework v1.13.0
github.com/hashicorp/terraform-plugin-framework-validators v0.15.0
github.com/hashicorp/terraform-plugin-go v0.25.0
github.com/hashicorp/terraform-plugin-log v0.9.0
- github.com/hashicorp/terraform-plugin-testing v1.10.0
+ github.com/hashicorp/terraform-plugin-testing v1.11.0
github.com/otiai10/copy v1.14.0
- github.com/stretchr/testify v1.9.0
+ github.com/stretchr/testify v1.10.0
)
require (
@@ -40,7 +40,7 @@ require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/speakeasy v0.2.0 // indirect
- github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
+ github.com/bmatcuk/doublestar/v4 v4.7.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
@@ -80,11 +80,11 @@ require (
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.0 // indirect
- github.com/hashicorp/hcl/v2 v2.22.0 // indirect
+ github.com/hashicorp/hcl/v2 v2.23.0 // indirect
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-exec v0.21.0 // indirect
- github.com/hashicorp/terraform-json v0.22.1 // indirect
- github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 // indirect
+ github.com/hashicorp/terraform-json v0.23.0 // indirect
+ github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.3 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
@@ -133,7 +133,7 @@ require (
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
- github.com/yuin/goldmark v1.7.4 // indirect
+ github.com/yuin/goldmark v1.7.7 // indirect
github.com/yuin/goldmark-meta v1.1.0 // indirect
github.com/zclconf/go-cty v1.15.0 // indirect
github.com/zeebo/errs v1.3.0 // indirect
@@ -148,15 +148,15 @@ require (
go.opentelemetry.io/otel/trace v1.30.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/atomic v1.11.0 // indirect
- golang.org/x/crypto v0.28.0 // indirect
+ golang.org/x/crypto v0.29.0 // indirect
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
- golang.org/x/sync v0.8.0 // indirect
- golang.org/x/sys v0.26.0 // indirect
- golang.org/x/term v0.25.0 // indirect
- golang.org/x/text v0.19.0 // indirect
+ golang.org/x/sync v0.9.0 // indirect
+ golang.org/x/sys v0.27.0 // indirect
+ golang.org/x/term v0.26.0 // indirect
+ golang.org/x/text v0.20.0 // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.26.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
diff --git a/go.sum b/go.sum
index 55c9312..d9a08c6 100644
--- a/go.sum
+++ b/go.sum
@@ -62,8 +62,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E=
github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
-github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
+github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q=
+github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs=
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
@@ -243,16 +243,16 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hc-install v0.9.0 h1:2dIk8LcvANwtv3QZLckxcjyF5w8KVtiMxu6G6eLhghE=
github.com/hashicorp/hc-install v0.9.0/go.mod h1:+6vOP+mf3tuGgMApVYtmsnDoKWMDcFXeTxCACYZ8SFg=
-github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M=
-github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
+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/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.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ=
github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg=
-github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7orfb5Ltvec=
-github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A=
-github.com/hashicorp/terraform-plugin-docs v0.19.4 h1:G3Bgo7J22OMtegIgn8Cd/CaSeyEljqjH3G39w28JK4c=
-github.com/hashicorp/terraform-plugin-docs v0.19.4/go.mod h1:4pLASsatTmRynVzsjEhbXZ6s7xBlUw/2Kt0zfrq8HxA=
+github.com/hashicorp/terraform-json v0.23.0 h1:sniCkExU4iKtTADReHzACkk8fnpQXrdD2xoR+lppBkI=
+github.com/hashicorp/terraform-json v0.23.0/go.mod h1:MHdXbBAbSg0GvzuWazEGKAn/cyNfIB7mN6y7KJN6y2c=
+github.com/hashicorp/terraform-plugin-docs v0.20.1 h1:Fq7E/HrU8kuZu3hNliZGwloFWSYfWEOWnylFhYQIoys=
+github.com/hashicorp/terraform-plugin-docs v0.20.1/go.mod h1:Yz6HoK7/EgzSrHPB9J/lWFzwl9/xep2OPnc5jaJDV90=
github.com/hashicorp/terraform-plugin-framework v1.13.0 h1:8OTG4+oZUfKgnfTdPTJwZ532Bh2BobF4H+yBiYJ/scw=
github.com/hashicorp/terraform-plugin-framework v1.13.0/go.mod h1:j64rwMGpgM3NYXTKuxrCnyubQb/4VKldEKlcG8cvmjU=
github.com/hashicorp/terraform-plugin-framework-validators v0.15.0 h1:RXMmu7JgpFjnI1a5QjMCBb11usrW2OtAG+iOTIj5c9Y=
@@ -261,10 +261,10 @@ github.com/hashicorp/terraform-plugin-go v0.25.0 h1:oi13cx7xXA6QciMcpcFi/rwA974r
github.com/hashicorp/terraform-plugin-go v0.25.0/go.mod h1:+SYagMYadJP86Kvn+TGeV+ofr/R3g4/If0O5sO96MVw=
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.34.0 h1:kJiWGx2kiQVo97Y5IOGR4EMcZ8DtMswHhUuFibsCQQE=
-github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0/go.mod h1:sl/UoabMc37HA6ICVMmGO+/0wofkVIRxf+BMb/dnoIg=
-github.com/hashicorp/terraform-plugin-testing v1.10.0 h1:2+tmRNhvnfE4Bs8rB6v58S/VpqzGC6RCh9Y8ujdn+aw=
-github.com/hashicorp/terraform-plugin-testing v1.10.0/go.mod h1:iWRW3+loP33WMch2P/TEyCxxct/ZEcCGMquSLSCVsrc=
+github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0 h1:wyKCCtn6pBBL46c1uIIBNUOWlNfYXfXpVo16iDyLp8Y=
+github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0/go.mod h1:B0Al8NyYVr8Mp/KLwssKXG1RqnTk7FySqSn4fRuLNgw=
+github.com/hashicorp/terraform-plugin-testing v1.11.0 h1:MeDT5W3YHbONJt2aPQyaBsgQeAIckwPX41EUHXEn29A=
+github.com/hashicorp/terraform-plugin-testing v1.11.0/go.mod h1:WNAHQ3DcgV/0J+B15WTE6hDvxcUdkPPpnB1FR3M910U=
github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI=
github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM=
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
@@ -413,8 +413,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
-github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
-github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
@@ -454,8 +454,8 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+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/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ=
github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
github.com/tinylib/msgp v1.2.1 h1:6ypy2qcCznxpP4hpORzhtXyTqrBs7cfM9MCCWY8zsmU=
@@ -489,8 +489,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
-github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+github.com/yuin/goldmark v1.7.7 h1:5m9rrB1sW3JUMToKFQfb+FGt1U7r57IHu5GrYrG2nqU=
+github.com/yuin/goldmark v1.7.7/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ=
@@ -542,8 +542,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
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.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
-golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
-golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
+golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
+golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -576,8 +576,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
-golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
+golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -603,8 +603,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
-golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
+golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -612,8 +612,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
-golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
-golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
+golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
+golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -623,8 +623,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
-golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
+golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
diff --git a/internal/codersdkvalidator/display_name.go b/internal/codersdkvalidator/display_name.go
new file mode 100644
index 0000000..1000c32
--- /dev/null
+++ b/internal/codersdkvalidator/display_name.go
@@ -0,0 +1,10 @@
+package codersdkvalidator
+
+import (
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+)
+
+func DisplayName() validator.String {
+ return validatorFromFunc(codersdk.DisplayNameValid, "value must be a valid display name")
+}
diff --git a/internal/codersdkvalidator/group_name.go b/internal/codersdkvalidator/group_name.go
new file mode 100644
index 0000000..20313db
--- /dev/null
+++ b/internal/codersdkvalidator/group_name.go
@@ -0,0 +1,10 @@
+package codersdkvalidator
+
+import (
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+)
+
+func GroupName() validator.String {
+ return validatorFromFunc(codersdk.GroupNameValid, "value must be a valid group name")
+}
diff --git a/internal/codersdkvalidator/name.go b/internal/codersdkvalidator/name.go
new file mode 100644
index 0000000..14adb25
--- /dev/null
+++ b/internal/codersdkvalidator/name.go
@@ -0,0 +1,10 @@
+package codersdkvalidator
+
+import (
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+)
+
+func Name() validator.String {
+ return validatorFromFunc(codersdk.NameValid, "value must be a valid name")
+}
diff --git a/internal/codersdkvalidator/regex.go b/internal/codersdkvalidator/regex.go
new file mode 100644
index 0000000..0077616
--- /dev/null
+++ b/internal/codersdkvalidator/regex.go
@@ -0,0 +1,16 @@
+package codersdkvalidator
+
+import (
+ "regexp"
+
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+)
+
+func checkRegexp(it string) error {
+ _, err := regexp.Compile("")
+ return err
+}
+
+func Regexp() validator.String {
+ return validatorFromFunc(checkRegexp, "value must be a valid regexp")
+}
diff --git a/internal/codersdkvalidator/template_version_name.go b/internal/codersdkvalidator/template_version_name.go
new file mode 100644
index 0000000..32c69d6
--- /dev/null
+++ b/internal/codersdkvalidator/template_version_name.go
@@ -0,0 +1,10 @@
+package codersdkvalidator
+
+import (
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+)
+
+func TemplateVersionName() validator.String {
+ return validatorFromFunc(codersdk.TemplateVersionNameValid, "value must be a valid template version name")
+}
diff --git a/internal/codersdkvalidator/user_real_name.go b/internal/codersdkvalidator/user_real_name.go
new file mode 100644
index 0000000..5bf9686
--- /dev/null
+++ b/internal/codersdkvalidator/user_real_name.go
@@ -0,0 +1,10 @@
+package codersdkvalidator
+
+import (
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+)
+
+func UserRealName() validator.String {
+ return validatorFromFunc(codersdk.UserRealNameValid, "value must be a valid name for a user")
+}
diff --git a/internal/codersdkvalidator/validator_from_func.go b/internal/codersdkvalidator/validator_from_func.go
new file mode 100644
index 0000000..9d5e631
--- /dev/null
+++ b/internal/codersdkvalidator/validator_from_func.go
@@ -0,0 +1,51 @@
+package codersdkvalidator
+
+import (
+ "context"
+
+ "github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+)
+
+type functionValidator struct {
+ check func(string) error
+ defaultMessage string
+ err error
+}
+
+func validatorFromFunc(check func(string) error, defaultMessage string) functionValidator {
+ return functionValidator{
+ check: check,
+ defaultMessage: defaultMessage,
+ }
+}
+
+var _ validator.String = functionValidator{}
+
+func (v functionValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
+ if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
+ return
+ }
+
+ name := req.ConfigValue.ValueString()
+ if v.err = v.check(name); v.err != nil {
+ resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic(
+ req.Path,
+ v.Description(ctx),
+ name,
+ ))
+ }
+}
+
+var _ validator.Describer = functionValidator{}
+
+func (v functionValidator) Description(_ context.Context) string {
+ if v.err != nil {
+ return v.err.Error()
+ }
+ return v.defaultMessage
+}
+
+func (v functionValidator) MarkdownDescription(ctx context.Context) string {
+ return v.Description(ctx)
+}
diff --git a/internal/provider/group_data_source_test.go b/internal/provider/group_data_source_test.go
index 349e855..5a081af 100644
--- a/internal/provider/group_data_source_test.go
+++ b/internal/provider/group_data_source_test.go
@@ -8,6 +8,7 @@ import (
"testing"
"text/template"
+ "github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/terraform-provider-coderd/integration"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
@@ -74,7 +75,7 @@ func TestAccGroupDataSource(t *testing.T) {
cfg := testAccGroupDataSourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- ID: PtrTo(group.ID.String()),
+ ID: ptr.Ref(group.ID.String()),
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
@@ -92,8 +93,8 @@ func TestAccGroupDataSource(t *testing.T) {
cfg := testAccGroupDataSourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- OrganizationID: PtrTo(firstUser.OrganizationIDs[0].String()),
- Name: PtrTo("example-group"),
+ OrganizationID: ptr.Ref(firstUser.OrganizationIDs[0].String()),
+ Name: ptr.Ref("example-group"),
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
@@ -111,7 +112,7 @@ func TestAccGroupDataSource(t *testing.T) {
cfg := testAccGroupDataSourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- Name: PtrTo("example-group"),
+ Name: ptr.Ref("example-group"),
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
@@ -129,7 +130,7 @@ func TestAccGroupDataSource(t *testing.T) {
cfg := testAccGroupDataSourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- OrganizationID: PtrTo(firstUser.OrganizationIDs[0].String()),
+ OrganizationID: ptr.Ref(firstUser.OrganizationIDs[0].String()),
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
diff --git a/internal/provider/group_resource.go b/internal/provider/group_resource.go
index a7dcd6a..fa370a0 100644
--- a/internal/provider/group_resource.go
+++ b/internal/provider/group_resource.go
@@ -6,8 +6,8 @@ import (
"strings"
"github.com/coder/coder/v2/codersdk"
+ "github.com/coder/terraform-provider-coderd/internal/codersdkvalidator"
"github.com/google/uuid"
- "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
@@ -77,8 +77,7 @@ func (r *GroupResource) Schema(ctx context.Context, req resource.SchemaRequest,
MarkdownDescription: "The unique name of the group.",
Required: true,
Validators: []validator.String{
- stringvalidator.LengthBetween(1, 36),
- stringvalidator.RegexMatches(nameValidRegex, "Group names must be alpahnumeric with hyphens."),
+ codersdkvalidator.GroupName(),
},
},
"display_name": schema.StringAttribute{
@@ -86,8 +85,7 @@ func (r *GroupResource) Schema(ctx context.Context, req resource.SchemaRequest,
Computed: true,
Optional: true,
Validators: []validator.String{
- stringvalidator.LengthBetween(1, 64),
- stringvalidator.RegexMatches(displayNameRegex, "Group display names must be alphanumeric with spaces"),
+ codersdkvalidator.DisplayName(),
},
Default: stringdefault.StaticString(""),
},
diff --git a/internal/provider/group_resource_test.go b/internal/provider/group_resource_test.go
index 159856f..865851a 100644
--- a/internal/provider/group_resource_test.go
+++ b/internal/provider/group_resource_test.go
@@ -8,6 +8,7 @@ import (
"testing"
"text/template"
+ "github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/terraform-provider-coderd/integration"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
@@ -44,17 +45,17 @@ func TestAccGroupResource(t *testing.T) {
cfg1 := testAccGroupResourceconfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- Name: PtrTo("example-group"),
- DisplayName: PtrTo("Example Group"),
- AvatarUrl: PtrTo("https://google.com"),
- QuotaAllowance: PtrTo(int32(100)),
- Members: PtrTo([]string{user1.ID.String()}),
+ Name: ptr.Ref("example-group"),
+ DisplayName: ptr.Ref("Example Group"),
+ AvatarUrl: ptr.Ref("https://google.com"),
+ QuotaAllowance: ptr.Ref(int32(100)),
+ Members: ptr.Ref([]string{user1.ID.String()}),
}
cfg2 := cfg1
- cfg2.Name = PtrTo("example-group-new")
- cfg2.DisplayName = PtrTo("Example Group New")
- cfg2.Members = PtrTo([]string{user2.ID.String()})
+ cfg2.Name = ptr.Ref("example-group-new")
+ cfg2.DisplayName = ptr.Ref("Example Group New")
+ cfg2.Members = ptr.Ref([]string{user2.ID.String()})
cfg3 := cfg2
cfg3.Members = nil
@@ -143,11 +144,11 @@ func TestAccGroupResourceAGPL(t *testing.T) {
cfg1 := testAccGroupResourceconfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- Name: PtrTo("example-group"),
- DisplayName: PtrTo("Example Group"),
- AvatarUrl: PtrTo("https://google.com"),
- QuotaAllowance: PtrTo(int32(100)),
- Members: PtrTo([]string{firstUser.ID.String()}),
+ Name: ptr.Ref("example-group"),
+ DisplayName: ptr.Ref("Example Group"),
+ AvatarUrl: ptr.Ref("https://google.com"),
+ QuotaAllowance: ptr.Ref(int32(100)),
+ Members: ptr.Ref([]string{firstUser.ID.String()}),
}
resource.Test(t, resource.TestCase{
diff --git a/internal/provider/license_resource.go b/internal/provider/license_resource.go
index 2dec235..5cb4778 100644
--- a/internal/provider/license_resource.go
+++ b/internal/provider/license_resource.go
@@ -39,9 +39,12 @@ func (r *LicenseResource) Metadata(ctx context.Context, req resource.MetadataReq
func (r *LicenseResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
- MarkdownDescription: "A license for a Coder deployment.\n\nIt's recommended to create multiple instances of this " +
- "resource when updating a license. Modifying an existing license will cause the resource to be replaced, " +
- "which may result in a brief unlicensed period.\n\n" +
+ MarkdownDescription: "A license for a Coder deployment.\n\nIt's recommended to set " +
+ "[`create_before_destroy`](https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle#create_before_destroy) " +
+ "on license resources. Without setting this, Terraform will remove the old " +
+ "license before adding the updated license. This will result in a temporary " +
+ "disruption to your users; during which they may be unable to use features " +
+ "that require a license.\n\n" +
"Terraform does not guarantee this resource " +
"will be created before other resources or attributes that require a licensed deployment. " +
"The `depends_on` meta-argument is instead recommended.",
diff --git a/internal/provider/license_resource_test.go b/internal/provider/license_resource_test.go
index e2d13d6..9a4abc7 100644
--- a/internal/provider/license_resource_test.go
+++ b/internal/provider/license_resource_test.go
@@ -24,7 +24,7 @@ func TestAccLicenseResource(t *testing.T) {
t.Skip("No license found for license resource tests, skipping")
}
- cfg1 := testAccLicenseResourceconfig{
+ cfg1 := testAccLicenseResourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
License: license,
@@ -42,13 +42,13 @@ func TestAccLicenseResource(t *testing.T) {
})
}
-type testAccLicenseResourceconfig struct {
+type testAccLicenseResourceConfig struct {
URL string
Token string
License string
}
-func (c testAccLicenseResourceconfig) String(t *testing.T) string {
+func (c testAccLicenseResourceConfig) String(t *testing.T) string {
t.Helper()
tpl := `
provider coderd {
diff --git a/internal/provider/organization_data_source_test.go b/internal/provider/organization_data_source_test.go
index 7f9bef3..c7bb982 100644
--- a/internal/provider/organization_data_source_test.go
+++ b/internal/provider/organization_data_source_test.go
@@ -8,6 +8,7 @@ import (
"testing"
"text/template"
+ "github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/terraform-provider-coderd/integration"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
@@ -37,7 +38,7 @@ func TestAccOrganizationDataSource(t *testing.T) {
cfg := testAccOrganizationDataSourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- ID: PtrTo(firstUser.OrganizationIDs[0].String()),
+ ID: ptr.Ref(firstUser.OrganizationIDs[0].String()),
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
@@ -55,7 +56,7 @@ func TestAccOrganizationDataSource(t *testing.T) {
cfg := testAccOrganizationDataSourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- Name: PtrTo("coder"),
+ Name: ptr.Ref("coder"),
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
@@ -73,7 +74,7 @@ func TestAccOrganizationDataSource(t *testing.T) {
cfg := testAccOrganizationDataSourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- IsDefault: PtrTo(true),
+ IsDefault: ptr.Ref(true),
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
@@ -91,8 +92,8 @@ func TestAccOrganizationDataSource(t *testing.T) {
cfg := testAccOrganizationDataSourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- IsDefault: PtrTo(true),
- Name: PtrTo("coder"),
+ IsDefault: ptr.Ref(true),
+ Name: ptr.Ref("coder"),
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
diff --git a/internal/provider/organization_resource.go b/internal/provider/organization_resource.go
new file mode 100644
index 0000000..397f92e
--- /dev/null
+++ b/internal/provider/organization_resource.go
@@ -0,0 +1,551 @@
+package provider
+
+import (
+ "context"
+ "fmt"
+ "regexp"
+
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/terraform-provider-coderd/internal/codersdkvalidator"
+ "github.com/google/uuid"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/attr"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+)
+
+// Ensure provider defined types fully satisfy framework interfaces.
+var _ resource.Resource = &OrganizationResource{}
+var _ resource.ResourceWithImportState = &OrganizationResource{}
+
+type OrganizationResource struct {
+ *CoderdProviderData
+}
+
+// OrganizationResourceModel describes the resource data model.
+type OrganizationResourceModel struct {
+ ID UUID `tfsdk:"id"`
+
+ Name types.String `tfsdk:"name"`
+ DisplayName types.String `tfsdk:"display_name"`
+ Description types.String `tfsdk:"description"`
+ Icon types.String `tfsdk:"icon"`
+
+ GroupSync types.Object `tfsdk:"group_sync"`
+ RoleSync types.Object `tfsdk:"role_sync"`
+}
+
+type GroupSyncModel struct {
+ Field types.String `tfsdk:"field"`
+ RegexFilter types.String `tfsdk:"regex_filter"`
+ AutoCreateMissing types.Bool `tfsdk:"auto_create_missing"`
+ Mapping types.Map `tfsdk:"mapping"`
+}
+
+var groupSyncAttrTypes = map[string]attr.Type{
+ "field": types.StringType,
+ "regex_filter": types.StringType,
+ "auto_create_missing": types.BoolType,
+ "mapping": types.MapType{ElemType: types.ListType{ElemType: UUIDType}},
+}
+
+func (m GroupSyncModel) ValueObject() types.Object {
+ return types.ObjectValueMust(groupSyncAttrTypes, map[string]attr.Value{
+ "field": m.Field,
+ "regex_filter": m.RegexFilter,
+ "auto_create_missing": m.AutoCreateMissing,
+ "mapping": m.Mapping,
+ })
+}
+
+type RoleSyncModel struct {
+ Field types.String `tfsdk:"field"`
+ Mapping types.Map `tfsdk:"mapping"`
+}
+
+var roleSyncAttrTypes = map[string]attr.Type{
+ "field": types.StringType,
+ "mapping": types.MapType{ElemType: types.ListType{ElemType: types.StringType}},
+}
+
+func (m RoleSyncModel) ValueObject() types.Object {
+ return types.ObjectValueMust(roleSyncAttrTypes, map[string]attr.Value{
+ "field": m.Field,
+ "mapping": m.Mapping,
+ })
+}
+
+func NewOrganizationResource() resource.Resource {
+ return &OrganizationResource{}
+}
+
+func (r *OrganizationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_organization"
+}
+
+func (r *OrganizationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "An organization on the Coder deployment",
+
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ CustomType: UUIDType,
+ Computed: true,
+ MarkdownDescription: "Organization ID",
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "Name of the organization.",
+ Required: true,
+ Validators: []validator.String{
+ codersdkvalidator.Name(),
+ },
+ },
+ "display_name": schema.StringAttribute{
+ MarkdownDescription: "Display name of the organization. Defaults to name.",
+ Computed: true,
+ Optional: true,
+ Default: stringdefault.StaticString(""),
+ Validators: []validator.String{
+ codersdkvalidator.DisplayName(),
+ },
+ },
+ "description": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Default: stringdefault.StaticString(""),
+ },
+ "icon": schema.StringAttribute{
+ Optional: true,
+ Computed: true,
+ Default: stringdefault.StaticString(""),
+ },
+ },
+
+ Blocks: map[string]schema.Block{
+ "group_sync": schema.SingleNestedBlock{
+ Attributes: map[string]schema.Attribute{
+ "field": schema.StringAttribute{
+ Optional: true,
+ MarkdownDescription: "The claim field that specifies what groups " +
+ "a user should be in.",
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ },
+ },
+ "regex_filter": schema.StringAttribute{
+ Optional: true,
+ MarkdownDescription: "A regular expression that will be used to " +
+ "filter the groups returned by the OIDC provider. Any group " +
+ "not matched will be ignored.",
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ codersdkvalidator.Regexp(),
+ },
+ },
+ "auto_create_missing": schema.BoolAttribute{
+ Optional: true,
+ MarkdownDescription: "Controls whether groups will be created if " +
+ "they are missing.",
+ },
+ "mapping": schema.MapAttribute{
+ ElementType: types.ListType{ElemType: UUIDType},
+ Optional: true,
+ MarkdownDescription: "A map from OIDC group name to Coder group ID.",
+ },
+ },
+ },
+ "role_sync": schema.SingleNestedBlock{
+ Attributes: map[string]schema.Attribute{
+ "field": schema.StringAttribute{
+ Optional: true,
+ MarkdownDescription: "The claim field that specifies what " +
+ "organization roles a user should be given.",
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ },
+ },
+ "mapping": schema.MapAttribute{
+ ElementType: types.ListType{ElemType: types.StringType},
+ Optional: true,
+ MarkdownDescription: "A map from OIDC group name to Coder " +
+ "organization role.",
+ },
+ },
+ },
+ },
+ }
+}
+
+func (r *OrganizationResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ // Prevent panic if the provider has not been configured.
+ if req.ProviderData == nil {
+ return
+ }
+
+ data, ok := req.ProviderData.(*CoderdProviderData)
+
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unable to configure provider data",
+ fmt.Sprintf("Expected *CoderdProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+
+ return
+ }
+
+ r.CoderdProviderData = data
+}
+
+func (r *OrganizationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ // Read Terraform prior state data into the model
+ var data OrganizationResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ var org codersdk.Organization
+ var err error
+ if data.ID.IsNull() {
+ orgName := data.Name.ValueString()
+ org, err = r.Client.OrganizationByName(ctx, orgName)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization by name, got error: %s", err))
+ return
+ }
+ data.ID = UUIDValue(org.ID)
+ } else {
+ orgID := data.ID.ValueUUID()
+ org, err = r.Client.Organization(ctx, orgID)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization by ID, got error: %s", err))
+ return
+ }
+ }
+
+ if !data.GroupSync.IsNull() {
+ groupSync, err := r.Client.GroupIDPSyncSettings(ctx, data.ID.ValueUUID().String())
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("unable to get organization group sync settings, got error: %s", err))
+ return
+ }
+
+ // Read values from Terraform
+ var groupSyncData GroupSyncModel
+ resp.Diagnostics.Append(data.GroupSync.As(ctx, &groupSyncData, basetypes.ObjectAsOptions{})...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if !groupSyncData.Field.IsNull() {
+ groupSyncData.Field = types.StringValue(groupSync.Field)
+ }
+ if !groupSyncData.RegexFilter.IsNull() {
+ groupSyncData.RegexFilter = types.StringValue(groupSync.RegexFilter.String())
+ }
+ if !groupSyncData.AutoCreateMissing.IsNull() {
+ groupSyncData.AutoCreateMissing = types.BoolValue(groupSync.AutoCreateMissing)
+ }
+ if !groupSyncData.Mapping.IsNull() {
+ elements := make(map[string][]string)
+ for key, ids := range groupSync.Mapping {
+ for _, id := range ids {
+ elements[key] = append(elements[key], id.String())
+ }
+ }
+
+ mapping, diags := types.MapValueFrom(ctx, types.ListType{ElemType: UUIDType}, elements)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ groupSyncData.Mapping = mapping
+ }
+
+ data.GroupSync = groupSyncData.ValueObject()
+ }
+
+ if !data.RoleSync.IsNull() {
+ roleSync, err := r.Client.RoleIDPSyncSettings(ctx, data.ID.ValueUUID().String())
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("unable to get organization role sync settings, got error: %s", err))
+ return
+ }
+
+ // Read values from Terraform
+ var roleSyncData RoleSyncModel
+ resp.Diagnostics.Append(data.RoleSync.As(ctx, &roleSyncData, basetypes.ObjectAsOptions{})...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ if !roleSyncData.Field.IsNull() {
+ roleSyncData.Field = types.StringValue(roleSync.Field)
+ }
+ if !roleSyncData.Mapping.IsNull() {
+ mapping, diags := types.MapValueFrom(ctx, types.ListType{ElemType: types.StringType}, roleSync.Mapping)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ roleSyncData.Mapping = mapping
+ }
+
+ data.RoleSync = roleSyncData.ValueObject()
+ }
+
+ // We've fetched the organization ID from state, and the latest values for
+ // everything else from the backend. Ensure that any mutable data is synced
+ // with the backend.
+ data.Name = types.StringValue(org.Name)
+ data.DisplayName = types.StringValue(org.DisplayName)
+ data.Description = types.StringValue(org.Description)
+ data.Icon = types.StringValue(org.Icon)
+
+ // Save updated data into Terraform state
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *OrganizationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ // Read Terraform plan data into the model
+ var data OrganizationResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tflog.Trace(ctx, "creating organization", map[string]any{
+ "id": data.ID.ValueUUID(),
+ "name": data.Name.ValueString(),
+ "display_name": data.DisplayName.ValueString(),
+ "description": data.Description.ValueString(),
+ "icon": data.Icon.ValueString(),
+ })
+ org, err := r.Client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
+ Name: data.Name.ValueString(),
+ DisplayName: data.DisplayName.ValueString(),
+ Description: data.Description.ValueString(),
+ Icon: data.Icon.ValueString(),
+ })
+ if err != nil {
+ resp.Diagnostics.AddError("Failed to create organization", err.Error())
+ return
+ }
+ tflog.Trace(ctx, "successfully created organization", map[string]any{
+ "id": org.ID,
+ "name": org.Name,
+ "display_name": org.DisplayName,
+ "description": org.Description,
+ "icon": org.Icon,
+ })
+ // Fill in `ID` since it must be "computed".
+ data.ID = UUIDValue(org.ID)
+ // We also fill in `DisplayName`, since it's optional but the backend will
+ // default it.
+ data.DisplayName = types.StringValue(org.DisplayName)
+
+ // Now apply group and role sync settings, if specified
+ orgID := data.ID.ValueUUID()
+ tflog.Trace(ctx, "updating group sync", map[string]any{
+ "orgID": orgID,
+ })
+ if !data.GroupSync.IsNull() {
+ resp.Diagnostics.Append(r.patchGroupSync(ctx, orgID, data.GroupSync)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ }
+ tflog.Trace(ctx, "updating role sync", map[string]any{
+ "orgID": orgID,
+ })
+ if !data.RoleSync.IsNull() {
+ resp.Diagnostics.Append(r.patchRoleSync(ctx, orgID, data.RoleSync)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ }
+
+ // Save data into Terraform state
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *OrganizationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ // Read Terraform plan data into the model
+ var data OrganizationResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ orgID := data.ID.ValueUUID()
+
+ // Update the organization metadata
+ tflog.Trace(ctx, "updating organization", map[string]any{
+ "id": orgID,
+ "new_name": data.Name.ValueString(),
+ "new_display_name": data.DisplayName.ValueString(),
+ "new_description": data.Description.ValueString(),
+ "new_icon": data.Icon.ValueString(),
+ })
+ org, err := r.Client.UpdateOrganization(ctx, orgID.String(), codersdk.UpdateOrganizationRequest{
+ Name: data.Name.ValueString(),
+ DisplayName: data.DisplayName.ValueString(),
+ Description: data.Description.ValueStringPointer(),
+ Icon: data.Icon.ValueStringPointer(),
+ })
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to update organization %s, got error: %s", orgID, err))
+ return
+ }
+
+ tflog.Trace(ctx, "successfully updated organization", map[string]any{
+ "id": orgID,
+ "name": org.Name,
+ "display_name": org.DisplayName,
+ "description": org.Description,
+ "icon": org.Icon,
+ })
+
+ tflog.Trace(ctx, "updating group sync", map[string]any{
+ "orgID": orgID,
+ })
+ if !data.GroupSync.IsNull() {
+ resp.Diagnostics.Append(r.patchGroupSync(ctx, orgID, data.GroupSync)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ }
+ tflog.Trace(ctx, "updating role sync", map[string]any{
+ "orgID": orgID,
+ })
+ if !data.RoleSync.IsNull() {
+ resp.Diagnostics.Append(r.patchRoleSync(ctx, orgID, data.RoleSync)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ }
+
+ // Save updated data into Terraform state
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *OrganizationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ // Read Terraform prior state data into the model
+ var data OrganizationResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ orgID := data.ID.ValueUUID()
+
+ tflog.Trace(ctx, "deleting organization", map[string]any{
+ "id": orgID,
+ "name": data.Name.ValueString(),
+ })
+ err := r.Client.DeleteOrganization(ctx, orgID.String())
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete organization %s, got error: %s", orgID, err))
+ return
+ }
+ tflog.Trace(ctx, "successfully deleted organization", map[string]any{
+ "id": orgID,
+ "name": data.Name.ValueString(),
+ })
+
+ // Read Terraform prior state data into the model
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+}
+
+func (r *OrganizationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ // Terraform will eventually `Read` in the rest of the fields after we have
+ // set the `name` attribute.
+ resource.ImportStatePassthroughID(ctx, path.Root("name"), req, resp)
+}
+
+func (r *OrganizationResource) patchGroupSync(
+ ctx context.Context,
+ orgID uuid.UUID,
+ groupSyncObject types.Object,
+) diag.Diagnostics {
+ var diags diag.Diagnostics
+
+ // Read values from Terraform
+ var groupSyncData GroupSyncModel
+ diags.Append(groupSyncObject.As(ctx, &groupSyncData, basetypes.ObjectAsOptions{})...)
+ if diags.HasError() {
+ return diags
+ }
+
+ // Convert that into the type used to send the PATCH to the backend
+ var groupSync codersdk.GroupSyncSettings
+ groupSync.Field = groupSyncData.Field.ValueString()
+ groupSync.RegexFilter = regexp.MustCompile(groupSyncData.RegexFilter.ValueString())
+ groupSync.AutoCreateMissing = groupSyncData.AutoCreateMissing.ValueBool()
+ groupSync.Mapping = make(map[string][]uuid.UUID)
+ // Terraform doesn't know how to turn one our `UUID` Terraform values into a
+ // `uuid.UUID`, so we have to do the unwrapping manually here.
+ var mapping map[string][]UUID
+ diags.Append(groupSyncData.Mapping.ElementsAs(ctx, &mapping, false)...)
+ if diags.HasError() {
+ return diags
+ }
+ for key, ids := range mapping {
+ for _, id := range ids {
+ groupSync.Mapping[key] = append(groupSync.Mapping[key], id.ValueUUID())
+ }
+ }
+
+ // Perform the PATCH
+ _, err := r.Client.PatchGroupIDPSyncSettings(ctx, orgID.String(), groupSync)
+ if err != nil {
+ diags.AddError("Group Sync Update error", err.Error())
+ return diags
+ }
+
+ return diags
+}
+
+func (r *OrganizationResource) patchRoleSync(
+ ctx context.Context,
+ orgID uuid.UUID,
+ roleSyncObject types.Object,
+) diag.Diagnostics {
+ var diags diag.Diagnostics
+
+ // Read values from Terraform
+ var roleSyncData RoleSyncModel
+ diags.Append(roleSyncObject.As(ctx, &roleSyncData, basetypes.ObjectAsOptions{})...)
+ if diags.HasError() {
+ return diags
+ }
+
+ // Convert that into the type used to send the PATCH to the backend
+ var roleSync codersdk.RoleSyncSettings
+ roleSync.Field = roleSyncData.Field.ValueString()
+ diags.Append(roleSyncData.Mapping.ElementsAs(ctx, &roleSync.Mapping, false)...)
+ if diags.HasError() {
+ return diags
+ }
+
+ // Perform the PATCH
+ _, err := r.Client.PatchRoleIDPSyncSettings(ctx, orgID.String(), roleSync)
+ if err != nil {
+ diags.AddError("Role Sync Update error", err.Error())
+ return diags
+ }
+
+ return diags
+}
diff --git a/internal/provider/organization_resource_test.go b/internal/provider/organization_resource_test.go
new file mode 100644
index 0000000..0a755c4
--- /dev/null
+++ b/internal/provider/organization_resource_test.go
@@ -0,0 +1,165 @@
+package provider
+
+import (
+ "context"
+ "os"
+ "strings"
+ "testing"
+ "text/template"
+
+ "github.com/coder/coder/v2/coderd/util/ptr"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/terraform-provider-coderd/integration"
+ "github.com/google/uuid"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/knownvalue"
+ "github.com/hashicorp/terraform-plugin-testing/statecheck"
+ "github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
+ "github.com/stretchr/testify/require"
+)
+
+func TestAccOrganizationResource(t *testing.T) {
+ if os.Getenv("TF_ACC") == "" {
+ t.Skip("Acceptance tests are disabled.")
+ }
+
+ ctx := context.Background()
+ client := integration.StartCoder(ctx, t, "organization_acc", true)
+ _, err := client.User(ctx, codersdk.Me)
+ require.NoError(t, err)
+
+ cfg1 := testAccOrganizationResourceConfig{
+ URL: client.URL.String(),
+ Token: client.SessionToken(),
+ Name: ptr.Ref("example-org"),
+ DisplayName: ptr.Ref("Example Organization"),
+ Description: ptr.Ref("This is an example organization"),
+ Icon: ptr.Ref("/icon/coder.svg"),
+ }
+
+ cfg2 := cfg1
+ cfg2.Name = ptr.Ref("example-org-new")
+ cfg2.DisplayName = ptr.Ref("Example Organization New")
+
+ cfg3 := cfg2
+ cfg3.GroupSync = ptr.Ref(codersdk.GroupSyncSettings{
+ Field: "wibble",
+ Mapping: map[string][]uuid.UUID{
+ "wibble": {uuid.MustParse("6e57187f-6543-46ab-a62c-a10065dd4314")},
+ },
+ })
+ cfg3.RoleSync = ptr.Ref(codersdk.RoleSyncSettings{
+ Field: "wobble",
+ Mapping: map[string][]string{
+ "wobble": {"wobbly"},
+ },
+ })
+
+ t.Run("CreateImportUpdateReadOk", func(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ IsUnitTest: true,
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ // Create and Read
+ {
+ Config: cfg1.String(t),
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("name"), knownvalue.StringExact("example-org")),
+ statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("display_name"), knownvalue.StringExact("Example Organization")),
+ statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("icon"), knownvalue.StringExact("/icon/coder.svg")),
+ },
+ },
+ // Import
+ {
+ Config: cfg1.String(t),
+ ResourceName: "coderd_organization.test",
+ ImportState: true,
+ ImportStateVerify: true,
+ ImportStateId: *cfg1.Name,
+ },
+ // Update and Read
+ {
+ Config: cfg2.String(t),
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("name"), knownvalue.StringExact("example-org-new")),
+ statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("display_name"), knownvalue.StringExact("Example Organization New")),
+ },
+ },
+ // Add group and role sync
+ {
+ Config: cfg3.String(t),
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("group_sync").AtMapKey("field"), knownvalue.StringExact("wibble")),
+ statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("group_sync").AtMapKey("mapping").AtMapKey("wibble").AtSliceIndex(0), knownvalue.StringExact("6e57187f-6543-46ab-a62c-a10065dd4314")),
+ statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("role_sync").AtMapKey("field"), knownvalue.StringExact("wobble")),
+ statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("role_sync").AtMapKey("mapping").AtMapKey("wobble").AtSliceIndex(0), knownvalue.StringExact("wobbly")),
+ },
+ },
+ },
+ })
+ })
+}
+
+type testAccOrganizationResourceConfig struct {
+ URL string
+ Token string
+
+ Name *string
+ DisplayName *string
+ Description *string
+ Icon *string
+
+ GroupSync *codersdk.GroupSyncSettings
+ RoleSync *codersdk.RoleSyncSettings
+}
+
+func (c testAccOrganizationResourceConfig) String(t *testing.T) string {
+ t.Helper()
+ tpl := `
+provider coderd {
+ url = "{{.URL}}"
+ token = "{{.Token}}"
+}
+
+resource "coderd_organization" "test" {
+ name = {{orNull .Name}}
+ display_name = {{orNull .DisplayName}}
+ description = {{orNull .Description}}
+ icon = {{orNull .Icon}}
+
+ {{- if .GroupSync}}
+ group_sync {
+ field = "{{.GroupSync.Field}}"
+ mapping = {
+ {{- range $key, $value := .GroupSync.Mapping}}
+ {{$key}} = {{printf "%q" $value}}
+ {{- end}}
+ }
+ }
+ {{- end}}
+
+ {{- if .RoleSync}}
+ role_sync {
+ field = "{{.RoleSync.Field}}"
+ mapping = {
+ {{- range $key, $value := .RoleSync.Mapping}}
+ {{$key}} = {{printf "%q" $value}}
+ {{- end}}
+ }
+ }
+ {{- end}}
+}
+`
+ funcMap := template.FuncMap{
+ "orNull": PrintOrNull,
+ }
+
+ buf := strings.Builder{}
+ tmpl, err := template.New("organizationResource").Funcs(funcMap).Parse(tpl)
+ require.NoError(t, err)
+
+ err = tmpl.Execute(&buf, c)
+ require.NoError(t, err)
+ return buf.String()
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index bfeea5e..b8b9fa7 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -78,7 +78,6 @@ This provider is only compatible with Coder version [2.10.1](https://github.com/
func (p *CoderdProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
var data CoderdProviderModel
-
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
@@ -139,6 +138,8 @@ func (p *CoderdProvider) Resources(ctx context.Context) []func() resource.Resour
NewTemplateResource,
NewWorkspaceProxyResource,
NewLicenseResource,
+ NewOrganizationResource,
+ NewProvisionerKeyResource,
}
}
diff --git a/internal/provider/provisioner_key_resource.go b/internal/provider/provisioner_key_resource.go
new file mode 100644
index 0000000..5904df0
--- /dev/null
+++ b/internal/provider/provisioner_key_resource.go
@@ -0,0 +1,154 @@
+package provider
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+
+ "github.com/coder/coder/v2/codersdk"
+)
+
+// Ensure provider defined types fully satisfy framework interfaces.
+var _ resource.Resource = &ProvisionerKeyResource{}
+
+func NewProvisionerKeyResource() resource.Resource {
+ return &ProvisionerKeyResource{}
+}
+
+// ProvisionerKeyResource defines the resource implementation.
+type ProvisionerKeyResource struct {
+ *CoderdProviderData
+}
+
+// ProvisionerKeyResourceModel describes the resource data model.
+type ProvisionerKeyResourceModel struct {
+ OrganizationID UUID `tfsdk:"organization_id"`
+ Name types.String `tfsdk:"name"`
+ Tags types.Map `tfsdk:"tags"`
+ Key types.String `tfsdk:"key"`
+}
+
+func (r *ProvisionerKeyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_provisioner_key"
+}
+
+func (r *ProvisionerKeyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "A provisioner key for a Coder deployment.",
+
+ Attributes: map[string]schema.Attribute{
+ "organization_id": schema.StringAttribute{
+ CustomType: UUIDType,
+ MarkdownDescription: "The organization that provisioners connected with this key will be connected to.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "name": schema.StringAttribute{
+ MarkdownDescription: "The name of the key.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "tags": schema.MapAttribute{
+ MarkdownDescription: "The tags that provisioners connected with this key will accept jobs for.",
+ ElementType: types.StringType,
+ Optional: true,
+ PlanModifiers: []planmodifier.Map{
+ mapplanmodifier.RequiresReplace(),
+ },
+ },
+ "key": schema.StringAttribute{
+ MarkdownDescription: "The acquired provisioner key",
+ Computed: true,
+ Sensitive: true,
+ },
+ },
+ }
+}
+
+func (r *ProvisionerKeyResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ // Prevent panic if the provider has not been configured.
+ if req.ProviderData == nil {
+ return
+ }
+
+ data, ok := req.ProviderData.(*CoderdProviderData)
+
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *CoderdProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+
+ return
+ }
+
+ r.CoderdProviderData = data
+}
+
+func (r *ProvisionerKeyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ // Read Terraform plan data into the model
+ var data ProvisionerKeyResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ var tags map[string]string
+ resp.Diagnostics.Append(data.Tags.ElementsAs(ctx, &tags, false)...)
+ createKeyResult, err := r.Client.CreateProvisionerKey(ctx, data.OrganizationID.ValueUUID(), codersdk.CreateProvisionerKeyRequest{
+ Name: data.Name.ValueString(),
+ Tags: tags,
+ })
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create provisioner_key, got error: %s", err))
+ return
+ }
+
+ data.Key = types.StringValue(createKeyResult.Key)
+ // Save data into Terraform state
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ProvisionerKeyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ // Read Terraform prior state data into the model
+ var data ProvisionerKeyResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Provisioner keys are immutable, no reading necessary.
+
+ // Save updated data into Terraform state
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *ProvisionerKeyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ // Provisioner keys are immutable, updating is always invalid.
+ resp.Diagnostics.AddError("Invalid Update", "Terraform is attempting to update a resource which must be replaced")
+}
+
+func (r *ProvisionerKeyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ // Read Terraform prior state data into the model
+ var data ProvisionerKeyResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ err := r.Client.DeleteProvisionerKey(ctx, data.OrganizationID.ValueUUID(), data.Name.ValueString())
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete provisionerkey, got error: %s", err))
+ return
+ }
+}
diff --git a/internal/provider/provisioner_key_resource_test.go b/internal/provider/provisioner_key_resource_test.go
new file mode 100644
index 0000000..9c89647
--- /dev/null
+++ b/internal/provider/provisioner_key_resource_test.go
@@ -0,0 +1,114 @@
+package provider
+
+import (
+ "context"
+ "os"
+ "strings"
+ "testing"
+ "text/template"
+
+ "github.com/coder/terraform-provider-coderd/integration"
+ "github.com/google/uuid"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-plugin-testing/knownvalue"
+ "github.com/hashicorp/terraform-plugin-testing/plancheck"
+ "github.com/hashicorp/terraform-plugin-testing/statecheck"
+ "github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
+ "github.com/stretchr/testify/require"
+)
+
+func TestAccProvisionerKeyResource(t *testing.T) {
+ if os.Getenv("TF_ACC") == "" {
+ t.Skip("Acceptance tests are disabled.")
+ }
+ ctx := context.Background()
+ client := integration.StartCoder(ctx, t, "provisioner_key_acc", true)
+ orgs, err := client.Organizations(ctx)
+ require.NoError(t, err)
+ firstOrg := orgs[0].ID
+
+ cfg1 := testAccProvisionerKeyResourceConfig{
+ URL: client.URL.String(),
+ Token: client.SessionToken(),
+
+ OrganizationID: firstOrg,
+ Name: "example-provisioner-key",
+ }
+
+ cfg2 := cfg1
+ cfg2.Tags = map[string]string{
+ "wibble": "wobble",
+ }
+
+ cfg3 := cfg2
+ cfg3.Name = "different-provisioner-key"
+
+ resource.Test(t, resource.TestCase{
+ IsUnitTest: true,
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: cfg1.String(t),
+ },
+ {
+ Config: cfg2.String(t),
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectResourceAction("coderd_provisioner_key.test", plancheck.ResourceActionReplace),
+ },
+ },
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("coderd_provisioner_key.test", tfjsonpath.New("tags").AtMapKey("wibble"), knownvalue.StringExact("wobble")),
+ },
+ },
+ {
+ Config: cfg3.String(t),
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectResourceAction("coderd_provisioner_key.test", plancheck.ResourceActionReplace),
+ },
+ },
+ },
+ },
+ })
+}
+
+type testAccProvisionerKeyResourceConfig struct {
+ URL string
+ Token string
+
+ OrganizationID uuid.UUID
+ Name string
+ Tags map[string]string
+}
+
+func (c testAccProvisionerKeyResourceConfig) String(t *testing.T) string {
+ t.Helper()
+
+ tpl := `
+provider coderd {
+ url = "{{.URL}}"
+ token = "{{.Token}}"
+}
+
+resource "coderd_provisioner_key" "test" {
+ organization_id = "{{.OrganizationID}}"
+ name = "{{.Name}}"
+
+ tags = {
+ {{- range $key, $value := .Tags}}
+ {{$key}} = "{{$value}}"
+ {{- end}}
+ }
+}
+`
+
+ buf := strings.Builder{}
+ tmpl, err := template.New("provisionerKeyResource").Parse(tpl)
+ require.NoError(t, err)
+
+ err = tmpl.Execute(&buf, c)
+ require.NoError(t, err)
+ return buf.String()
+}
diff --git a/internal/provider/template_data_source_test.go b/internal/provider/template_data_source_test.go
index cebdba7..f6759b3 100644
--- a/internal/provider/template_data_source_test.go
+++ b/internal/provider/template_data_source_test.go
@@ -14,6 +14,7 @@ import (
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/stretchr/testify/require"
+ "github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/terraform-provider-coderd/integration"
)
@@ -49,8 +50,8 @@ func TestAccTemplateDataSource(t *testing.T) {
Description: "An example template",
Icon: "/path/to/icon.png",
VersionID: version.ID,
- DefaultTTLMillis: PtrTo((10 * time.Hour).Milliseconds()),
- ActivityBumpMillis: PtrTo((4 * time.Hour).Milliseconds()),
+ DefaultTTLMillis: ptr.Ref((10 * time.Hour).Milliseconds()),
+ ActivityBumpMillis: ptr.Ref((4 * time.Hour).Milliseconds()),
AutostopRequirement: &codersdk.TemplateAutostopRequirement{
DaysOfWeek: []string{"sunday"},
Weeks: 1,
@@ -58,12 +59,12 @@ func TestAccTemplateDataSource(t *testing.T) {
AutostartRequirement: &codersdk.TemplateAutostartRequirement{
DaysOfWeek: []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"},
},
- AllowUserCancelWorkspaceJobs: PtrTo(true),
- AllowUserAutostart: PtrTo(true),
- AllowUserAutostop: PtrTo(true),
- FailureTTLMillis: PtrTo((1 * time.Hour).Milliseconds()),
- TimeTilDormantMillis: PtrTo((7 * 24 * time.Hour).Milliseconds()),
- TimeTilDormantAutoDeleteMillis: PtrTo((30 * 24 * time.Hour).Milliseconds()),
+ AllowUserCancelWorkspaceJobs: ptr.Ref(true),
+ AllowUserAutostart: ptr.Ref(true),
+ AllowUserAutostop: ptr.Ref(true),
+ FailureTTLMillis: ptr.Ref((1 * time.Hour).Milliseconds()),
+ TimeTilDormantMillis: ptr.Ref((7 * 24 * time.Hour).Milliseconds()),
+ TimeTilDormantAutoDeleteMillis: ptr.Ref((30 * 24 * time.Hour).Milliseconds()),
DisableEveryoneGroupAccess: true,
RequireActiveVersion: true,
})
@@ -93,9 +94,9 @@ func TestAccTemplateDataSource(t *testing.T) {
UpdateWorkspaceLastUsedAt: false,
UpdateWorkspaceDormantAt: false,
RequireActiveVersion: tpl.RequireActiveVersion,
- DeprecationMessage: PtrTo("This template is deprecated"),
+ DeprecationMessage: ptr.Ref("This template is deprecated"),
DisableEveryoneGroupAccess: true,
- MaxPortShareLevel: PtrTo(codersdk.WorkspaceAgentPortShareLevelOwner),
+ MaxPortShareLevel: ptr.Ref(codersdk.WorkspaceAgentPortShareLevelOwner),
})
require.NoError(t, err)
@@ -153,8 +154,8 @@ func TestAccTemplateDataSource(t *testing.T) {
cfg := testAccTemplateDataSourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- OrganizationID: PtrTo(orgID.String()),
- Name: PtrTo(tpl.Name),
+ OrganizationID: ptr.Ref(orgID.String()),
+ Name: ptr.Ref(tpl.Name),
}
resource.Test(t, resource.TestCase{
IsUnitTest: true,
@@ -173,7 +174,7 @@ func TestAccTemplateDataSource(t *testing.T) {
cfg := testAccTemplateDataSourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- ID: PtrTo(tpl.ID.String()),
+ ID: ptr.Ref(tpl.ID.String()),
}
resource.Test(t, resource.TestCase{
IsUnitTest: true,
@@ -210,7 +211,7 @@ func TestAccTemplateDataSource(t *testing.T) {
cfg := testAccTemplateDataSourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- Name: PtrTo(tpl.Name),
+ Name: ptr.Ref(tpl.Name),
}
resource.Test(t, resource.TestCase{
IsUnitTest: true,
diff --git a/internal/provider/template_resource.go b/internal/provider/template_resource.go
index f1d7adc..a5834db 100644
--- a/internal/provider/template_resource.go
+++ b/internal/provider/template_resource.go
@@ -10,8 +10,10 @@ import (
"strings"
"cdr.dev/slog"
+ "github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisionersdk"
+ "github.com/coder/terraform-provider-coderd/internal/codersdkvalidator"
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
@@ -257,8 +259,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
MarkdownDescription: "The name of the template.",
Required: true,
Validators: []validator.String{
- stringvalidator.LengthBetween(1, 32),
- stringvalidator.RegexMatches(nameValidRegex, "Template names must be alphanumeric with hyphens."),
+ codersdkvalidator.Name(),
},
},
"display_name": schema.StringAttribute{
@@ -266,8 +267,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
Optional: true,
Computed: true,
Validators: []validator.String{
- stringvalidator.LengthBetween(1, 64),
- stringvalidator.RegexMatches(displayNameRegex, "Template display names must be alphanumeric with spaces."),
+ codersdkvalidator.DisplayName(),
},
},
"description": schema.StringAttribute{
@@ -417,8 +417,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
Optional: true,
Computed: true,
Validators: []validator.String{
- stringvalidator.LengthBetween(1, 64),
- stringvalidator.RegexMatches(templateVersionNameRegex, "Template version names must be alphanumeric with underscores and dots."),
+ codersdkvalidator.TemplateVersionName(),
},
},
"message": schema.StringAttribute{
@@ -1253,7 +1252,7 @@ func (r *TemplateResourceModel) toUpdateRequest(ctx context.Context, diag *diag.
TimeTilDormantAutoDeleteMillis: r.TimeTilDormantAutoDeleteMillis.ValueInt64(),
RequireActiveVersion: r.RequireActiveVersion.ValueBool(),
DeprecationMessage: r.DeprecationMessage.ValueStringPointer(),
- MaxPortShareLevel: PtrTo(codersdk.WorkspaceAgentPortShareLevel(r.MaxPortShareLevel.ValueString())),
+ MaxPortShareLevel: ptr.Ref(codersdk.WorkspaceAgentPortShareLevel(r.MaxPortShareLevel.ValueString())),
// If we're managing ACL, we want to delete the everyone group
DisableEveryoneGroupAccess: !r.ACL.IsNull(),
}
diff --git a/internal/provider/template_resource_test.go b/internal/provider/template_resource_test.go
index acb07f2..c844da0 100644
--- a/internal/provider/template_resource_test.go
+++ b/internal/provider/template_resource_test.go
@@ -17,6 +17,7 @@ import (
cp "github.com/otiai10/copy"
"github.com/stretchr/testify/require"
+ "github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/terraform-provider-coderd/integration"
)
@@ -42,12 +43,12 @@ func TestAccTemplateResource(t *testing.T) {
cfg1 := testAccTemplateResourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- Name: PtrTo("example-template"),
+ Name: ptr.Ref("example-template"),
Versions: []testAccTemplateVersionConfig{
{
// Auto-generated version name
Directory: &exTemplateOne,
- Active: PtrTo(true),
+ Active: ptr.Ref(true),
},
},
ACL: testAccTemplateACLConfig{
@@ -57,28 +58,28 @@ func TestAccTemplateResource(t *testing.T) {
cfg2 := cfg1
cfg2.Versions = slices.Clone(cfg2.Versions)
- cfg2.Name = PtrTo("example-template-new")
+ cfg2.Name = ptr.Ref("example-template-new")
cfg2.Versions[0].Directory = &exTemplateTwo
- cfg2.Versions[0].Name = PtrTo("new")
+ cfg2.Versions[0].Name = ptr.Ref("new")
cfg3 := cfg2
cfg3.Versions = slices.Clone(cfg3.Versions)
cfg3.Versions = append(cfg3.Versions, testAccTemplateVersionConfig{
- Name: PtrTo("legacy-template"),
+ Name: ptr.Ref("legacy-template"),
Directory: &exTemplateOne,
- Active: PtrTo(false),
+ Active: ptr.Ref(false),
TerraformVariables: []testAccTemplateKeyValueConfig{
{
- Key: PtrTo("name"),
- Value: PtrTo("world"),
+ Key: ptr.Ref("name"),
+ Value: ptr.Ref("world"),
},
},
})
cfg4 := cfg3
cfg4.Versions = slices.Clone(cfg4.Versions)
- cfg4.Versions[0].Active = PtrTo(false)
- cfg4.Versions[1].Active = PtrTo(true)
+ cfg4.Versions[0].Active = ptr.Ref(false)
+ cfg4.Versions[1].Active = ptr.Ref(true)
cfg5 := cfg4
cfg5.Versions = slices.Clone(cfg5.Versions)
@@ -240,26 +241,26 @@ func TestAccTemplateResource(t *testing.T) {
cfg1 := testAccTemplateResourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- Name: PtrTo("example-template2"),
+ Name: ptr.Ref("example-template2"),
Versions: []testAccTemplateVersionConfig{
{
// Auto-generated version name
- Directory: PtrTo("../../integration/template-test/example-template-2/"),
+ Directory: ptr.Ref("../../integration/template-test/example-template-2/"),
TerraformVariables: []testAccTemplateKeyValueConfig{
{
- Key: PtrTo("name"),
- Value: PtrTo("world"),
+ Key: ptr.Ref("name"),
+ Value: ptr.Ref("world"),
},
},
- Active: PtrTo(true),
+ Active: ptr.Ref(true),
},
{
// Auto-generated version name
- Directory: PtrTo("../../integration/template-test/example-template-2/"),
+ Directory: ptr.Ref("../../integration/template-test/example-template-2/"),
TerraformVariables: []testAccTemplateKeyValueConfig{
{
- Key: PtrTo("name"),
- Value: PtrTo("world"),
+ Key: ptr.Ref("name"),
+ Value: ptr.Ref("world"),
},
},
},
@@ -271,28 +272,28 @@ func TestAccTemplateResource(t *testing.T) {
cfg2 := cfg1
cfg2.Versions = slices.Clone(cfg2.Versions)
- cfg2.Versions[1].Name = PtrTo("new-name")
+ cfg2.Versions[1].Name = ptr.Ref("new-name")
cfg3 := cfg2
cfg3.Versions = slices.Clone(cfg3.Versions)
- cfg3.Versions[0].Name = PtrTo("new-name-one")
- cfg3.Versions[1].Name = PtrTo("new-name-two")
+ cfg3.Versions[0].Name = ptr.Ref("new-name-one")
+ cfg3.Versions[1].Name = ptr.Ref("new-name-two")
cfg3.Versions[0], cfg3.Versions[1] = cfg3.Versions[1], cfg3.Versions[0]
cfg4 := cfg1
cfg4.Versions = slices.Clone(cfg4.Versions)
- cfg4.Versions[0].Directory = PtrTo("../../integration/template-test/example-template/")
+ cfg4.Versions[0].Directory = ptr.Ref("../../integration/template-test/example-template/")
cfg5 := cfg4
cfg5.Versions = slices.Clone(cfg5.Versions)
- cfg5.Versions[1].Directory = PtrTo("../../integration/template-test/example-template/")
+ cfg5.Versions[1].Directory = ptr.Ref("../../integration/template-test/example-template/")
cfg6 := cfg5
cfg6.Versions = slices.Clone(cfg6.Versions)
cfg6.Versions[0].TerraformVariables = []testAccTemplateKeyValueConfig{
{
- Key: PtrTo("name"),
- Value: PtrTo("world2"),
+ Key: ptr.Ref("name"),
+ Value: ptr.Ref("world2"),
},
}
@@ -368,18 +369,18 @@ func TestAccTemplateResource(t *testing.T) {
cfg1 := testAccTemplateResourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- Name: PtrTo("example-template3"),
+ Name: ptr.Ref("example-template3"),
Versions: []testAccTemplateVersionConfig{
{
// Auto-generated version name
- Directory: PtrTo("../../integration/template-test/example-template-2/"),
+ Directory: ptr.Ref("../../integration/template-test/example-template-2/"),
TerraformVariables: []testAccTemplateKeyValueConfig{
{
- Key: PtrTo("name"),
- Value: PtrTo("world"),
+ Key: ptr.Ref("name"),
+ Value: ptr.Ref("world"),
},
},
- Active: PtrTo(true),
+ Active: ptr.Ref(true),
},
},
ACL: testAccTemplateACLConfig{
@@ -391,8 +392,8 @@ func TestAccTemplateResource(t *testing.T) {
cfg2.Versions = slices.Clone(cfg2.Versions)
cfg2.Versions[0].TerraformVariables = []testAccTemplateKeyValueConfig{
{
- Key: PtrTo("name"),
- Value: PtrTo("world2"),
+ Key: ptr.Ref("name"),
+ Value: ptr.Ref("world2"),
},
}
@@ -423,7 +424,7 @@ func TestAccTemplateResourceEnterprise(t *testing.T) {
t.Skip("Acceptance tests are disabled.")
}
ctx := context.Background()
- client := integration.StartCoder(ctx, t, "template_acc", true)
+ client := integration.StartCoder(ctx, t, "template_resource_acc", true)
firstUser, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
@@ -436,29 +437,29 @@ func TestAccTemplateResourceEnterprise(t *testing.T) {
cfg1 := testAccTemplateResourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- Name: PtrTo("example-template"),
+ Name: ptr.Ref("example-template"),
Versions: []testAccTemplateVersionConfig{
{
// Auto-generated version name
- Directory: PtrTo("../../integration/template-test/example-template"),
- Active: PtrTo(true),
+ Directory: ptr.Ref("../../integration/template-test/example-template"),
+ Active: ptr.Ref(true),
},
},
ACL: testAccTemplateACLConfig{
GroupACL: []testAccTemplateKeyValueConfig{
{
- Key: PtrTo(firstUser.OrganizationIDs[0].String()),
- Value: PtrTo("use"),
+ Key: ptr.Ref(firstUser.OrganizationIDs[0].String()),
+ Value: ptr.Ref("use"),
},
{
- Key: PtrTo(group.ID.String()),
- Value: PtrTo("admin"),
+ Key: ptr.Ref(group.ID.String()),
+ Value: ptr.Ref("admin"),
},
},
UserACL: []testAccTemplateKeyValueConfig{
{
- Key: PtrTo(firstUser.ID.String()),
- Value: PtrTo("admin"),
+ Key: ptr.Ref(firstUser.ID.String()),
+ Value: ptr.Ref("admin"),
},
},
},
@@ -466,17 +467,17 @@ func TestAccTemplateResourceEnterprise(t *testing.T) {
cfg2 := cfg1
cfg2.ACL.GroupACL = slices.Clone(cfg2.ACL.GroupACL[1:])
- cfg2.MaxPortShareLevel = PtrTo("owner")
+ cfg2.MaxPortShareLevel = ptr.Ref("owner")
cfg3 := cfg2
cfg3.ACL.null = true
- cfg3.MaxPortShareLevel = PtrTo("public")
+ cfg3.MaxPortShareLevel = ptr.Ref("public")
cfg4 := cfg3
- cfg4.AllowUserAutostart = PtrTo(false)
+ cfg4.AllowUserAutostart = ptr.Ref(false)
cfg4.AutostopRequirement = testAccAutostopRequirementConfig{
- DaysOfWeek: PtrTo([]string{"monday", "tuesday"}),
- Weeks: PtrTo(int64(2)),
+ DaysOfWeek: ptr.Ref([]string{"monday", "tuesday"}),
+ Weeks: ptr.Ref(int64(2)),
}
resource.Test(t, resource.TestCase{
@@ -564,54 +565,54 @@ func TestAccTemplateResourceAGPL(t *testing.T) {
t.Skip("Acceptance tests are disabled.")
}
ctx := context.Background()
- client := integration.StartCoder(ctx, t, "template_acc", false)
+ client := integration.StartCoder(ctx, t, "template_resource_agpl_acc", false)
firstUser, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
cfg1 := testAccTemplateResourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- Name: PtrTo("example-template"),
+ Name: ptr.Ref("example-template"),
Versions: []testAccTemplateVersionConfig{
{
// Auto-generated version name
- Directory: PtrTo("../../integration/template-test/example-template/"),
- Active: PtrTo(true),
+ Directory: ptr.Ref("../../integration/template-test/example-template/"),
+ Active: ptr.Ref(true),
},
},
- AllowUserAutostart: PtrTo(false),
+ AllowUserAutostart: ptr.Ref(false),
}
cfg2 := cfg1
cfg2.AllowUserAutostart = nil
- cfg2.AutostopRequirement.DaysOfWeek = PtrTo([]string{"monday", "tuesday"})
+ cfg2.AutostopRequirement.DaysOfWeek = ptr.Ref([]string{"monday", "tuesday"})
cfg3 := cfg2
cfg3.AutostopRequirement.null = true
- cfg3.AutostartRequirement = PtrTo([]string{})
+ cfg3.AutostartRequirement = ptr.Ref([]string{})
cfg4 := cfg3
- cfg4.FailureTTL = PtrTo(int64(1))
+ cfg4.FailureTTL = ptr.Ref(int64(1))
cfg5 := cfg4
cfg5.FailureTTL = nil
cfg5.AutostartRequirement = nil
- cfg5.RequireActiveVersion = PtrTo(true)
+ cfg5.RequireActiveVersion = ptr.Ref(true)
cfg6 := cfg5
cfg6.RequireActiveVersion = nil
cfg6.ACL = testAccTemplateACLConfig{
GroupACL: []testAccTemplateKeyValueConfig{
{
- Key: PtrTo(firstUser.OrganizationIDs[0].String()),
- Value: PtrTo("use"),
+ Key: ptr.Ref(firstUser.OrganizationIDs[0].String()),
+ Value: ptr.Ref("use"),
},
},
}
cfg7 := cfg6
cfg7.ACL.null = true
- cfg7.MaxPortShareLevel = PtrTo("owner")
+ cfg7.MaxPortShareLevel = ptr.Ref("owner")
for _, cfg := range []testAccTemplateResourceConfig{cfg1, cfg2, cfg3, cfg4} {
resource.Test(t, resource.TestCase{
@@ -688,7 +689,7 @@ resource "coderd_template" "sample" {
}`
ctx := context.Background()
- client := integration.StartCoder(ctx, t, "template_acc", false)
+ client := integration.StartCoder(ctx, t, "template_resource_variables_acc", false)
cfg = fmt.Sprintf(cfg, client.URL.String(), client.SessionToken())
diff --git a/internal/provider/user_data_source_test.go b/internal/provider/user_data_source_test.go
index 2d69d13..6a9309f 100644
--- a/internal/provider/user_data_source_test.go
+++ b/internal/provider/user_data_source_test.go
@@ -8,6 +8,7 @@ import (
"testing"
"text/template"
+ "github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/terraform-provider-coderd/integration"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
@@ -53,9 +54,10 @@ func TestAccUserDataSource(t *testing.T) {
cfg := testAccUserDataSourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- Username: PtrTo(user.Username),
+ Username: ptr.Ref(user.Username),
}
resource.Test(t, resource.TestCase{
+ IsUnitTest: true,
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
@@ -71,9 +73,10 @@ func TestAccUserDataSource(t *testing.T) {
cfg := testAccUserDataSourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- ID: PtrTo(user.ID.String()),
+ ID: ptr.Ref(user.ID.String()),
}
resource.Test(t, resource.TestCase{
+ IsUnitTest: true,
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
// User by ID
@@ -91,6 +94,7 @@ func TestAccUserDataSource(t *testing.T) {
Token: client.SessionToken(),
}
resource.Test(t, resource.TestCase{
+ IsUnitTest: true,
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
// Neither ID nor Username
@@ -103,6 +107,24 @@ func TestAccUserDataSource(t *testing.T) {
})
})
+ t.Run("InvalidUUIDError", func(t *testing.T) {
+ cfg := testAccUserDataSourceConfig{
+ URL: client.URL.String(),
+ Token: client.SessionToken(),
+ ID: ptr.Ref("invalid-uuid"),
+ }
+ resource.Test(t, resource.TestCase{
+ IsUnitTest: true,
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: cfg.String(t),
+ ExpectError: regexp.MustCompile(`The provided value cannot be parsed as a UUID`),
+ },
+ },
+ })
+ })
}
type testAccUserDataSourceConfig struct {
diff --git a/internal/provider/user_resource.go b/internal/provider/user_resource.go
index a560df5..3fa570e 100644
--- a/internal/provider/user_resource.go
+++ b/internal/provider/user_resource.go
@@ -22,6 +22,7 @@ import (
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/coder/coder/v2/codersdk"
+ "github.com/coder/terraform-provider-coderd/internal/codersdkvalidator"
)
// Ensure provider defined types fully satisfy framework interfaces.
@@ -71,8 +72,7 @@ func (r *UserResource) Schema(ctx context.Context, req resource.SchemaRequest, r
MarkdownDescription: "Username of the user.",
Required: true,
Validators: []validator.String{
- stringvalidator.LengthBetween(1, 32),
- stringvalidator.RegexMatches(nameValidRegex, "Username must be alphanumeric with hyphens."),
+ codersdkvalidator.Name(),
},
},
"name": schema.StringAttribute{
@@ -80,7 +80,7 @@ func (r *UserResource) Schema(ctx context.Context, req resource.SchemaRequest, r
Computed: true,
Optional: true,
Validators: []validator.String{
- stringvalidator.LengthBetween(1, 128),
+ codersdkvalidator.UserRealName(),
},
},
"email": schema.StringAttribute{
diff --git a/internal/provider/user_resource_test.go b/internal/provider/user_resource_test.go
index a7bb470..b9c6bb7 100644
--- a/internal/provider/user_resource_test.go
+++ b/internal/provider/user_resource_test.go
@@ -7,6 +7,7 @@ import (
"testing"
"text/template"
+ "github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/terraform-provider-coderd/integration"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/stretchr/testify/require"
@@ -22,22 +23,22 @@ func TestAccUserResource(t *testing.T) {
cfg1 := testAccUserResourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- Username: PtrTo("example"),
- Name: PtrTo("Example User"),
- Email: PtrTo("example@coder.com"),
- Roles: PtrTo([]string{"owner", "auditor"}),
- LoginType: PtrTo("password"),
- Password: PtrTo("SomeSecurePassword!"),
+ Username: ptr.Ref("example"),
+ Name: ptr.Ref("Example User"),
+ Email: ptr.Ref("example@coder.com"),
+ Roles: ptr.Ref([]string{"owner", "auditor"}),
+ LoginType: ptr.Ref("password"),
+ Password: ptr.Ref("SomeSecurePassword!"),
}
cfg2 := cfg1
- cfg2.Username = PtrTo("exampleNew")
+ cfg2.Username = ptr.Ref("exampleNew")
cfg3 := cfg2
- cfg3.Name = PtrTo("Example New")
+ cfg3.Name = ptr.Ref("Example New")
cfg4 := cfg3
- cfg4.LoginType = PtrTo("github")
+ cfg4.LoginType = ptr.Ref("github")
cfg4.Password = nil
resource.Test(t, resource.TestCase{
diff --git a/internal/provider/util.go b/internal/provider/util.go
index 12be3f3..169286f 100644
--- a/internal/provider/util.go
+++ b/internal/provider/util.go
@@ -8,22 +8,11 @@ import (
"net/http"
"os"
"path/filepath"
- "regexp"
"github.com/coder/coder/v2/codersdk"
"github.com/google/uuid"
)
-var (
- nameValidRegex = regexp.MustCompile("^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$")
- templateVersionNameRegex = regexp.MustCompile(`^[a-zA-Z0-9]+(?:[_.-]{1}[a-zA-Z0-9]+)*$`)
- displayNameRegex = regexp.MustCompile(`^[^\s](.*[^\s])?$`)
-)
-
-func PtrTo[T any](v T) *T {
- return &v
-}
-
func PrintOrNull(v any) string {
if v == nil {
return "null"
@@ -96,11 +85,11 @@ func computeDirectoryHash(directory string) (string, error) {
// memberDiff returns the members to add and remove from the group, given the current members and the planned members.
// plannedMembers is deliberately our custom type, as Terraform cannot automatically produce `[]uuid.UUID` from a set.
-func memberDiff(curMembers []uuid.UUID, plannedMembers []UUID) (add, remove []string) {
- curSet := make(map[uuid.UUID]struct{}, len(curMembers))
+func memberDiff(currentMembers []uuid.UUID, plannedMembers []UUID) (add, remove []string) {
+ curSet := make(map[uuid.UUID]struct{}, len(currentMembers))
planSet := make(map[uuid.UUID]struct{}, len(plannedMembers))
- for _, userID := range curMembers {
+ for _, userID := range currentMembers {
curSet[userID] = struct{}{}
}
for _, plannedUserID := range plannedMembers {
@@ -109,7 +98,7 @@ func memberDiff(curMembers []uuid.UUID, plannedMembers []UUID) (add, remove []st
add = append(add, plannedUserID.ValueString())
}
}
- for _, curUserID := range curMembers {
+ for _, curUserID := range currentMembers {
if _, exists := planSet[curUserID]; !exists {
remove = append(remove, curUserID.String())
}
diff --git a/internal/provider/uuid.go b/internal/provider/uuid.go
index 8cd8912..ac37b04 100644
--- a/internal/provider/uuid.go
+++ b/internal/provider/uuid.go
@@ -48,16 +48,16 @@ func (t uuidType) ValueFromString(ctx context.Context, in basetypes.StringValue)
return NewUUIDUnknown(), diags
}
- value, err := uuid.Parse(in.ValueString())
- if err != nil {
- // The framework doesn't want us to return validation errors here
- // for some reason. They get caught by `ValidateAttribute` instead,
- // and this function isn't called directly by our provider - UUIDValue
- // takes a valid UUID instead of a string.
- return NewUUIDUnknown(), diags
- }
-
- return UUIDValue(value), diags
+ // This function deliberately does not handle invalid UUIDs.
+ // Instead, `ValidateAttribute` will be called
+ // on the stored string during `validate` `plan` and `apply`,
+ // which will also create an error diagnostic.
+ // For that reason, storing the zero UUID is fine.
+ v, _ := uuid.Parse(in.ValueString())
+ return UUID{
+ StringValue: in,
+ value: v,
+ }, diags
}
// ValueFromTerraform implements basetypes.StringTypable.
diff --git a/internal/provider/uuid_internal_test.go b/internal/provider/uuid_internal_test.go
index 6283bb9..697d9c3 100644
--- a/internal/provider/uuid_internal_test.go
+++ b/internal/provider/uuid_internal_test.go
@@ -7,6 +7,7 @@ import (
"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-go/tftypes"
"github.com/stretchr/testify/require"
)
@@ -37,9 +38,12 @@ func TestUUIDTypeValueFromTerraform(t *testing.T) {
expected: UUIDValue(ValidUUID),
},
{
- name: "invalid UUID",
- input: tftypes.NewValue(tftypes.String, "invalid"),
- expected: NewUUIDUnknown(),
+ name: "invalid UUID",
+ input: tftypes.NewValue(tftypes.String, "invalid"),
+ expected: UUID{
+ StringValue: basetypes.NewStringValue("invalid"),
+ value: uuid.Nil,
+ },
},
}
diff --git a/internal/provider/workspace_proxy_resource_test.go b/internal/provider/workspace_proxy_resource_test.go
index a2447ea..88d7013 100644
--- a/internal/provider/workspace_proxy_resource_test.go
+++ b/internal/provider/workspace_proxy_resource_test.go
@@ -8,6 +8,7 @@ import (
"testing"
"text/template"
+ "github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/terraform-provider-coderd/integration"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/stretchr/testify/require"
@@ -23,14 +24,14 @@ func TestAccWorkspaceProxyResource(t *testing.T) {
cfg1 := testAccWorkspaceProxyResourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- Name: PtrTo("example"),
- DisplayName: PtrTo("Example WS Proxy"),
- Icon: PtrTo("/emojis/1f407.png"),
+ Name: ptr.Ref("example"),
+ DisplayName: ptr.Ref("Example WS Proxy"),
+ Icon: ptr.Ref("/emojis/1f407.png"),
}
cfg2 := cfg1
- cfg2.Name = PtrTo("example-new")
- cfg2.DisplayName = PtrTo("Example WS Proxy New")
+ cfg2.Name = ptr.Ref("example-new")
+ cfg2.DisplayName = ptr.Ref("Example WS Proxy New")
resource.Test(t, resource.TestCase{
IsUnitTest: true,
@@ -64,9 +65,9 @@ func TestAccWorkspaceProxyResourceAGPL(t *testing.T) {
cfg1 := testAccWorkspaceProxyResourceConfig{
URL: client.URL.String(),
Token: client.SessionToken(),
- Name: PtrTo("example"),
- DisplayName: PtrTo("Example WS Proxy"),
- Icon: PtrTo("/emojis/1f407.png"),
+ Name: ptr.Ref("example"),
+ DisplayName: ptr.Ref("Example WS Proxy"),
+ Icon: ptr.Ref("/emojis/1f407.png"),
}
resource.Test(t, resource.TestCase{