diff --git a/.github/workflows/cla.yaml b/.github/workflows/cla.yaml
deleted file mode 100644
index 342c983..0000000
--- a/.github/workflows/cla.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-# uncomment when configured correctly
-# name: "CLA Assistant"
-# on:
-# issue_comment:
-# types: [created]
-# pull_request_target:
-# types: [opened,closed,synchronize]
-
-# jobs:
-# CLAssistant:
-# runs-on: ubuntu-latest
-# steps:
-# - name: "CLA Assistant"
-# if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
-# uses: contributor-assistant/github-action@v2.4.0
-# env:
-# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-# # the below token should have repo scope and must be manually added by you in the repository's secret
-# PERSONAL_ACCESS_TOKEN : ${{ secrets.CDRCOMMUNITY_GITHUB_TOKEN }}
-# with:
-# remote-organization-name: 'coder'
-# remote-repository-name: 'cla'
-# path-to-signatures: 'v2022-09-04/signatures.json'
-# path-to-document: 'https://github.com/coder/cla/blob/main/README.md'
-# # branch should not be protected
-# branch: 'main'
-# allowlist: dependabot*
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 4e8900b..72e9c9f 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.1.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@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
+ with:
+ go-version: "1.22"
+ - name: Import GPG Key
+ id: import_gpg
+ uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0
+ with:
+ gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
+ - name: Run GoReleaser
+ uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.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..4e7a7ca 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -24,31 +24,42 @@ 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@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.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@2226d7cb06a077cd73e56eedd38eecad18e5d837 # v6.5.0
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@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
go-version-file: "go.mod"
cache: true
+
+ - uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
+
- 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 +83,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@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.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 +108,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@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.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..7efde44 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,7 +1,7 @@
-# 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-issues-per-linter: 0
max-same-issues: 0
linters:
@@ -9,19 +9,25 @@ linters:
enable:
- durationcheck
- errcheck
- - exportloopref
- forcetypeassert
- godot
- gofmt
- gosimple
+ - govet
- ineffassign
- makezero
- misspell
- nilerr
+ - paralleltest
- predeclared
- staticcheck
- - tenv
- unconvert
- unparam
- unused
- - vet
\ No newline at end of file
+ - usetesting
+
+linters-settings:
+ paralleltest:
+ # Terraform acceptance subtests all share a Coder instance, and cannot run
+ # in parallel.
+ ignore-missing-subtests: true
diff --git a/Makefile b/Makefile
index 54a7a12..faf8f49 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,12 @@
default: testacc
fmt:
+ go fmt ./...
terraform fmt -recursive
+lint:
+ golangci-lint run ./...
+
gen:
go generate ./...
@@ -11,7 +15,10 @@ build: terraform-provider-coderd
terraform-provider-coderd: internal/provider/*.go main.go
CGO_ENABLED=0 go build .
+test: testacc
+.PHONY: test
+
# Run acceptance tests
-.PHONY: testacc
testacc:
TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 120m
+.PHONY: testacc
diff --git a/README.md b/README.md
index 8eeafa0..e79f8eb 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ The provider currently supports resources and data sources for:
- Templates + Template Versions
- Groups
- Workspace Proxies
-- Organizations (Data Source only)
+- Organizations
## Requirements
diff --git a/docs/data-sources/group.md b/docs/data-sources/group.md
index 8a50e9e..f06c55b 100644
--- a/docs/data-sources/group.md
+++ b/docs/data-sources/group.md
@@ -58,7 +58,7 @@ resource "coderd_template" "example" {
- `display_name` (String)
- `members` (Attributes Set) Members of the group. (see [below for nested schema](#nestedatt--members))
- `quota_allowance` (Number) The number of quota credits to allocate to each user in the group.
-- `source` (String) The source of the group. Either 'oidc' or 'user'.
+- `source` (String) The source of the group. Either `oidc` or `user`.
### Nested Schema for `members`
@@ -69,7 +69,6 @@ Read-Only:
- `email` (String)
- `id` (String)
- `last_seen_at` (Number) Unix timestamp of when the member was last seen.
-- `login_type` (String) The login type of the member. Can be 'oidc', 'token', 'password', 'github' or 'none'.
-- `status` (String) The status of the member. Can be 'active', 'dormant' or 'suspended'.
-- `theme_preference` (String)
+- `login_type` (String) The login type of the member. Can be `oidc`, `token`, `password`, `github` or `none`.
+- `status` (String) The status of the member. Can be `active`, `dormant` or `suspended`.
- `username` (String)
diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md
index 366c16a..252db9e 100644
--- a/docs/data-sources/user.md
+++ b/docs/data-sources/user.md
@@ -48,9 +48,8 @@ resource "coderd_group" "bosses" {
- `created_at` (Number) Unix timestamp of when the user was created.
- `email` (String) Email of the user.
- `last_seen_at` (Number) Unix timestamp of when the user was last seen.
-- `login_type` (String) Type of login for the user. Valid types are 'none', 'password', 'github', and 'oidc'.
+- `login_type` (String) Type of login for the user. Valid types are `none`, `password', `github`, and `oidc`.
- `name` (String) Display name of the user.
- `organization_ids` (Set of String) IDs of organizations the user is associated with.
-- `roles` (Set of String) Roles assigned to the user. Valid roles are 'owner', 'template-admin', 'user-admin', and 'auditor'.
+- `roles` (Set of String) Roles assigned to the user. Valid roles are `owner`, `template-admin`, `user-admin`, and `auditor`.
- `suspended` (Boolean) Whether the user is suspended.
-- `theme_preference` (String) The user's preferred theme.
diff --git a/docs/index.md b/docs/index.md
index 3d9be49..79df017 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -27,7 +27,7 @@ terraform {
}
provider "coderd" {
- url = "coder.example.com"
+ url = "https://coder.example.com"
token = "****"
}
diff --git a/docs/resources/group.md b/docs/resources/group.md
index 2d265d9..735881e 100644
--- a/docs/resources/group.md
+++ b/docs/resources/group.md
@@ -5,7 +5,6 @@ subcategory: ""
description: |-
A group on the Coder deployment.
Creating groups requires an Enterprise license.
- When importing, the ID supplied can be either a group UUID retrieved via the API or /.
---
# coderd_group (Resource)
@@ -14,8 +13,6 @@ A group on the Coder deployment.
Creating groups requires an Enterprise license.
-When importing, the ID supplied can be either a group UUID retrieved via the API or `/`.
-
## Example Usage
```terraform
@@ -55,10 +52,28 @@ resource "coderd_group" "group1" {
- `avatar_url` (String) The URL of the group's avatar.
- `display_name` (String) The display name of the group. Defaults to the group name.
-- `members` (Set of String) Members of the group, by ID. If null, members will not be added or removed by Terraform. To have a group resource with unmanaged members, but be able to read the members in Terraform, use `data.coderd_group`
+- `members` (Set of String) Members of the group, by ID. If `null`, members will not be added or removed by Terraform. To have a group resource with unmanaged members, but be able to read the members in Terraform, use `data.coderd_group`
- `organization_id` (String) The organization ID that the group belongs to. Defaults to the provider default organization ID.
- `quota_allowance` (Number) The number of quota credits to allocate to each user in the group.
### Read-Only
- `id` (String) Group ID.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+# The ID supplied can be either a group UUID retrieved via the API
+# or a fully qualified name: `/`.
+$ terraform import coderd_group.example coder/developers
+```
+Alternatively, in Terraform v1.5.0 and later, an [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used:
+
+```terraform
+import {
+ to = coderd_group.example
+ id = "coder/developers"
+}
+```
diff --git a/docs/resources/license.md b/docs/resources/license.md
new file mode 100644
index 0000000..ec29706
--- /dev/null
+++ b/docs/resources/license.md
@@ -0,0 +1,41 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "coderd_license Resource - terraform-provider-coderd"
+subcategory: ""
+description: |-
+ A license for a Coder deployment.
+ 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.
+---
+
+# coderd_license (Resource)
+
+A license for a Coder deployment.
+
+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
+
+### Required
+
+- `license` (String, Sensitive) A license key for Coder.
+
+### Read-Only
+
+- `expires_at` (Number) Unix timestamp of when the license expires.
+- `id` (Number) Integer ID of the license.
diff --git a/docs/resources/organization.md b/docs/resources/organization.md
new file mode 100644
index 0000000..88246dd
--- /dev/null
+++ b/docs/resources/organization.md
@@ -0,0 +1,103 @@
+---
+# 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.
+ ~> Warning
+ This resource is only compatible with Coder version 2.16.0 https://github.com/coder/coder/releases/tag/v2.16.0 and later.
+---
+
+# coderd_organization (Resource)
+
+An organization on the Coder deployment.
+
+~> **Warning**
+This resource is only compatible with Coder version [2.16.0](https://github.com/coder/coder/releases/tag/v2.16.0) and later.
+
+## Example Usage
+
+```terraform
+resource "coderd_organization" "blueberry" {
+ name = "blueberry"
+ display_name = "Blueberry"
+ description = "The organization for blueberries"
+ icon = "/emojis/1fad0.png"
+
+ org_sync_idp_groups = [
+ "wibble",
+ "wobble",
+ ]
+
+ group_sync {
+ field = "coder_groups"
+ mapping = {
+ toast = [coderd_group.bread.id]
+ }
+ }
+
+ role_sync {
+ field = "coder_roles"
+ mapping = {
+ manager = ["organization-user-admin"]
+ }
+ }
+}
+```
+
+
+## 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) Group sync settings to sync groups from an IdP. (see [below for nested schema](#nestedblock--group_sync))
+- `icon` (String)
+- `org_sync_idp_groups` (Set of String) Claims from the IdP provider that will give users access to this organization.
+- `role_sync` (Block, Optional) Role sync settings to sync organization roles from an IdP. (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
+# The ID supplied can be either a organization UUID retrieved via the API
+# or the name of the organization.
+$ terraform import coderd_organization.our_org our-org
+```
+Alternatively, in Terraform v1.5.0 and later, an [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used:
+
+```terraform
+import {
+ to = coderd_organization.our_org
+ id = "our-org"
+}
+```
diff --git a/docs/resources/organization_sync_settings.md b/docs/resources/organization_sync_settings.md
new file mode 100644
index 0000000..efb6985
--- /dev/null
+++ b/docs/resources/organization_sync_settings.md
@@ -0,0 +1,47 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "coderd_organization_sync_settings Resource - terraform-provider-coderd"
+subcategory: ""
+description: |-
+ IdP sync settings for organizations.
+ This resource can only be created once. Attempts to create multiple will fail.
+ ~> Warning
+ This resource is only compatible with Coder version 2.19.0 https://github.com/coder/coder/releases/tag/v2.19.0 and later.
+---
+
+# coderd_organization_sync_settings (Resource)
+
+IdP sync settings for organizations.
+
+This resource can only be created once. Attempts to create multiple will fail.
+
+~> **Warning**
+This resource is only compatible with Coder version [2.19.0](https://github.com/coder/coder/releases/tag/v2.19.0) and later.
+
+## Example Usage
+
+```terraform
+// Important note: You can only have one resource of this type!
+resource "coderd_organization_sync_settings" "org_sync" {
+ field = "wibble"
+ assign_default = false
+
+ mapping = {
+ wobble = [
+ coderd_organization.my_organization.id,
+ ]
+ }
+}
+```
+
+
+## Schema
+
+### Required
+
+- `assign_default` (Boolean) When true, every user will be added to the default organization, regardless of claims.
+- `field` (String) The claim field that specifies what organizations a user should be in.
+
+### Optional
+
+- `mapping` (Map of List of String) A map from OIDC group name to Coder organization ID.
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/docs/resources/template.md b/docs/resources/template.md
index 92d9fc7..0b15d2f 100644
--- a/docs/resources/template.md
+++ b/docs/resources/template.md
@@ -4,17 +4,14 @@ page_title: "coderd_template Resource - terraform-provider-coderd"
subcategory: ""
description: |-
A Coder template.
- Logs from building template versions are streamed from the provisioner when the TF_LOG environment variable is INFO or higher.
- When importing, the ID supplied can be either a template UUID retrieved via the API or /.
+ Logs from building template versions can be optionally streamed from the provisioner by setting the TF_LOG environment variable to INFO or higher.
---
# coderd_template (Resource)
A Coder template.
-Logs from building template versions are streamed from the provisioner when the `TF_LOG` environment variable is `INFO` or higher.
-
-When importing, the ID supplied can be either a template UUID retrieved via the API or `/`.
+Logs from building template versions can be optionally streamed from the provisioner by setting the `TF_LOG` environment variable to `INFO` or higher.
## Example Usage
@@ -22,8 +19,8 @@ When importing, the ID supplied can be either a template UUID retrieved via the
// Provider populated from environment variables
provider "coderd" {}
-// Get the commit SHA of the configuration's git repository
-variable "TFC_CONFIGURATION_VERSION_GIT_COMMIT_SHA" {
+// Can be populated using an environment variable, or an external datasource script
+variable "COMMIT_SHA" {
type = string
}
@@ -38,12 +35,12 @@ resource "coderd_template" "ubuntu-main" {
description = "The main template for developing on Ubuntu."
versions = [
{
- name = "stable-${var.TFC_CONFIGURATION_VERSION_GIT_COMMIT_SHA}"
+ name = "stable-${var.COMMIT_SHA}"
description = "The stable version of the template."
directory = "./stable-template"
},
{
- name = "staging-${var.TFC_CONFIGURATION_VERSION_GIT_COMMIT_SHA}"
+ name = "staging-${var.COMMIT_SHA}"
description = "The staging version of the template."
directory = "./staging-template"
}
@@ -71,7 +68,7 @@ resource "coderd_template" "ubuntu-main" {
- `acl` (Attributes) (Enterprise) Access control list for the template. If null, ACL policies will not be added, removed, or read by Terraform. (see [below for nested schema](#nestedatt--acl))
- `activity_bump_ms` (Number) The activity bump duration for all workspaces created from this template, in milliseconds. Defaults to one hour.
- `allow_user_auto_start` (Boolean) (Enterprise) Whether users can auto-start workspaces created from this template. Defaults to true.
-- `allow_user_auto_stop` (Boolean) (Enterprise) Whether users can auto-start workspaces created from this template. Defaults to true.
+- `allow_user_auto_stop` (Boolean) (Enterprise) Whether users can auto-stop workspaces created from this template. Defaults to true.
- `allow_user_cancel_workspace_jobs` (Boolean) Whether users can cancel in-progress workspace jobs using this template. Defaults to true.
- `auto_start_permitted_days_of_week` (Set of String) (Enterprise) List of days of the week in which autostart is allowed to happen, for all workspaces created from this template. Defaults to all days. If no days are specified, autostart is not allowed.
- `auto_stop_requirement` (Attributes) (Enterprise) The auto-stop requirement for all workspaces created from this template. (see [below for nested schema](#nestedatt--auto_stop_requirement))
@@ -80,7 +77,8 @@ resource "coderd_template" "ubuntu-main" {
- `description` (String) A description of the template.
- `display_name` (String) The display name of the template. Defaults to the template name.
- `failure_ttl_ms` (Number) (Enterprise) The max lifetime before Coder stops all resources for failed workspaces created from this template, in milliseconds.
-- `icon` (String) Relative path or external URL that specifes an icon to be displayed in the dashboard.
+- `icon` (String) Relative path or external URL that specifies an icon to be displayed in the dashboard.
+- `max_port_share_level` (String) (Enterprise) The maximum port share level for workspaces created from this template. Defaults to `owner` on an Enterprise deployment, or `public` otherwise.
- `organization_id` (String) The ID of the organization. Defaults to the provider's default organization
- `require_active_version` (Boolean) (Enterprise) Whether workspaces must be created from the active version of this template. Defaults to false.
- `time_til_dormant_autodelete_ms` (Number) (Enterprise) The max lifetime before Coder permanently deletes dormant workspaces created from this template.
@@ -101,7 +99,7 @@ Optional:
- `active` (Boolean) Whether this version is the active version of the template. Only one version can be active at a time.
- `message` (String) A message describing the changes in this version of the template. Messages longer than 72 characters will be truncated.
-- `name` (String) The name of the template version. Automatically generated if not provided. If provided, the name *must* change each time the directory contents are updated.
+- `name` (String) The name of the template version. Automatically generated if not provided. If provided, the name *must* change each time the directory contents, or the `tf_vars` attribute are updated.
- `provisioner_tags` (Attributes Set) Provisioner tags for the template version. (see [below for nested schema](#nestedatt--versions--provisioner_tags))
- `tf_vars` (Attributes Set) Terraform variables for the template version. (see [below for nested schema](#nestedatt--versions--tf_vars))
@@ -163,3 +161,25 @@ Optional:
- `days_of_week` (Set of String) List of days of the week on which restarts are required. Restarts happen within the user's quiet hours (in their configured timezone). If no days are specified, restarts are not required.
- `weeks` (Number) Weeks is the number of weeks between required restarts. Weeks are synced across all workspaces (and Coder deployments) using modulo math on a hardcoded epoch week of January 2nd, 2023 (the first Monday of 2023). Values of 0 or 1 indicate weekly restarts. Values of 2 indicate fortnightly restarts, etc.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+# The ID supplied can be either a template UUID retrieved via the API
+# or a fully qualified name: `/`.
+$ terraform import coderd_template.example coder/dogfood
+```
+Once imported, you'll need to manually declare in your config:
+- The `versions` list, in order to specify the source directories for new versions of the template.
+- (Enterprise) The `acl` attribute, in order to specify the users and groups that have access to the template.
+
+Alternatively, in Terraform v1.5.0 and later, an [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used:
+
+```terraform
+import {
+ to = coderd_template.example
+ id = "coder/dogfood"
+}
+```
diff --git a/docs/resources/user.md b/docs/resources/user.md
index 1671fa6..4f95850 100644
--- a/docs/resources/user.md
+++ b/docs/resources/user.md
@@ -4,15 +4,12 @@ page_title: "coderd_user Resource - terraform-provider-coderd"
subcategory: ""
description: |-
A user on the Coder deployment.
- When importing, the ID supplied can be either a user UUID or a username.
---
# coderd_user (Resource)
A user on the Coder deployment.
-When importing, the ID supplied can be either a user UUID or a username.
-
## Example Usage
```terraform
@@ -56,12 +53,30 @@ resource "coderd_user" "admin" {
### Optional
-- `login_type` (String) Type of login for the user. Valid types are 'none', 'password', 'github', and 'oidc'.
+- `login_type` (String) Type of login for the user. Valid types are `none`, `password`, `github`, and `oidc`.
- `name` (String) Display name of the user. Defaults to username.
-- `password` (String, Sensitive) Password for the user. Required when login_type is 'password'. Passwords are saved into the state as plain text and should only be used for testing purposes.
-- `roles` (Set of String) Roles assigned to the user. Valid roles are 'owner', 'template-admin', 'user-admin', and 'auditor'.
+- `password` (String, Sensitive) Password for the user. Required when `login_type` is `password`. Passwords are saved into the state as plain text and should only be used for testing purposes.
+- `roles` (Set of String) Roles assigned to the user. Valid roles are `owner`, `template-admin`, `user-admin`, and `auditor`.
- `suspended` (Boolean) Whether the user is suspended.
### Read-Only
- `id` (String) User ID
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+# The ID supplied can be either a user UUID retrieved via the API
+# or a username.
+$ terraform import coderd_user.example developer
+```
+Alternatively, in Terraform v1.5.0 and later, an [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used:
+
+```terraform
+import {
+ to = coderd_user.example
+ id = "developer"
+}
+```
diff --git a/docs/resources/workspace_proxy.md b/docs/resources/workspace_proxy.md
index ad77a82..72262ff 100644
--- a/docs/resources/workspace_proxy.md
+++ b/docs/resources/workspace_proxy.md
@@ -48,7 +48,7 @@ resource "kubernetes_deployment" "syd_wsproxy" {
### Required
-- `icon` (String) Relative path or external URL that specifes an icon to be displayed in the dashboard.
+- `icon` (String) Relative path or external URL that specifies an icon to be displayed in the dashboard.
- `name` (String) Name of the workspace proxy.
### Optional
diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf
index fe3b9dc..d50d5e7 100644
--- a/examples/provider/provider.tf
+++ b/examples/provider/provider.tf
@@ -7,7 +7,7 @@ terraform {
}
provider "coderd" {
- url = "coder.example.com"
+ url = "https://coder.example.com"
token = "****"
}
diff --git a/examples/resources/coderd_group/import.sh b/examples/resources/coderd_group/import.sh
new file mode 100644
index 0000000..a49cac3
--- /dev/null
+++ b/examples/resources/coderd_group/import.sh
@@ -0,0 +1,11 @@
+# The ID supplied can be either a group UUID retrieved via the API
+# or a fully qualified name: `/`.
+$ terraform import coderd_group.example coder/developers
+```
+Alternatively, in Terraform v1.5.0 and later, an [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used:
+
+```terraform
+import {
+ to = coderd_group.example
+ id = "coder/developers"
+}
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..45bb87a
--- /dev/null
+++ b/examples/resources/coderd_organization/import.sh
@@ -0,0 +1,11 @@
+# The ID supplied can be either a organization UUID retrieved via the API
+# or the name of the organization.
+$ terraform import coderd_organization.our_org our-org
+```
+Alternatively, in Terraform v1.5.0 and later, an [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used:
+
+```terraform
+import {
+ to = coderd_organization.our_org
+ id = "our-org"
+}
diff --git a/examples/resources/coderd_organization/resource.tf b/examples/resources/coderd_organization/resource.tf
new file mode 100644
index 0000000..cb26a86
--- /dev/null
+++ b/examples/resources/coderd_organization/resource.tf
@@ -0,0 +1,25 @@
+resource "coderd_organization" "blueberry" {
+ name = "blueberry"
+ display_name = "Blueberry"
+ description = "The organization for blueberries"
+ icon = "/emojis/1fad0.png"
+
+ org_sync_idp_groups = [
+ "wibble",
+ "wobble",
+ ]
+
+ group_sync {
+ field = "coder_groups"
+ mapping = {
+ toast = [coderd_group.bread.id]
+ }
+ }
+
+ role_sync {
+ field = "coder_roles"
+ mapping = {
+ manager = ["organization-user-admin"]
+ }
+ }
+}
diff --git a/examples/resources/coderd_organization_sync_settings/resource.tf b/examples/resources/coderd_organization_sync_settings/resource.tf
new file mode 100644
index 0000000..f1da134
--- /dev/null
+++ b/examples/resources/coderd_organization_sync_settings/resource.tf
@@ -0,0 +1,11 @@
+// Important note: You can only have one resource of this type!
+resource "coderd_organization_sync_settings" "org_sync" {
+ field = "wibble"
+ assign_default = false
+
+ mapping = {
+ wobble = [
+ coderd_organization.my_organization.id,
+ ]
+ }
+}
diff --git a/examples/resources/coderd_template/import.sh b/examples/resources/coderd_template/import.sh
new file mode 100644
index 0000000..91892f6
--- /dev/null
+++ b/examples/resources/coderd_template/import.sh
@@ -0,0 +1,15 @@
+# The ID supplied can be either a template UUID retrieved via the API
+# or a fully qualified name: `/`.
+$ terraform import coderd_template.example coder/dogfood
+```
+Once imported, you'll need to manually declare in your config:
+- The `versions` list, in order to specify the source directories for new versions of the template.
+- (Enterprise) The `acl` attribute, in order to specify the users and groups that have access to the template.
+
+Alternatively, in Terraform v1.5.0 and later, an [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used:
+
+```terraform
+import {
+ to = coderd_template.example
+ id = "coder/dogfood"
+}
diff --git a/examples/resources/coderd_template/resource.tf b/examples/resources/coderd_template/resource.tf
index 3cbc8fe..e644d6e 100644
--- a/examples/resources/coderd_template/resource.tf
+++ b/examples/resources/coderd_template/resource.tf
@@ -1,8 +1,8 @@
// Provider populated from environment variables
provider "coderd" {}
-// Get the commit SHA of the configuration's git repository
-variable "TFC_CONFIGURATION_VERSION_GIT_COMMIT_SHA" {
+// Can be populated using an environment variable, or an external datasource script
+variable "COMMIT_SHA" {
type = string
}
@@ -17,12 +17,12 @@ resource "coderd_template" "ubuntu-main" {
description = "The main template for developing on Ubuntu."
versions = [
{
- name = "stable-${var.TFC_CONFIGURATION_VERSION_GIT_COMMIT_SHA}"
+ name = "stable-${var.COMMIT_SHA}"
description = "The stable version of the template."
directory = "./stable-template"
},
{
- name = "staging-${var.TFC_CONFIGURATION_VERSION_GIT_COMMIT_SHA}"
+ name = "staging-${var.COMMIT_SHA}"
description = "The staging version of the template."
directory = "./staging-template"
}
diff --git a/examples/resources/coderd_user/import.sh b/examples/resources/coderd_user/import.sh
new file mode 100644
index 0000000..1257bab
--- /dev/null
+++ b/examples/resources/coderd_user/import.sh
@@ -0,0 +1,11 @@
+# The ID supplied can be either a user UUID retrieved via the API
+# or a username.
+$ terraform import coderd_user.example developer
+```
+Alternatively, in Terraform v1.5.0 and later, an [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used:
+
+```terraform
+import {
+ to = coderd_user.example
+ id = "developer"
+}
diff --git a/go.mod b/go.mod
index eebc6ee..897c9e0 100644
--- a/go.mod
+++ b/go.mod
@@ -1,170 +1,202 @@
module github.com/coder/terraform-provider-coderd
-go 1.22.5
+go 1.24.1
require (
- cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6
- github.com/coder/coder/v2 v2.14.2
- github.com/docker/docker v27.1.2+incompatible
- github.com/docker/go-connections v0.4.0
+ cdr.dev/slog v1.6.2-0.20241112041820-0ec81e6e67bb
+ github.com/coder/coder/v2 v2.21.0
+ github.com/docker/docker v28.1.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-framework v1.11.0
- github.com/hashicorp/terraform-plugin-framework-validators v0.13.0
- github.com/hashicorp/terraform-plugin-go v0.23.0
+ github.com/hashicorp/terraform-plugin-docs v0.21.0
+ github.com/hashicorp/terraform-plugin-framework v1.14.1
+ github.com/hashicorp/terraform-plugin-framework-validators v0.17.0
+ github.com/hashicorp/terraform-plugin-go v0.26.0
github.com/hashicorp/terraform-plugin-log v0.9.0
- github.com/hashicorp/terraform-plugin-testing v1.10.0
- github.com/otiai10/copy v1.14.0
- github.com/stretchr/testify v1.9.0
+ github.com/hashicorp/terraform-plugin-testing v1.12.0
+ github.com/otiai10/copy v1.14.1
+ github.com/stretchr/testify v1.10.0
)
require (
github.com/BurntSushi/toml v1.2.1 // indirect
- github.com/DataDog/appsec-internal-go v1.5.0 // indirect
- github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 // indirect
- github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 // indirect
- github.com/DataDog/datadog-go/v5 v5.3.0 // indirect
- github.com/DataDog/go-libddwaf/v2 v2.4.2 // indirect
- github.com/DataDog/go-tuf v1.0.2-0.5.2 // indirect
+ github.com/DataDog/appsec-internal-go v1.9.0 // indirect
+ github.com/DataDog/datadog-agent/pkg/obfuscate v0.58.0 // indirect
+ github.com/DataDog/datadog-agent/pkg/proto v0.58.0 // indirect
+ github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.58.0 // indirect
+ github.com/DataDog/datadog-agent/pkg/trace v0.58.0 // indirect
+ github.com/DataDog/datadog-agent/pkg/util/log v0.58.0 // indirect
+ github.com/DataDog/datadog-agent/pkg/util/scrubber v0.58.0 // indirect
+ github.com/DataDog/datadog-go/v5 v5.5.0 // indirect
+ github.com/DataDog/go-libddwaf/v3 v3.5.1 // indirect
+ github.com/DataDog/go-runtime-metrics-internal v0.0.4-0.20241206090539-a14610dc22b6 // indirect
+ github.com/DataDog/go-sqllexer v0.0.14 // indirect
+ github.com/DataDog/go-tuf v1.1.0-0.5.2 // indirect
github.com/DataDog/gostackparse v0.7.0 // indirect
- github.com/DataDog/sketches-go v1.4.2 // indirect
+ github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.20.0 // indirect
+ github.com/DataDog/sketches-go v1.4.5 // indirect
github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
- github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect
+ github.com/ProtonMail/go-crypto v1.1.3 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c // indirect
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.8.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 // indirect
- github.com/coder/serpent v0.7.0 // indirect
- github.com/coder/terraform-provider-coder v0.23.0 // indirect
- github.com/containerd/log v0.1.0 // indirect
- github.com/coreos/go-oidc/v3 v3.11.0 // indirect
+ github.com/coder/serpent v0.10.0 // indirect
+ github.com/coder/terraform-provider-coder/v2 v2.1.3 // indirect
+ github.com/coder/websocket v1.8.12 // indirect
+ github.com/coreos/go-oidc/v3 v3.13.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
- github.com/ebitengine/purego v0.6.0-alpha.5 // indirect
- github.com/fatih/color v1.17.0 // indirect
+ github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 // indirect
+ github.com/ebitengine/purego v0.8.2 // indirect
+ github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-chi/chi/v5 v5.1.0 // indirect
- github.com/go-jose/go-jose/v4 v4.0.2 // indirect
+ github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-ole/go-ole v1.2.6 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
- github.com/google/go-cmp v0.6.0 // indirect
+ github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b // indirect
- github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
- github.com/hashicorp/cli v1.1.6 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect
+ github.com/hashicorp/cli v1.1.7 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
- github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect
+ github.com/hashicorp/go-cty v1.5.0 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
- github.com/hashicorp/go-plugin v1.6.0 // indirect
+ github.com/hashicorp/go-plugin v1.6.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
+ github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // indirect
+ github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
+ github.com/hashicorp/go-sockaddr v1.0.2 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
- github.com/hashicorp/hc-install v0.8.0 // indirect
- github.com/hashicorp/hcl/v2 v2.21.0 // indirect
+ github.com/hashicorp/hc-install v0.9.1 // 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-registry-address v0.2.3 // indirect
+ github.com/hashicorp/terraform-exec v0.22.0 // indirect
+ github.com/hashicorp/terraform-json v0.24.0 // indirect
+ github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 // indirect
+ github.com/hashicorp/terraform-registry-address v0.2.4 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
- github.com/hashicorp/yamux v0.1.1 // indirect
+ github.com/hashicorp/yamux v0.1.2 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
- github.com/imdario/mergo v0.3.15 // indirect
- github.com/klauspost/compress v1.17.9 // indirect
+ github.com/imdario/mergo v0.3.16 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
- github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
+ github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
- github.com/mattn/go-runewidth v0.0.15 // indirect
+ github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
- github.com/morikuni/aec v1.0.0 // indirect
- github.com/muesli/termenv v0.15.2 // indirect
- github.com/oklog/run v1.0.0 // indirect
+ github.com/moby/moby v28.0.0+incompatible // indirect
+ github.com/moby/sys/atomicwriter v0.1.0 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/muesli/termenv v0.16.0 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/oklog/run v1.1.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
+ github.com/otiai10/mint v1.6.3 // indirect
github.com/outcaste-io/ristretto v0.2.3 // indirect
- github.com/philhofer/fwd v1.1.2 // indirect
- github.com/pion/transport/v2 v2.0.0 // indirect
+ github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect
+ github.com/pion/transport/v2 v2.2.10 // indirect
github.com/pion/udp v0.1.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/posener/complete v1.2.3 // indirect
- github.com/prometheus/client_golang v1.19.1 // indirect
+ github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
+ github.com/prometheus/client_golang v1.21.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
- github.com/prometheus/common v0.48.0 // indirect
- github.com/prometheus/procfs v0.12.0 // indirect
- github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 // indirect
- github.com/rivo/uniseg v0.4.4 // indirect
+ github.com/prometheus/common v0.62.0 // indirect
+ github.com/prometheus/procfs v0.15.1 // indirect
+ github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
+ github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.7.0 // indirect
+ github.com/shirou/gopsutil/v3 v3.24.4 // indirect
+ github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
- github.com/spf13/afero v1.11.0 // indirect
- github.com/spf13/cast v1.6.0 // indirect
+ github.com/spf13/afero v1.14.0 // indirect
+ github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
- github.com/tinylib/msgp v1.1.8 // indirect
- github.com/valyala/fasthttp v1.55.0 // indirect
+ github.com/tinylib/msgp v1.2.1 // indirect
+ github.com/tklauser/go-sysconf v0.3.12 // indirect
+ github.com/tklauser/numcpus v0.6.1 // indirect
+ github.com/valyala/fasthttp v1.59.0 // indirect
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.8 // indirect
github.com/yuin/goldmark-meta v1.1.0 // indirect
- github.com/zclconf/go-cty v1.15.0 // indirect
+ github.com/yusufpapurcu/wmi v1.2.4 // indirect
+ github.com/zclconf/go-cty v1.16.2 // indirect
github.com/zeebo/errs v1.3.0 // indirect
go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect
- go.nhat.io/otelsql v0.13.0 // indirect
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
- go.opentelemetry.io/otel v1.28.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 // indirect
- go.opentelemetry.io/otel/metric v1.28.0 // indirect
- go.opentelemetry.io/otel/sdk v1.28.0 // indirect
- go.opentelemetry.io/otel/trace v1.28.0 // indirect
- go.opentelemetry.io/proto/otlp v1.3.1 // indirect
+ go.nhat.io/otelsql v0.15.0 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/collector/component v0.104.0 // indirect
+ go.opentelemetry.io/collector/config/configtelemetry v0.104.0 // indirect
+ go.opentelemetry.io/collector/pdata v1.11.0 // indirect
+ go.opentelemetry.io/collector/pdata/pprofile v0.104.0 // indirect
+ go.opentelemetry.io/collector/semconv v0.104.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
+ go.opentelemetry.io/otel v1.34.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect
+ go.opentelemetry.io/otel/metric v1.34.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.34.0 // indirect
+ go.opentelemetry.io/otel/trace v1.34.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
- golang.org/x/crypto v0.26.0 // indirect
- golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
- golang.org/x/mod v0.19.0 // indirect
- golang.org/x/net v0.27.0 // indirect
- golang.org/x/oauth2 v0.21.0 // indirect
- golang.org/x/sync v0.8.0 // indirect
- golang.org/x/sys v0.23.0 // indirect
- golang.org/x/term v0.23.0 // indirect
- golang.org/x/text v0.17.0 // indirect
- golang.org/x/time v0.5.0 // indirect
- golang.org/x/tools v0.23.0 // indirect
- golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ go.uber.org/zap v1.27.0 // indirect
+ golang.org/x/crypto v0.36.0 // indirect
+ golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
+ golang.org/x/mod v0.24.0 // indirect
+ golang.org/x/net v0.38.0 // indirect
+ golang.org/x/oauth2 v0.28.0 // indirect
+ golang.org/x/sync v0.12.0 // indirect
+ golang.org/x/sys v0.31.0 // indirect
+ golang.org/x/term v0.30.0 // indirect
+ golang.org/x/text v0.23.0 // indirect
+ golang.org/x/time v0.11.0 // indirect
+ golang.org/x/tools v0.31.0 // indirect
+ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/appengine v1.6.8 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect
- google.golang.org/grpc v1.65.0 // indirect
- google.golang.org/protobuf v1.34.2 // indirect
- gopkg.in/DataDog/dd-trace-go.v1 v1.64.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
+ google.golang.org/grpc v1.71.0 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
+ gopkg.in/DataDog/dd-trace-go.v1 v1.72.1 // indirect
+ gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
- gotest.tools/v3 v3.5.1 // indirect
- nhooyr.io/websocket v1.8.7 // indirect
storj.io/drpc v0.0.33 // indirect
)
diff --git a/go.sum b/go.sum
index b7bc394..781e76f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,13 +1,13 @@
-cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6 h1:KHblWIE/KHOwQ6lEbMZt6YpcGve2FEZ1sDtrW1Am5UI=
-cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6/go.mod h1:NaoTA7KwopCrnaSb0JXTC0PTp/O/Y83Lndnq0OEV3ZQ=
-cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y=
-cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
-cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
-cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
-cloud.google.com/go/logging v1.11.0 h1:v3ktVzXMV7CwHq1MBF65wcqLMA7i+z3YxbUsoK7mOKs=
-cloud.google.com/go/logging v1.11.0/go.mod h1:5LDiJC/RxTt+fHc1LAt20R9TKiUTReDg6RuuFOZ67+A=
-cloud.google.com/go/longrunning v0.5.11 h1:Havn1kGjz3whCfoD8dxMLP73Ph5w+ODyZB9RUsDxtGk=
-cloud.google.com/go/longrunning v0.5.11/go.mod h1:rDn7//lmlfWV1Dx6IB4RatCPenTwwmqXuiP0/RgoEO4=
+cdr.dev/slog v1.6.2-0.20241112041820-0ec81e6e67bb h1:4MKA8lBQLnCqj2myJCb5Lzoa65y0tABO4gHrxuMdsCQ=
+cdr.dev/slog v1.6.2-0.20241112041820-0ec81e6e67bb/go.mod h1:NaoTA7KwopCrnaSb0JXTC0PTp/O/Y83Lndnq0OEV3ZQ=
+cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
+cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
+cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
+cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
+cloud.google.com/go/logging v1.12.0 h1:ex1igYcGFd4S/RZWOCU51StlIEuey5bjqwH9ZYjHibk=
+cloud.google.com/go/logging v1.12.0/go.mod h1:wwYBt5HlYP1InnrtYI0wtwttpVU1rifnMT7RejksUAM=
+cloud.google.com/go/longrunning v0.6.2 h1:xjDfh1pQcWPEvnfjZmwjKQEcHnpz6lHjfy7Fo0MK+hc=
+cloud.google.com/go/longrunning v0.6.2/go.mod h1:k/vIs83RN4bE3YCswdXC5PFfWVILjm3hpEUlSko4PiI=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
@@ -16,22 +16,36 @@ github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
-github.com/DataDog/appsec-internal-go v1.5.0 h1:8kS5zSx5T49uZ8dZTdT19QVAvC/B8ByyZdhQKYQWHno=
-github.com/DataDog/appsec-internal-go v1.5.0/go.mod h1:pEp8gjfNLtEOmz+iZqC8bXhu0h4k7NUsW/qiQb34k1U=
-github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8=
-github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0/go.mod h1:HzySONXnAgSmIQfL6gOv9hWprKJkx8CicuXuUbmgWfo=
-github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 h1:5nE6N3JSs2IG3xzMthNFhXfOaXlrsdgqmJ73lndFf8c=
-github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1/go.mod h1:Vc+snp0Bey4MrrJyiV2tVxxJb6BmLomPvN1RgAvjGaQ=
-github.com/DataDog/datadog-go/v5 v5.3.0 h1:2q2qjFOb3RwAZNU+ez27ZVDwErJv5/VpbBPprz7Z+s8=
-github.com/DataDog/datadog-go/v5 v5.3.0/go.mod h1:XRDJk1pTc00gm+ZDiBKsjh7oOOtJfYfglVCmFb8C2+Q=
-github.com/DataDog/go-libddwaf/v2 v2.4.2 h1:ilquGKUmN9/Ty0sIxiEyznVRxP3hKfmH15Y1SMq5gjA=
-github.com/DataDog/go-libddwaf/v2 v2.4.2/go.mod h1:gsCdoijYQfj8ce/T2bEDNPZFIYnmHluAgVDpuQOWMZE=
-github.com/DataDog/go-tuf v1.0.2-0.5.2 h1:EeZr937eKAWPxJ26IykAdWA4A0jQXJgkhUjqEI/w7+I=
-github.com/DataDog/go-tuf v1.0.2-0.5.2/go.mod h1:zBcq6f654iVqmkk8n2Cx81E1JnNTMOAx1UEO/wZR+P0=
+github.com/DataDog/appsec-internal-go v1.9.0 h1:cGOneFsg0JTRzWl5U2+og5dbtyW3N8XaYwc5nXe39Vw=
+github.com/DataDog/appsec-internal-go v1.9.0/go.mod h1:wW0cRfWBo4C044jHGwYiyh5moQV2x0AhnwqMuiX7O/g=
+github.com/DataDog/datadog-agent/pkg/obfuscate v0.58.0 h1:nOrRNCHyriM/EjptMrttFOQhRSmvfagESdpyknb5VPg=
+github.com/DataDog/datadog-agent/pkg/obfuscate v0.58.0/go.mod h1:MfDvphBMmEMwE3a30h27AtPO7OzmvdoVTiGY1alEmo4=
+github.com/DataDog/datadog-agent/pkg/proto v0.58.0 h1:JX2Q0C5QnKcYqnYHWUcP0z7R0WB8iiQz3aWn+kT5DEc=
+github.com/DataDog/datadog-agent/pkg/proto v0.58.0/go.mod h1:0wLYojGxRZZFQ+SBbFjay9Igg0zbP88l03TfZaVZ6Dc=
+github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.58.0 h1:5hGO0Z8ih0bRojuq+1ZwLFtdgsfO3TqIjbwJAH12sOQ=
+github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.58.0/go.mod h1:jN5BsZI+VilHJV1Wac/efGxS4TPtXa1Lh9SiUyv93F4=
+github.com/DataDog/datadog-agent/pkg/trace v0.58.0 h1:4AjohoBWWN0nNaeD/0SDZ8lRTYmnJ48CqREevUfSets=
+github.com/DataDog/datadog-agent/pkg/trace v0.58.0/go.mod h1:MFnhDW22V5M78MxR7nv7abWaGc/B4L42uHH1KcIKxZs=
+github.com/DataDog/datadog-agent/pkg/util/log v0.58.0 h1:2MENBnHNw2Vx/ebKRyOPMqvzWOUps2Ol2o/j8uMvN4U=
+github.com/DataDog/datadog-agent/pkg/util/log v0.58.0/go.mod h1:1KdlfcwhqtYHS1szAunsgSfvgoiVsf3mAJc+WvNTnIE=
+github.com/DataDog/datadog-agent/pkg/util/scrubber v0.58.0 h1:Jkf91q3tuIer4Hv9CLJIYjlmcelAsoJRMmkHyz+p1Dc=
+github.com/DataDog/datadog-agent/pkg/util/scrubber v0.58.0/go.mod h1:krOxbYZc4KKE7bdEDu10lLSQBjdeSFS/XDSclsaSf1Y=
+github.com/DataDog/datadog-go/v5 v5.5.0 h1:G5KHeB8pWBNXT4Jtw0zAkhdxEAWSpWH00geHI6LDrKU=
+github.com/DataDog/datadog-go/v5 v5.5.0/go.mod h1:K9kcYBlxkcPP8tvvjZZKs/m1edNAUFzBbdpTUKfCsuw=
+github.com/DataDog/go-libddwaf/v3 v3.5.1 h1:GWA4ln4DlLxiXm+X7HA/oj0ZLcdCwOS81KQitegRTyY=
+github.com/DataDog/go-libddwaf/v3 v3.5.1/go.mod h1:n98d9nZ1gzenRSk53wz8l6d34ikxS+hs62A31Fqmyi4=
+github.com/DataDog/go-runtime-metrics-internal v0.0.4-0.20241206090539-a14610dc22b6 h1:bpitH5JbjBhfcTG+H2RkkiUXpYa8xSuIPnyNtTaSPog=
+github.com/DataDog/go-runtime-metrics-internal v0.0.4-0.20241206090539-a14610dc22b6/go.mod h1:quaQJ+wPN41xEC458FCpTwyROZm3MzmTZ8q8XOXQiPs=
+github.com/DataDog/go-sqllexer v0.0.14 h1:xUQh2tLr/95LGxDzLmttLgTo/1gzFeOyuwrQa/Iig4Q=
+github.com/DataDog/go-sqllexer v0.0.14/go.mod h1:KwkYhpFEVIq+BfobkTC1vfqm4gTi65skV/DpDBXtexc=
+github.com/DataDog/go-tuf v1.1.0-0.5.2 h1:4CagiIekonLSfL8GMHRHcHudo1fQnxELS9g4tiAupQ4=
+github.com/DataDog/go-tuf v1.1.0-0.5.2/go.mod h1:zBcq6f654iVqmkk8n2Cx81E1JnNTMOAx1UEO/wZR+P0=
github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4=
github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM=
-github.com/DataDog/sketches-go v1.4.2 h1:gppNudE9d19cQ98RYABOetxIhpTCl4m7CnbRZjvVA/o=
-github.com/DataDog/sketches-go v1.4.2/go.mod h1:xJIXldczJyyjnbDop7ZZcLxJdV3+7Kra7H1KMgpgkLk=
+github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.20.0 h1:fKv05WFWHCXQmUTehW1eEZvXJP65Qv00W4V01B1EqSA=
+github.com/DataDog/opentelemetry-mapping-go/pkg/otlp/attributes v0.20.0/go.mod h1:dvIWN9pA2zWNTw5rhDWZgzZnhcfpH++d+8d1SWW6xkY=
+github.com/DataDog/sketches-go v1.4.5 h1:ki7VfeNz7IcNafq7yI/j5U/YCkO3LJiMDtXz9OMQbyE=
+github.com/DataDog/sketches-go v1.4.5/go.mod h1:7Y8GN8Jf66DLyDhc94zuWA3uHEt/7ttt8jHOBWWrSOg=
github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0=
github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
@@ -43,150 +57,136 @@ github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBa
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
-github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg=
-github.com/ProtonMail/go-crypto v1.1.0-alpha.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
+github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk=
+github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
-github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
-github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
+github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M=
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+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.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
+github.com/bmatcuk/doublestar/v4 v4.8.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=
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
-github.com/bytedance/sonic v1.10.0 h1:qtNZduETEIWJVIyDl01BeNxur2rW9OwTQ/yBqFRkKEk=
-github.com/bytedance/sonic v1.10.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/charmbracelet/lipgloss v0.8.0 h1:IS00fk4XAHcf8uZKc3eHeMUTCxUH6NkaTrdyCQk84RU=
-github.com/charmbracelet/lipgloss v0.8.0/go.mod h1:p4eYUZZJ/0oXTuCQKFF8mqyKCz0ja6y+7DniDDw5KKU=
-github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
-github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
-github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
-github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
+github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
+github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
+github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
+github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
+github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
+github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
+github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
+github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
+github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 h1:kHaBemcxl8o/pQ5VM1c8PVE1PubbNx3mjUr09OqWGCs=
+github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
-github.com/coder/coder/v2 v2.14.2 h1:RNNDDwjNK5D1XMQlK7LWrS4niVclkl1FXoaOaW7N5rs=
-github.com/coder/coder/v2 v2.14.2/go.mod h1:dO79BI5XlP8rrtne1JpRcVehe27bNMXdZKyn1NsWbjA=
+github.com/coder/coder/v2 v2.21.0 h1:2MfGpgJ1Pd1szkUK1xQ+JYcQEIBSRf2U1Eo1yV8dX5Q=
+github.com/coder/coder/v2 v2.21.0/go.mod h1:QLX7sMgj72WylhO0Pyw/ld1W4hfXwIEbPMQXLbgnE3E=
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc=
-github.com/coder/serpent v0.7.0 h1:zGpD2GlF3lKIVkMjNGKbkip88qzd5r/TRcc30X/SrT0=
-github.com/coder/serpent v0.7.0/go.mod h1:REkJ5ZFHQUWFTPLExhXYZ1CaHFjxvGNRlLXLdsI08YA=
-github.com/coder/terraform-provider-coder v0.23.0 h1:DuNLWxhnGlXyG0g+OCAZRI6xd8+bJjIEnE4F3hYgA4E=
-github.com/coder/terraform-provider-coder v0.23.0/go.mod h1:wMun9UZ9HT2CzF6qPPBup1odzBpVUc0/xSFoXgdI3tk=
+github.com/coder/serpent v0.10.0 h1:ofVk9FJXSek+SmL3yVE3GoArP83M+1tX+H7S4t8BSuM=
+github.com/coder/serpent v0.10.0/go.mod h1:cZFW6/fP+kE9nd/oRkEHJpG6sXCtQ+AX7WMMEHv0Y3Q=
+github.com/coder/terraform-provider-coder/v2 v2.1.3 h1:zB7ObGsiOGBHcJUUMmcSauEPlTWRIYmMYieF05LxHSc=
+github.com/coder/terraform-provider-coder/v2 v2.1.3/go.mod h1:RHGyb+ghiy8UpDAMJM8duRFuzd+1VqA3AtkRLh2P3Ug=
+github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
+github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
-github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
-github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
-github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
-github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
+github.com/coreos/go-oidc/v3 v3.13.0 h1:M66zd0pcc5VxvBNM4pB331Wrsanby+QomQYjN8HamW8=
+github.com/coreos/go-oidc/v3 v3.13.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
+github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo=
+github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
+github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
+github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
-github.com/docker/docker v27.1.2+incompatible h1:AhGzR1xaQIy53qCkxARaFluI00WPGtXn0AJuoQsVYTY=
-github.com/docker/docker v27.1.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
-github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
+github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I=
+github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
+github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
-github.com/ebitengine/purego v0.6.0-alpha.5 h1:EYID3JOAdmQ4SNZYJHu9V6IqOeRQDBYxqKAg9PyoHFY=
-github.com/ebitengine/purego v0.6.0-alpha.5/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
+github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 h1:8EXxF+tCLqaVk8AOC29zl2mnhQjwyLxxOTuhUazWRsg=
+github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4/go.mod h1:I5sHm0Y0T1u5YjlyqC5GVArM7aNZRUYtTjmJ8mPJFds=
+github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
+github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
-github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
-github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
+github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
+github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
-github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
-github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
-github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
-github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
-github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
-github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
-github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
-github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
-github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
-github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
-github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
-github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
-github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
+github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8=
+github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM=
+github.com/go-git/go-git/v5 v5.13.0 h1:vLn5wlGIh/X78El6r3Jr+30W16Blk0CTcxTYcYPWi5E=
+github.com/go-git/go-git/v5 v5.13.0/go.mod h1:Wjo7/JyVKtQgUNdXYXIepzWfJQkUEIGvkvVkiXRR/zw=
+github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
+github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
-github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
-github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
-github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
-github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
-github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
-github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
-github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
-github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
-github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
-github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
-github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
-github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
-github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
-github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
-github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
-github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
-github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
-github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
-github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
-github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk=
-github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
-github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
-github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
+github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
@@ -196,13 +196,10 @@ github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b/go.mod h1:czg5+yv1E0Z
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
-github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
-github.com/hashicorp/cli v1.1.6 h1:CMOV+/LJfL1tXCOKrgAX0uRKnzjj/mpmqNXloRSy2K8=
-github.com/hashicorp/cli v1.1.6/go.mod h1:MPon5QYlgjjo0BSoAiN0ESeT5fRzDjVRp+uioJ0piz4=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
+github.com/hashicorp/cli v1.1.7 h1:/fZJ+hNdwfTSfsxMBa9WWMlfjUZbX8/LnUxgAd7lCVU=
+github.com/hashicorp/cli v1.1.7/go.mod h1:e6Mfpga9OCT1vqzFuoGZiiF/KaG9CbUfO5s3ghU3YgU=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -211,52 +208,59 @@ github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuD
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
-github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI=
-github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs=
+github.com/hashicorp/go-cty v1.5.0 h1:EkQ/v+dDNUqnuVpmS5fPqyY71NXVgT5gf32+57xY8g0=
+github.com/hashicorp/go-cty v1.5.0/go.mod h1:lFUCG5kd8exDobgSfyj4ONE/dc822kiYMguVKdHGMLM=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
-github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A=
-github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI=
+github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog=
+github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
+github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 h1:UpiO20jno/eV1eVZcxqWnUohyKRe1g8FPV/xH1s/2qs=
+github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
+github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U=
+github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
+github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
+github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
+github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
-github.com/hashicorp/hc-install v0.8.0 h1:LdpZeXkZYMQhoKPCecJHlKvUkQFixN/nvyR1CdfOLjI=
-github.com/hashicorp/hc-install v0.8.0/go.mod h1:+MwJYjDfCruSD/udvBmRB22Nlkwwkwf5sAB6uTIhSaU=
-github.com/hashicorp/hcl/v2 v2.21.0 h1:lve4q/o/2rqwYOgUg3y3V2YPyD1/zkCLGjIV74Jit14=
-github.com/hashicorp/hcl/v2 v2.21.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
+github.com/hashicorp/hc-install v0.9.1 h1:gkqTfE3vVbafGQo6VZXcy2v5yoz2bE0+nhZXruCuODQ=
+github.com/hashicorp/hc-install v0.9.1/go.mod h1:pWWvN/IrfeBK4XPeXXYkL6EjMufHkCK5DvwxeLKuBf0=
+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-plugin-framework v1.11.0 h1:M7+9zBArexHFXDx/pKTxjE6n/2UCXY6b8FIq9ZYhwfE=
-github.com/hashicorp/terraform-plugin-framework v1.11.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM=
-github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 h1:bxZfGo9DIUoLLtHMElsu+zwqI4IsMZQBRRy4iLzZJ8E=
-github.com/hashicorp/terraform-plugin-framework-validators v0.13.0/go.mod h1:wGeI02gEhj9nPANU62F2jCaHjXulejm/X+af4PdZaNo=
-github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co=
-github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ=
+github.com/hashicorp/terraform-exec v0.22.0 h1:G5+4Sz6jYZfRYUCg6eQgDsqTzkNXV+fP8l+uRmZHj64=
+github.com/hashicorp/terraform-exec v0.22.0/go.mod h1:bjVbsncaeh8jVdhttWYZuBGj21FcYw6Ia/XfHcNO7lQ=
+github.com/hashicorp/terraform-json v0.24.0 h1:rUiyF+x1kYawXeRth6fKFm/MdfBS6+lW4NbeATsYz8Q=
+github.com/hashicorp/terraform-json v0.24.0/go.mod h1:Nfj5ubo9xbu9uiAoZVBsNOjvNKB66Oyrvtit74kC7ow=
+github.com/hashicorp/terraform-plugin-docs v0.21.0 h1:yoyA/Y719z9WdFJAhpUkI1jRbKP/nteVNBaI3hW7iQ8=
+github.com/hashicorp/terraform-plugin-docs v0.21.0/go.mod h1:J4Wott1J2XBKZPp/NkQv7LMShJYOcrqhQ2myXBcu64s=
+github.com/hashicorp/terraform-plugin-framework v1.14.1 h1:jaT1yvU/kEKEsxnbrn4ZHlgcxyIfjvZ41BLdlLk52fY=
+github.com/hashicorp/terraform-plugin-framework v1.14.1/go.mod h1:xNUKmvTs6ldbwTuId5euAtg37dTxuyj3LHS3uj7BHQ4=
+github.com/hashicorp/terraform-plugin-framework-validators v0.17.0 h1:0uYQcqqgW3BMyyve07WJgpKorXST3zkpzvrOnf3mpbg=
+github.com/hashicorp/terraform-plugin-framework-validators v0.17.0/go.mod h1:VwdfgE/5Zxm43flraNa0VjcvKQOGVrcO4X8peIri0T0=
+github.com/hashicorp/terraform-plugin-go v0.26.0 h1:cuIzCv4qwigug3OS7iKhpGAbZTiypAfFQmw8aE65O2M=
+github.com/hashicorp/terraform-plugin-go v0.26.0/go.mod h1:+CXjuLDiFgqR+GcrM5a2E2Kal5t5q2jb0E3D57tTdNY=
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
-github.com/hashicorp/terraform-plugin-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-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-plugin-sdk/v2 v2.36.1 h1:WNMsTLkZf/3ydlgsuXePa3jvZFwAJhruxTxP/c1Viuw=
+github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1/go.mod h1:P6o64QS97plG44iFzSM6rAn6VJIC/Sy9a9IkEtl79K4=
+github.com/hashicorp/terraform-plugin-testing v1.12.0 h1:tpIe+T5KBkA1EO6aT704SPLedHUo55RenguLHcaSBdI=
+github.com/hashicorp/terraform-plugin-testing v1.12.0/go.mod h1:jbDQUkT9XRjAh1Bvyufq+PEH1Xs4RqIdpOQumSgSXBM=
+github.com/hashicorp/terraform-registry-address v0.2.4 h1:JXu/zHB2Ymg/TGVCRu10XqNa4Sh2bWcqCNyKWjnCPJA=
+github.com/hashicorp/terraform-registry-address v0.2.4/go.mod h1:tUNYTVyCtU4OIGXXMDp7WNcJ+0W1B4nmstVDgHMjfAU=
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
-github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
-github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
+github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
+github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY=
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
@@ -264,24 +268,22 @@ github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
-github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
-github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
+github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
+github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
-github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
-github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
-github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
-github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
-github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -289,29 +291,35 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
-github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
-github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
+github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c h1:VtwQ41oftZwlMnOEbMWQtSEUgU64U4s+GHk7hZK+jtY=
+github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
-github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
-github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
+github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
+github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE=
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
@@ -319,43 +327,49 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
-github.com/moby/moby v27.1.1+incompatible h1:WdCIKJ4WIxhrKti5c+Z7sj2SLADbsuB/reEBpQ4rtOQ=
-github.com/moby/moby v27.1.1+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc=
+github.com/moby/moby v28.0.0+incompatible h1:D+F1Z56b/DS8J5pUkTG/stemqrvHBQ006hUqJxjV9P0=
+github.com/moby/moby v28.0.0+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc=
+github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
+github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
+github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
+github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
-github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
-github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
-github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
-github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
-github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
-github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
+github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A=
+github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
+github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
+github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
-github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
-github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
-github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
-github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
+github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
+github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
+github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
+github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/outcaste-io/ristretto v0.2.3 h1:AK4zt/fJ76kjlYObOeNwh4T3asEuaCmp26pOvUOL9w0=
github.com/outcaste-io/ristretto v0.2.3/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac=
-github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
-github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
-github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
-github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
+github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 h1:jYi87L8j62qkXzaYHAQAhEapgukhenIMZRBKTNRLHJ4=
+github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
-github.com/pion/transport/v2 v2.0.0 h1:bsMYyqHCbkvHwj+eNCFBuxtlKndKfyGI2vaQmM3fIE4=
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
+github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
+github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
+github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
+github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8=
github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
@@ -365,44 +379,59 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
-github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
-github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
+github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI=
+github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
+github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
+github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
-github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
-github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
-github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
-github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
-github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 h1:Qp27Idfgi6ACvFQat5+VJvlYToylpM/hcyLBI3WaKPA=
-github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+uGV8BrTv8W/SIwk=
+github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
+github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3 h1:4+LEVOB87y175cLJC/mbsgKmoDOjrBldtXvioEy96WY=
+github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3/go.mod h1:vl5+MqJ1nBINuSsUI2mGgH79UweUT/B5Fy8857PqyyI=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
-github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+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=
github.com/secure-systems-lab/go-securesystemslib v0.7.0 h1:OwvJ5jQf9LnIAS83waAjPbcMsODrTQUpJ02eNLUoxBg=
github.com/secure-systems-lab/go-securesystemslib v0.7.0/go.mod h1:/2gYnlnHVQ6xeGtfIqFy7Do03K4cdCY0A/GlJLDKLHI=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
+github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
+github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
+github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
+github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
+github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
+github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
-github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
+github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
+github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
-github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
+github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
+github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
-github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -414,38 +443,43 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
-github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
-github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
-github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
-github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
-github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
-github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
-github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
-github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/tinylib/msgp v1.2.1 h1:6ypy2qcCznxpP4hpORzhtXyTqrBs7cfM9MCCWY8zsmU=
+github.com/tinylib/msgp v1.2.1/go.mod h1:2vIGs3lcUo8izAATNobrCHevYZC/LMsJtw4JPiYPHro=
+github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
+github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
+github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
+github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
-github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
+github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
+github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
+github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvCazn8G65U=
+github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
+github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
+github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
+github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
@@ -454,12 +488,14 @@ 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.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
+github.com/yuin/goldmark v1.7.8/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=
-github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+github.com/zclconf/go-cty v1.16.2 h1:LAJSwc3v81IRBZyUVQDUdZ7hs3SYs9jv0eZJDWHD/70=
+github.com/zclconf/go-cty v1.16.2/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
@@ -468,55 +504,72 @@ github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
go.abhg.dev/goldmark/frontmatter v0.2.0 h1:P8kPG0YkL12+aYk2yU3xHv4tcXzeVnN+gU0tJ5JnxRw=
go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px76YjkOzhB4YlU=
-go.nhat.io/otelsql v0.13.0 h1:L6obwZRxgFQqeSvo7jCemP659fu7pqsDHQNuZ3Ev1yI=
-go.nhat.io/otelsql v0.13.0/go.mod h1:HyYpqd7G9BK+9cPLydV+2JN/4J5D3wlX6+jDLTk52GE=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
-go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
-go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw=
+go.nhat.io/otelsql v0.15.0 h1:e2lpIaFPe62Pa1fXZoOWXTvMzcN4SwHwHdCz1wDUG6c=
+go.nhat.io/otelsql v0.15.0/go.mod h1:IYUaWCLf7c883mzhfVpHXTBn0jxF4TRMkQjX6fqhXJ8=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/collector/component v0.104.0 h1:jqu/X9rnv8ha0RNZ1a9+x7OU49KwSMsPbOuIEykHuQE=
+go.opentelemetry.io/collector/component v0.104.0/go.mod h1:1C7C0hMVSbXyY1ycCmaMUAR9fVwpgyiNQqxXtEWhVpw=
+go.opentelemetry.io/collector/config/configtelemetry v0.104.0 h1:eHv98XIhapZA8MgTiipvi+FDOXoFhCYOwyKReOt+E4E=
+go.opentelemetry.io/collector/config/configtelemetry v0.104.0/go.mod h1:WxWKNVAQJg/Io1nA3xLgn/DWLE/W1QOB2+/Js3ACi40=
+go.opentelemetry.io/collector/pdata v1.11.0 h1:rzYyV1zfTQQz1DI9hCiaKyyaczqawN75XO9mdXmR/hE=
+go.opentelemetry.io/collector/pdata v1.11.0/go.mod h1:IHxHsp+Jq/xfjORQMDJjSH6jvedOSTOyu3nbxqhWSYE=
+go.opentelemetry.io/collector/pdata/pprofile v0.104.0 h1:MYOIHvPlKEJbWLiBKFQWGD0xd2u22xGVLt4jPbdxP4Y=
+go.opentelemetry.io/collector/pdata/pprofile v0.104.0/go.mod h1:7WpyHk2wJZRx70CGkBio8klrYTTXASbyIhf+rH4FKnA=
+go.opentelemetry.io/collector/semconv v0.104.0 h1:dUvajnh+AYJLEW/XOPk0T0BlwltSdi3vrjO7nSOos3k=
+go.opentelemetry.io/collector/semconv v0.104.0/go.mod h1:yMVUCNoQPZVq/IPfrHrnntZTWsLf5YGZ7qwKulIl5hw=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
+go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
+go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY=
-go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.24.0 h1:JYE2HM7pZbOt5Jhk8ndWZTUWYOVift2cHjXVMkPdmdc=
-go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.24.0/go.mod h1:yMb/8c6hVsnma0RpsBMNo0fEiQKeclawtgaIaOp2MLY=
-go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8=
-go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA=
-go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
-go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
-go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
-go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
-go.opentelemetry.io/otel/sdk/metric v1.24.0 h1:yyMQrPzF+k88/DbH7o4FMAs80puqd+9osbiBrJrz/w8=
-go.opentelemetry.io/otel/sdk/metric v1.24.0/go.mod h1:I6Y5FjH6rvEnTTAYQz3Mmv2kl6Ek5IIrmwTLqMrrOE0=
-go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
-go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
-go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
-go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
+go.opentelemetry.io/otel/exporters/prometheus v0.49.0 h1:Er5I1g/YhfYv9Affk9nJLfH/+qCCVVg1f2R9AbJfqDQ=
+go.opentelemetry.io/otel/exporters/prometheus v0.49.0/go.mod h1:KfQ1wpjf3zsHjzP149P4LyAwWRupc6c7t1ZJ9eXpKQM=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0 h1:FiOTYABOX4tdzi8A0+mtzcsTmi6WBOxk66u0f1Mj9Gs=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.33.0/go.mod h1:xyo5rS8DgzV0Jtsht+LCEMwyiDbjpsxBpWETwFRF0/4=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0 h1:W5AWUn/IVe8RFb5pZx1Uh9Laf/4+Qmm4kJL5zPuvR+0=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.33.0/go.mod h1:mzKxJywMNBdEX8TSJais3NnsVZUaJ+bAy6UxPTng2vk=
+go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
+go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
+go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
+go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
+go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
+go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
+go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.1-0.20240429205332-517bace7cc29 h1:w0QrHuh0hhUZ++UTQaBM2DMdrWQghZ/UsUb+Wb1+8YE=
go.uber.org/goleak v1.3.1-0.20240429205332-517bace7cc29/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
-golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
-golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
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.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
-golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
-golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
-golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
+golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+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=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
-golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
+golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -526,11 +579,13 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
-golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
-golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
-golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
-golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
+golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
+golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -538,15 +593,18 @@ 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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
+golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+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=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -557,86 +615,107 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+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.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
-golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+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.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
-golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
-golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
-golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
+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.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
+golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
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=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
-golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
-golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
-golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
-golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
+golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
-golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
+golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
+golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
-google.golang.org/genproto v0.0.0-20240730163845-b1a4ccb954bf h1:OqdXDEakZCVtDiZTjcxfwbHPCT11ycCEsTKesBVKvyY=
-google.golang.org/genproto v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:mCr1K1c8kX+1iSBREvU3Juo11CB+QOEWxbRS01wWl5M=
-google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f h1:b1Ln/PG8orm0SsBbHZWke8dDp2lrCD4jSmfglFpTZbk=
-google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f/go.mod h1:AHT0dDg3SoMOgZGnZk29b5xTbPHMoEC8qthmBLJCpys=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
-google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
-google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
+google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk=
+google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc=
+google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=
+google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
+google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
+google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
-google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
-gopkg.in/DataDog/dd-trace-go.v1 v1.64.0 h1:zXQo6iv+dKRrDBxMXjRXLSKN2lY9uM34XFI4nPyp0eA=
-gopkg.in/DataDog/dd-trace-go.v1 v1.64.0/go.mod h1:qzwVu8Qr8CqzQNw2oKEXRdD+fMnjYatjYMGE0tdCVG4=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/DataDog/dd-trace-go.v1 v1.72.1 h1:QG2HNpxe9H4WnztDYbdGQJL/5YIiiZ6xY1+wMuQ2c1w=
+gopkg.in/DataDog/dd-trace-go.v1 v1.72.1/go.mod h1:XqDhDqsLpThFnJc4z0FvAEItISIAUka+RHwmQ6EfN1U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
-honnef.co/go/gotraceui v0.2.0 h1:dmNsfQ9Vl3GwbiVD7Z8d/osC6WtGGrasyrC2suc4ZIQ=
-honnef.co/go/gotraceui v0.2.0/go.mod h1:qHo4/W75cA3bX0QQoSvDjbJa4R8mAyyFjbWAj63XElc=
-nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
-nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
+lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
+lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
+modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
+modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
+modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0=
+modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
+modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
+modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
+modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
+modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
+modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
+modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
+modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
+modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
+modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
+modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
storj.io/drpc v0.0.33 h1:yCGZ26r66ZdMP0IcTYsj7WDAUIIjzXk6DJhbhvt9FHI=
storj.io/drpc v0.0.33/go.mod h1:vR804UNzhBa49NOJ6HeLjd2H3MakC1j5Gv8bsOQT6N4=
diff --git a/integration/integration.go b/integration/integration.go
index 0b6f98e..9f7120f 100644
--- a/integration/integration.go
+++ b/integration/integration.go
@@ -51,10 +51,10 @@ func StartCoder(ctx context.Context, t *testing.T, name string, useLicense bool)
ctr, err := cli.ContainerCreate(ctx, &container.Config{
Image: coderImg + ":" + coderVersion,
Env: []string{
- "CODER_HTTP_ADDRESS=0.0.0.0:3000", // Listen on all interfaces inside the container
- "CODER_ACCESS_URL=http://localhost:3000", // Set explicitly to avoid creating try.coder.app URLs.
- "CODER_IN_MEMORY=true", // We don't necessarily care about real persistence here.
- "CODER_TELEMETRY_ENABLE=false", // Avoid creating noise.
+ "CODER_HTTP_ADDRESS=0.0.0.0:3000", // Listen on all interfaces inside the container
+ "CODER_ACCESS_URL=http://localhost:3000", // Set explicitly to avoid creating try.coder.app URLs.
+ "CODER_TELEMETRY_ENABLE=false", // Avoid creating noise.
+ "CODER_DANGEROUS_DISABLE_RATE_LIMITS=true", // Avoid hitting file rate limit in tests.
},
Labels: map[string]string{},
ExposedPorts: map[nat.Port]struct{}{nat.Port("3000/tcp"): {}},
@@ -96,7 +96,7 @@ func StartCoder(ctx context.Context, t *testing.T, name string, useLicense bool)
t.Logf("not ready yet: %s", err.Error())
}
return err == nil
- }, 10*time.Second, time.Second, "coder failed to become ready in time")
+ }, 20*time.Second, time.Second, "coder failed to become ready in time")
_, err = client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
Email: testEmail,
Username: testUsername,
diff --git a/integration/integration_test.go b/integration/integration_test.go
index 7f289c4..79834a8 100644
--- a/integration/integration_test.go
+++ b/integration/integration_test.go
@@ -22,6 +22,7 @@ import (
// - Runs the `main.tf` specified in the given test directory against the Coder deployment
// - Asserts the state of the deployment via `codersdk`.
func TestIntegration(t *testing.T) {
+ t.Parallel()
if os.Getenv("TF_ACC") == "1" {
t.Skip("Skipping integration tests during tf acceptance tests")
}
@@ -35,7 +36,7 @@ func TestIntegration(t *testing.T) {
}
timeoutMins, err := strconv.Atoi(timeoutStr)
require.NoError(t, err, "invalid value specified for timeout")
- ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutMins)*time.Minute)
+ ctx, cancel := context.WithTimeout(t.Context(), time.Duration(timeoutMins)*time.Minute)
t.Cleanup(cancel)
tfrcPath := setupProvider(t)
@@ -97,7 +98,7 @@ func TestIntegration(t *testing.T) {
assert.Equal(t, "dean", user.Username)
// Check group
- defaultOrg, err := c.OrganizationByName(ctx, "first-organization")
+ defaultOrg, err := c.OrganizationByName(ctx, "coder")
assert.NoError(t, err)
group, err := c.GroupByOrgAndName(ctx, defaultOrg.ID, "employees")
assert.NoError(t, err)
@@ -109,7 +110,7 @@ func TestIntegration(t *testing.T) {
name: "template-test",
preF: func(t testing.TB, c *codersdk.Client) {},
assertF: func(t testing.TB, c *codersdk.Client) {
- defaultOrg, err := c.OrganizationByName(ctx, "first-organization")
+ defaultOrg, err := c.OrganizationByName(ctx, "coder")
assert.NoError(t, err)
user, err := c.User(ctx, "ethan")
require.NoError(t, err)
@@ -145,6 +146,8 @@ func TestIntegration(t *testing.T) {
},
} {
t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+
client := StartCoder(ctx, t, tt.name, true)
wd, err := os.Getwd()
require.NoError(t, err)
@@ -166,7 +169,7 @@ func TestIntegration(t *testing.T) {
tfCmd.Stderr = &buf
tt.preF(t, client)
if err := tfCmd.Run(); !assert.NoError(t, err) {
- t.Logf("%s", buf.String())
+ t.Log(buf.String())
t.FailNow()
}
tt.assertF(t, client)
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.go b/internal/provider/group_data_source.go
index 95c8e6e..9e3639f 100644
--- a/internal/provider/group_data_source.go
+++ b/internal/provider/group_data_source.go
@@ -40,14 +40,13 @@ type GroupDataSourceModel struct {
}
type Member struct {
- ID UUID `tfsdk:"id"`
- Username types.String `tfsdk:"username"`
- Email types.String `tfsdk:"email"`
- CreatedAt types.Int64 `tfsdk:"created_at"`
- LastSeenAt types.Int64 `tfsdk:"last_seen_at"`
- Status types.String `tfsdk:"status"`
- LoginType types.String `tfsdk:"login_type"`
- ThemePreference types.String `tfsdk:"theme_preference"`
+ ID UUID `tfsdk:"id"`
+ Username types.String `tfsdk:"username"`
+ Email types.String `tfsdk:"email"`
+ CreatedAt types.Int64 `tfsdk:"created_at"`
+ LastSeenAt types.Int64 `tfsdk:"last_seen_at"`
+ Status types.String `tfsdk:"status"`
+ LoginType types.String `tfsdk:"login_type"`
}
func (d *GroupDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
@@ -93,7 +92,7 @@ func (d *GroupDataSource) Schema(ctx context.Context, req datasource.SchemaReque
Computed: true,
},
"source": schema.StringAttribute{
- MarkdownDescription: "The source of the group. Either 'oidc' or 'user'.",
+ MarkdownDescription: "The source of the group. Either `oidc` or `user`.",
Computed: true,
},
"members": schema.SetNestedAttribute{
@@ -120,16 +119,13 @@ func (d *GroupDataSource) Schema(ctx context.Context, req datasource.SchemaReque
Computed: true,
},
"status": schema.StringAttribute{
- MarkdownDescription: "The status of the member. Can be 'active', 'dormant' or 'suspended'.",
+ MarkdownDescription: "The status of the member. Can be `active`, `dormant` or `suspended`.",
Computed: true,
},
"login_type": schema.StringAttribute{
- MarkdownDescription: "The login type of the member. Can be 'oidc', 'token', 'password', 'github' or 'none'.",
+ MarkdownDescription: "The login type of the member. Can be `oidc`, `token`, `password`, `github` or `none`.",
Computed: true,
},
- "theme_preference": schema.StringAttribute{
- Computed: true,
- },
// TODO: Upgrade requested user type if required
},
},
@@ -187,6 +183,11 @@ func (d *GroupDataSource) Read(ctx context.Context, req datasource.ReadRequest,
groupID := data.ID.ValueUUID()
group, err = client.Group(ctx, groupID)
if err != nil {
+ if isNotFound(err) {
+ resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("Group with ID %s not found. Marking as deleted.", groupID.String()))
+ resp.State.RemoveResource(ctx)
+ return
+ }
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get group by ID, got error: %s", err))
return
}
@@ -195,6 +196,11 @@ func (d *GroupDataSource) Read(ctx context.Context, req datasource.ReadRequest,
} else {
group, err = client.GroupByOrgAndName(ctx, data.OrganizationID.ValueUUID(), data.Name.ValueString())
if err != nil {
+ if isNotFound(err) {
+ resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("Group with name %s not found in organization with ID %s. Marking as deleted.", data.Name.ValueString(), data.OrganizationID.ValueString()))
+ resp.State.RemoveResource(ctx)
+ return
+ }
resp.Diagnostics.AddError("Failed to get group by name and org ID", err.Error())
return
}
@@ -207,14 +213,13 @@ func (d *GroupDataSource) Read(ctx context.Context, req datasource.ReadRequest,
members := make([]Member, 0, len(group.Members))
for _, member := range group.Members {
members = append(members, Member{
- ID: UUIDValue(member.ID),
- Username: types.StringValue(member.Username),
- Email: types.StringValue(member.Email),
- CreatedAt: types.Int64Value(member.CreatedAt.Unix()),
- LastSeenAt: types.Int64Value(member.LastSeenAt.Unix()),
- Status: types.StringValue(string(member.Status)),
- LoginType: types.StringValue(string(member.LoginType)),
- ThemePreference: types.StringValue(member.ThemePreference),
+ ID: UUIDValue(member.ID),
+ Username: types.StringValue(member.Username),
+ Email: types.StringValue(member.Email),
+ CreatedAt: types.Int64Value(member.CreatedAt.Unix()),
+ LastSeenAt: types.Int64Value(member.LastSeenAt.Unix()),
+ Status: types.StringValue(string(member.Status)),
+ LoginType: types.StringValue(string(member.LoginType)),
})
}
data.Members = members
diff --git a/internal/provider/group_data_source_test.go b/internal/provider/group_data_source_test.go
index 349e855..3cd43f2 100644
--- a/internal/provider/group_data_source_test.go
+++ b/internal/provider/group_data_source_test.go
@@ -1,13 +1,13 @@
package provider
import (
- "context"
"os"
"regexp"
"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/hashicorp/terraform-plugin-testing/helper/resource"
@@ -15,10 +15,11 @@ import (
)
func TestAccGroupDataSource(t *testing.T) {
+ t.Parallel()
if os.Getenv("TF_ACC") == "" {
t.Skip("Acceptance tests are disabled.")
}
- ctx := context.Background()
+ ctx := t.Context()
client := integration.StartCoder(ctx, t, "group_data_acc", true)
firstUser, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
@@ -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 c7f11bd..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"
@@ -62,8 +62,7 @@ func (r *GroupResource) Metadata(ctx context.Context, req resource.MetadataReque
func (r *GroupResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "A group on the Coder deployment.\n\n" +
- "Creating groups requires an Enterprise license.\n\n" +
- "When importing, the ID supplied can be either a group UUID retrieved via the API or `/`.",
+ "Creating groups requires an Enterprise license.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
@@ -77,13 +76,16 @@ func (r *GroupResource) Schema(ctx context.Context, req resource.SchemaRequest,
"name": schema.StringAttribute{
MarkdownDescription: "The unique name of the group.",
Required: true,
+ Validators: []validator.String{
+ codersdkvalidator.GroupName(),
+ },
},
"display_name": schema.StringAttribute{
MarkdownDescription: "The display name of the group. Defaults to the group name.",
Computed: true,
Optional: true,
Validators: []validator.String{
- stringvalidator.LengthAtLeast(1),
+ codersdkvalidator.DisplayName(),
},
Default: stringdefault.StaticString(""),
},
@@ -110,7 +112,7 @@ func (r *GroupResource) Schema(ctx context.Context, req resource.SchemaRequest,
},
},
"members": schema.SetAttribute{
- MarkdownDescription: "Members of the group, by ID. If null, members will not be added or removed by Terraform. To have a group resource with unmanaged members, but be able to read the members in Terraform, use `data.coderd_group`",
+ MarkdownDescription: "Members of the group, by ID. If `null`, members will not be added or removed by Terraform. To have a group resource with unmanaged members, but be able to read the members in Terraform, use `data.coderd_group`",
ElementType: UUIDType,
Optional: true,
},
@@ -215,6 +217,11 @@ func (r *GroupResource) Read(ctx context.Context, req resource.ReadRequest, resp
group, err := client.Group(ctx, groupID)
if err != nil {
+ if isNotFound(err) {
+ resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("Group with ID %s not found. Marking as deleted.", groupID.String()))
+ resp.State.RemoveResource(ctx)
+ return
+ }
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get group, got error: %s", err))
return
}
diff --git a/internal/provider/group_resource_test.go b/internal/provider/group_resource_test.go
index 159856f..3c1b589 100644
--- a/internal/provider/group_resource_test.go
+++ b/internal/provider/group_resource_test.go
@@ -1,13 +1,13 @@
package provider
import (
- "context"
"os"
"regexp"
"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/hashicorp/terraform-plugin-testing/helper/resource"
@@ -15,10 +15,11 @@ import (
)
func TestAccGroupResource(t *testing.T) {
+ t.Parallel()
if os.Getenv("TF_ACC") == "" {
t.Skip("Acceptance tests are disabled.")
}
- ctx := context.Background()
+ ctx := t.Context()
client := integration.StartCoder(ctx, t, "group_acc", true)
firstUser, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
@@ -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
@@ -132,10 +133,11 @@ func TestAccGroupResource(t *testing.T) {
}
func TestAccGroupResourceAGPL(t *testing.T) {
+ t.Parallel()
if os.Getenv("TF_ACC") == "" {
t.Skip("Acceptance tests are disabled.")
}
- ctx := context.Background()
+ ctx := t.Context()
client := integration.StartCoder(ctx, t, "group_acc_agpl", false)
firstUser, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
@@ -143,11 +145,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
new file mode 100644
index 0000000..5cb4778
--- /dev/null
+++ b/internal/provider/license_resource.go
@@ -0,0 +1,198 @@
+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/int32planmodifier"
+ "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 = &LicenseResource{}
+
+func NewLicenseResource() resource.Resource {
+ return &LicenseResource{}
+}
+
+// LicenseResource defines the resource implementation.
+type LicenseResource struct {
+ data *CoderdProviderData
+}
+
+// LicenseResourceModel describes the resource data model.
+type LicenseResourceModel struct {
+ ID types.Int32 `tfsdk:"id"`
+ ExpiresAt types.Int64 `tfsdk:"expires_at"`
+ License types.String `tfsdk:"license"`
+}
+
+func (r *LicenseResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_license"
+}
+
+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 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.",
+
+ Attributes: map[string]schema.Attribute{
+ "id": schema.Int32Attribute{
+ MarkdownDescription: "Integer ID of the license.",
+ Computed: true,
+ PlanModifiers: []planmodifier.Int32{
+ int32planmodifier.UseStateForUnknown(),
+ },
+ },
+ "expires_at": schema.Int64Attribute{
+ MarkdownDescription: "Unix timestamp of when the license expires.",
+ Computed: true,
+ },
+ "license": schema.StringAttribute{
+ MarkdownDescription: "A license key for Coder.",
+ Required: true,
+ Sensitive: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ },
+ }
+}
+
+func (r *LicenseResource) 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.data = data
+}
+
+func (r *LicenseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var data LicenseResourceModel
+
+ // Read Terraform plan data into the model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ client := r.data.Client
+
+ license, err := client.AddLicense(ctx, codersdk.AddLicenseRequest{
+ License: data.License.ValueString(),
+ })
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to add license, got error: %s", err))
+ return
+ }
+ data.ID = types.Int32Value(license.ID)
+ expiresAt, err := license.ExpiresAt()
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse license expiration, got error: %s", err))
+ return
+ }
+ data.ExpiresAt = types.Int64Value(expiresAt.Unix())
+
+ // Save data into Terraform state
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *LicenseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var data LicenseResourceModel
+
+ // Read Terraform prior state data into the model
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ licenses, err := r.data.Client.Licenses(ctx)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to list licenses, got error: %s", err))
+ return
+ }
+
+ found := false
+ for _, license := range licenses {
+ if license.ID == data.ID.ValueInt32() {
+ found = true
+ expiresAt, err := license.ExpiresAt()
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to parse license expiration, got error: %s", err))
+ return
+ }
+ data.ExpiresAt = types.Int64Value(expiresAt.Unix())
+ }
+ }
+ if !found {
+ resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("License with ID %d not found. Marking as deleted.", data.ID.ValueInt32()))
+ resp.State.RemoveResource(ctx)
+ return
+ }
+
+ // Save updated data into Terraform state
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *LicenseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var data LicenseResourceModel
+
+ // Read Terraform plan data into the model
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Update is handled by replacement
+
+ // Save updated data into Terraform state
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *LicenseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var data LicenseResourceModel
+
+ // Read Terraform prior state data into the model
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ client := r.data.Client
+
+ err := client.DeleteLicense(ctx, data.ID.ValueInt32())
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete license, got error: %s", err))
+ return
+ }
+}
diff --git a/internal/provider/license_resource_test.go b/internal/provider/license_resource_test.go
new file mode 100644
index 0000000..d9c8ffb
--- /dev/null
+++ b/internal/provider/license_resource_test.go
@@ -0,0 +1,74 @@
+package provider
+
+import (
+ "os"
+ "strings"
+ "testing"
+ "text/template"
+
+ "github.com/coder/terraform-provider-coderd/integration"
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/stretchr/testify/require"
+)
+
+func TestAccLicenseResource(t *testing.T) {
+ t.Parallel()
+ if os.Getenv("TF_ACC") == "" {
+ t.Skip("Acceptance tests are disabled.")
+ }
+ ctx := t.Context()
+ client := integration.StartCoder(ctx, t, "license_acc", false)
+
+ license := os.Getenv("CODER_ENTERPRISE_LICENSE")
+ if license == "" {
+ t.Skip("No license found for license resource tests, skipping")
+ }
+
+ cfg1 := testAccLicenseResourceConfig{
+ URL: client.URL.String(),
+ Token: client.SessionToken(),
+ License: license,
+ }
+
+ resource.Test(t, resource.TestCase{
+ IsUnitTest: true,
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: cfg1.String(t),
+ },
+ },
+ })
+}
+
+type testAccLicenseResourceConfig struct {
+ URL string
+ Token string
+ License string
+}
+
+func (c testAccLicenseResourceConfig) String(t *testing.T) string {
+ t.Helper()
+ tpl := `
+provider coderd {
+ url = "{{.URL}}"
+ token = "{{.Token}}"
+}
+
+resource "coderd_license" "test" {
+ license = "{{.License}}"
+}
+`
+ funcMap := template.FuncMap{
+ "orNull": PrintOrNull,
+ }
+
+ buf := strings.Builder{}
+ tmpl, err := template.New("licenseResource").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/organization_data_source.go b/internal/provider/organization_data_source.go
index 59b598c..835a2ed 100644
--- a/internal/provider/organization_data_source.go
+++ b/internal/provider/organization_data_source.go
@@ -127,6 +127,11 @@ func (d *OrganizationDataSource) Read(ctx context.Context, req datasource.ReadRe
orgID := data.ID.ValueUUID()
org, err = client.Organization(ctx, orgID)
if err != nil {
+ if isNotFound(err) {
+ resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("Organization with ID %s not found. Marking as deleted.", data.ID.ValueString()))
+ resp.State.RemoveResource(ctx)
+ return
+ }
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization by ID, got error: %s", err))
return
}
@@ -137,6 +142,11 @@ func (d *OrganizationDataSource) Read(ctx context.Context, req datasource.ReadRe
} else if data.IsDefault.ValueBool() { // Get Default
org, err = client.OrganizationByName(ctx, "default")
if err != nil {
+ if isNotFound(err) {
+ resp.Diagnostics.AddWarning("Client Warning", "Default organization not found. Marking as deleted.")
+ resp.State.RemoveResource(ctx)
+ return
+ }
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get default organization, got error: %s", err))
return
}
@@ -147,6 +157,11 @@ func (d *OrganizationDataSource) Read(ctx context.Context, req datasource.ReadRe
} else { // By Name
org, err = client.OrganizationByName(ctx, data.Name.ValueString())
if err != nil {
+ if isNotFound(err) {
+ resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("Organization with name %s not found. Marking as deleted.", data.Name))
+ resp.State.RemoveResource(ctx)
+ return
+ }
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get organization by name, got error: %s", err))
return
}
diff --git a/internal/provider/organization_data_source_test.go b/internal/provider/organization_data_source_test.go
index fe744db..1bedc4f 100644
--- a/internal/provider/organization_data_source_test.go
+++ b/internal/provider/organization_data_source_test.go
@@ -1,13 +1,13 @@
package provider
import (
- "context"
"os"
"regexp"
"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/hashicorp/terraform-plugin-testing/helper/resource"
@@ -15,10 +15,11 @@ import (
)
func TestAccOrganizationDataSource(t *testing.T) {
+ t.Parallel()
if os.Getenv("TF_ACC") == "" {
t.Skip("Acceptance tests are disabled.")
}
- ctx := context.Background()
+ ctx := t.Context()
client := integration.StartCoder(ctx, t, "org_data_acc", false)
firstUser, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
@@ -26,7 +27,7 @@ func TestAccOrganizationDataSource(t *testing.T) {
defaultCheckFn := resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.coderd_organization.test", "id", firstUser.OrganizationIDs[0].String()),
resource.TestCheckResourceAttr("data.coderd_organization.test", "is_default", "true"),
- resource.TestCheckResourceAttr("data.coderd_organization.test", "name", "first-organization"),
+ resource.TestCheckResourceAttr("data.coderd_organization.test", "name", "coder"),
resource.TestCheckResourceAttr("data.coderd_organization.test", "members.#", "1"),
resource.TestCheckTypeSetElemAttr("data.coderd_organization.test", "members.*", firstUser.ID.String()),
resource.TestCheckResourceAttrSet("data.coderd_organization.test", "created_at"),
@@ -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("first-organization"),
+ 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("first-organization"),
+ 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..8fbaba5
--- /dev/null
+++ b/internal/provider/organization_resource.go
@@ -0,0 +1,652 @@
+package provider
+
+import (
+ "context"
+ "fmt"
+ "regexp"
+
+ "github.com/coder/coder/v2/coderd/util/slice"
+ "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"`
+
+ OrgSyncIdpGroups types.Set `tfsdk:"org_sync_idp_groups"`
+ 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.
+
+~> **Warning**
+This resource is only compatible with Coder version [2.16.0](https://github.com/coder/coder/releases/tag/v2.16.0) and later.
+`,
+ 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,
+ 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(""),
+ },
+
+ "org_sync_idp_groups": schema.SetAttribute{
+ ElementType: types.StringType,
+ Optional: true,
+ MarkdownDescription: "Claims from the IdP provider that will give users access to this organization.",
+ },
+ },
+
+ Blocks: map[string]schema.Block{
+ "group_sync": schema.SingleNestedBlock{
+ MarkdownDescription: `Group sync settings to sync groups from an IdP.`,
+ 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{
+ MarkdownDescription: `Role sync settings to sync organization roles from an IdP.`,
+ 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)
+
+ orgID := data.ID.ValueUUID()
+
+ // Apply org sync patches, if specified
+ if !data.OrgSyncIdpGroups.IsNull() {
+ tflog.Trace(ctx, "updating org sync", map[string]any{
+ "orgID": orgID,
+ })
+
+ var claims []string
+ resp.Diagnostics.Append(data.OrgSyncIdpGroups.ElementsAs(ctx, &claims, false)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(r.patchOrgSyncMapping(ctx, orgID, []string{}, claims)...)
+ }
+
+ // Apply group and role sync settings, if specified
+ if !data.GroupSync.IsNull() {
+ tflog.Trace(ctx, "updating group sync", map[string]any{
+ "orgID": orgID,
+ })
+
+ resp.Diagnostics.Append(r.patchGroupSync(ctx, orgID, data.GroupSync)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ }
+ if !data.RoleSync.IsNull() {
+ tflog.Trace(ctx, "updating role sync", map[string]any{
+ "orgID": orgID,
+ })
+ 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,
+ })
+
+ // Apply org sync patches, if specified
+ if !data.OrgSyncIdpGroups.IsNull() {
+ tflog.Trace(ctx, "updating org sync mappings", map[string]any{
+ "orgID": orgID,
+ })
+
+ var state OrganizationResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ var currentClaims []string
+ resp.Diagnostics.Append(state.OrgSyncIdpGroups.ElementsAs(ctx, ¤tClaims, false)...)
+
+ var plannedClaims []string
+ resp.Diagnostics.Append(data.OrgSyncIdpGroups.ElementsAs(ctx, &plannedClaims, false)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(r.patchOrgSyncMapping(ctx, orgID, currentClaims, plannedClaims)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ }
+
+ if !data.GroupSync.IsNull() {
+ tflog.Trace(ctx, "updating group sync", map[string]any{
+ "orgID": orgID,
+ })
+ resp.Diagnostics.Append(r.patchGroupSync(ctx, orgID, data.GroupSync)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ }
+ if !data.RoleSync.IsNull() {
+ tflog.Trace(ctx, "updating role sync", map[string]any{
+ "orgID": orgID,
+ })
+ 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()
+
+ // Remove org sync mappings, if we were managing them
+ if !data.OrgSyncIdpGroups.IsNull() {
+ tflog.Trace(ctx, "deleting org sync mappings", map[string]any{
+ "orgID": orgID,
+ })
+
+ var claims []string
+ resp.Diagnostics.Append(data.OrgSyncIdpGroups.ElementsAs(ctx, &claims, false)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ resp.Diagnostics.Append(r.patchOrgSyncMapping(ctx, orgID, claims, []string{})...)
+ }
+
+ 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
+}
+
+func (r *OrganizationResource) patchOrgSyncMapping(
+ ctx context.Context,
+ orgID uuid.UUID,
+ currentClaims, plannedClaims []string,
+) diag.Diagnostics {
+ var diags diag.Diagnostics
+
+ add, remove := slice.SymmetricDifference(currentClaims, plannedClaims)
+ var addMappings []codersdk.IDPSyncMapping[uuid.UUID]
+ for _, claim := range add {
+ addMappings = append(addMappings, codersdk.IDPSyncMapping[uuid.UUID]{
+ Given: claim,
+ Gets: orgID,
+ })
+ }
+ var removeMappings []codersdk.IDPSyncMapping[uuid.UUID]
+ for _, claim := range remove {
+ removeMappings = append(removeMappings, codersdk.IDPSyncMapping[uuid.UUID]{
+ Given: claim,
+ Gets: orgID,
+ })
+ }
+
+ _, err := r.Client.PatchOrganizationIDPSyncMapping(ctx, codersdk.PatchOrganizationIDPSyncMappingRequest{
+ Add: addMappings,
+ Remove: removeMappings,
+ })
+ if err != nil {
+ diags.AddError("Org Sync Update error", err.Error())
+ }
+
+ return diags
+}
diff --git a/internal/provider/organization_resource_test.go b/internal/provider/organization_resource_test.go
new file mode 100644
index 0000000..239f29d
--- /dev/null
+++ b/internal/provider/organization_resource_test.go
@@ -0,0 +1,219 @@
+package provider
+
+import (
+ "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) {
+ t.Parallel()
+ if os.Getenv("TF_ACC") == "" {
+ t.Skip("Acceptance tests are disabled.")
+ }
+
+ ctx := t.Context()
+ 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.OrgSyncIdpGroups = []string{"wibble", "wobble"}
+
+ cfg4 := cfg3
+ cfg4.OrgSyncIdpGroups = []string{"wibbley", "wobbley"}
+
+ cfg5 := cfg4
+ cfg5.GroupSync = ptr.Ref(codersdk.GroupSyncSettings{
+ Field: "wibble",
+ Mapping: map[string][]uuid.UUID{
+ "wibble": {uuid.MustParse("6e57187f-6543-46ab-a62c-a10065dd4314")},
+ },
+ })
+ cfg5.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 org sync
+ {
+ Config: cfg3.String(t),
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("org_sync_idp_groups").AtSliceIndex(0), knownvalue.StringExact("wibble")),
+ statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("org_sync_idp_groups").AtSliceIndex(1), knownvalue.StringExact("wobble")),
+ },
+ },
+ // Patch org sync
+ {
+ Config: cfg4.String(t),
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("org_sync_idp_groups").AtSliceIndex(0), knownvalue.StringExact("wibbley")),
+ statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("org_sync_idp_groups").AtSliceIndex(1), knownvalue.StringExact("wobbley")),
+ },
+ },
+ // Add group and role sync
+ {
+ Config: cfg5.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")),
+ },
+ },
+ },
+ })
+ })
+
+ t.Run("DefaultDisplayName", func(t *testing.T) {
+ cfg1 := testAccOrganizationResourceConfig{
+ URL: client.URL.String(),
+ Token: client.SessionToken(),
+ Name: ptr.Ref("example-org"),
+ Description: ptr.Ref("This is an example organization"),
+ Icon: ptr.Ref("/icon/coder.svg"),
+ }
+ resource.Test(t, resource.TestCase{
+ IsUnitTest: true,
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: cfg1.String(t),
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("coderd_organization.test", tfjsonpath.New("display_name"), knownvalue.StringExact("example-org")),
+ },
+ },
+ },
+ })
+ })
+}
+
+type testAccOrganizationResourceConfig struct {
+ URL string
+ Token string
+
+ Name *string
+ DisplayName *string
+ Description *string
+ Icon *string
+
+ OrgSyncIdpGroups []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 .OrgSyncIdpGroups}}
+ org_sync_idp_groups = [
+ {{- range $name := .OrgSyncIdpGroups }}
+ "{{$name}}",
+ {{- end}}
+ ]
+ {{- end}}
+
+ {{- 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/organization_sync_settings_resource.go b/internal/provider/organization_sync_settings_resource.go
new file mode 100644
index 0000000..d492d3d
--- /dev/null
+++ b/internal/provider/organization_sync_settings_resource.go
@@ -0,0 +1,258 @@
+package provider
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/google/uuid"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/diag"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+)
+
+// Ensure provider defined types fully satisfy framework interfaces.
+var _ resource.Resource = &OrganizationSyncSettingsResource{}
+
+type OrganizationSyncSettingsResource struct {
+ *CoderdProviderData
+}
+
+// OrganizationSyncSettingsResourceModel describes the resource data model.
+type OrganizationSyncSettingsResourceModel struct {
+ Field types.String `tfsdk:"field"`
+ AssignDefault types.Bool `tfsdk:"assign_default"`
+ Mapping types.Map `tfsdk:"mapping"`
+}
+
+func NewOrganizationSyncSettingsResource() resource.Resource {
+ return &OrganizationSyncSettingsResource{}
+}
+
+func (r *OrganizationSyncSettingsResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_organization_sync_settings"
+}
+
+func (r *OrganizationSyncSettingsResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: `IdP sync settings for organizations.
+
+This resource can only be created once. Attempts to create multiple will fail.
+
+~> **Warning**
+This resource is only compatible with Coder version [2.19.0](https://github.com/coder/coder/releases/tag/v2.19.0) and later.
+`,
+ Attributes: map[string]schema.Attribute{
+ "field": schema.StringAttribute{
+ Required: true,
+ MarkdownDescription: "The claim field that specifies what organizations " +
+ "a user should be in.",
+ Validators: []validator.String{
+ stringvalidator.LengthAtLeast(1),
+ },
+ },
+ "assign_default": schema.BoolAttribute{
+ Required: true,
+ MarkdownDescription: "When true, every user will be added to the default " +
+ "organization, regardless of claims.",
+ },
+ "mapping": schema.MapAttribute{
+ ElementType: types.ListType{ElemType: UUIDType},
+ Optional: true,
+ MarkdownDescription: "A map from OIDC group name to Coder organization ID.",
+ },
+ },
+ }
+}
+
+func (r *OrganizationSyncSettingsResource) 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 *OrganizationSyncSettingsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ // Read Terraform prior state data into the model
+ var data OrganizationSyncSettingsResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ settings, err := r.Client.OrganizationIDPSyncSettings(ctx)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("unable to get organization sync settings, got error: %s", err))
+ return
+ }
+
+ // Store the latest values that we just fetched.
+ data.Field = types.StringValue(settings.Field)
+ data.AssignDefault = types.BoolValue(settings.AssignDefault)
+
+ if !data.Mapping.IsNull() {
+ // Convert IDs to strings
+ elements := make(map[string][]string)
+ for key, ids := range settings.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
+ }
+ data.Mapping = mapping
+ }
+
+ // Save updated data into Terraform state
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *OrganizationSyncSettingsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ // Read Terraform plan data into the model
+ var data OrganizationSyncSettingsResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tflog.Trace(ctx, "creating organization sync", map[string]any{
+ "field": data.Field.ValueString(),
+ "assign_default": data.AssignDefault.ValueBool(),
+ })
+
+ // Create and Update use a shared implementation
+ resp.Diagnostics.Append(r.patch(ctx, data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tflog.Trace(ctx, "successfully created organization sync", map[string]any{
+ "field": data.Field.ValueString(),
+ "assign_default": data.AssignDefault.ValueBool(),
+ })
+
+ // Save data into Terraform state
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *OrganizationSyncSettingsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ // Read Terraform plan data into the model
+ var data OrganizationSyncSettingsResourceModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ // Update the organization metadata
+ tflog.Trace(ctx, "updating organization", map[string]any{
+ "field": data.Field.ValueString(),
+ "assign_default": data.AssignDefault.ValueBool(),
+ })
+
+ // Create and Update use a shared implementation
+ resp.Diagnostics.Append(r.patch(ctx, data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tflog.Trace(ctx, "successfully updated organization", map[string]any{
+ "field": data.Field.ValueString(),
+ "assign_default": data.AssignDefault.ValueBool(),
+ })
+
+ // Save updated data into Terraform state
+ resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
+
+func (r *OrganizationSyncSettingsResource) patch(
+ ctx context.Context,
+ data OrganizationSyncSettingsResourceModel,
+) diag.Diagnostics {
+ var diags diag.Diagnostics
+ field := data.Field.ValueString()
+ assignDefault := data.AssignDefault.ValueBool()
+
+ if data.Mapping.IsNull() {
+ _, err := r.Client.PatchOrganizationIDPSyncConfig(ctx, codersdk.PatchOrganizationIDPSyncConfigRequest{
+ Field: field,
+ AssignDefault: assignDefault,
+ })
+
+ if err != nil {
+ diags.AddError("failed to create organization sync", err.Error())
+ return diags
+ }
+ } else {
+ settings := codersdk.OrganizationSyncSettings{
+ Field: field,
+ AssignDefault: assignDefault,
+ Mapping: 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(data.Mapping.ElementsAs(ctx, &mapping, false)...)
+ if diags.HasError() {
+ return diags
+ }
+ for key, ids := range mapping {
+ for _, id := range ids {
+ settings.Mapping[key] = append(settings.Mapping[key], id.ValueUUID())
+ }
+ }
+
+ _, err := r.Client.PatchOrganizationIDPSyncSettings(ctx, settings)
+ if err != nil {
+ diags.AddError("failed to create organization sync", err.Error())
+ return diags
+ }
+ }
+
+ return diags
+}
+
+func (r *OrganizationSyncSettingsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ // Read Terraform prior state data into the model
+ var data OrganizationSyncSettingsResourceModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ tflog.Trace(ctx, "deleting organization sync", map[string]any{})
+ _, err := r.Client.PatchOrganizationIDPSyncConfig(ctx, codersdk.PatchOrganizationIDPSyncConfigRequest{
+ // This disables organization sync without causing state conflicts for
+ // organization resources that might still specify `org_sync_idp_groups`.
+ Field: "",
+ })
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("unable to delete organization sync, got error: %s", err))
+ return
+ }
+ tflog.Trace(ctx, "successfully deleted organization sync", map[string]any{})
+
+ // Read Terraform prior state data into the model
+ resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
+}
diff --git a/internal/provider/organization_sync_settings_resource_test.go b/internal/provider/organization_sync_settings_resource_test.go
new file mode 100644
index 0000000..ac7bf2a
--- /dev/null
+++ b/internal/provider/organization_sync_settings_resource_test.go
@@ -0,0 +1,125 @@
+package provider
+
+import (
+ "os"
+ "strings"
+ "testing"
+ "text/template"
+
+ "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 TestAccOrganizationSyncSettingsResource(t *testing.T) {
+ t.Parallel()
+ if os.Getenv("TF_ACC") == "" {
+ t.Skip("Acceptance tests are disabled.")
+ }
+
+ ctx := t.Context()
+ client := integration.StartCoder(ctx, t, "organization_sync_settings_acc", true)
+ _, err := client.User(ctx, codersdk.Me)
+ require.NoError(t, err)
+
+ cfg1 := testAccOrganizationSyncSettingsResourceConfig{
+ URL: client.URL.String(),
+ Token: client.SessionToken(),
+
+ Field: "wibble",
+ AssignDefault: true,
+ }
+
+ cfg2 := cfg1
+ cfg2.Field = "wobble"
+ cfg2.AssignDefault = false
+
+ cfg3 := cfg2
+ cfg3.Mapping = map[string][]uuid.UUID{
+ "wibble": {uuid.MustParse("151b5a4e-391a-464d-a88c-ac50f1458d6f")},
+ }
+
+ t.Run("CreateUpdateReadOk", 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_sync_settings.test", tfjsonpath.New("field"), knownvalue.StringExact("wibble")),
+ statecheck.ExpectKnownValue("coderd_organization_sync_settings.test", tfjsonpath.New("assign_default"), knownvalue.Bool(true)),
+ },
+ },
+ // Update and Read
+ {
+ Config: cfg2.String(t),
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("coderd_organization_sync_settings.test", tfjsonpath.New("field"), knownvalue.StringExact("wobble")),
+ statecheck.ExpectKnownValue("coderd_organization_sync_settings.test", tfjsonpath.New("assign_default"), knownvalue.Bool(false)),
+ },
+ },
+ // Add mapping
+ {
+ Config: cfg3.String(t),
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("coderd_organization_sync_settings.test", tfjsonpath.New("field"), knownvalue.StringExact("wobble")),
+ statecheck.ExpectKnownValue("coderd_organization_sync_settings.test", tfjsonpath.New("mapping").AtMapKey("wibble").AtSliceIndex(0), knownvalue.StringExact("151b5a4e-391a-464d-a88c-ac50f1458d6f")),
+ },
+ },
+ },
+ })
+ })
+}
+
+type testAccOrganizationSyncSettingsResourceConfig struct {
+ URL string
+ Token string
+
+ Field string
+ AssignDefault bool
+ Mapping map[string][]uuid.UUID
+}
+
+func (c testAccOrganizationSyncSettingsResourceConfig) String(t *testing.T) string {
+ t.Helper()
+ tpl := `
+provider coderd {
+ url = "{{.URL}}"
+ token = "{{.Token}}"
+}
+
+resource "coderd_organization_sync_settings" "test" {
+ field = "{{.Field}}"
+ assign_default = {{.AssignDefault}}
+
+ {{- if .Mapping}}
+ mapping = {
+ {{- range $key, $value := .Mapping}}
+ {{$key}} = [
+ {{- range $id := $value}}
+ "{{$id}}",
+ {{- end}}
+ ]
+ {{- end}}
+ }
+ {{- end}}
+}
+`
+ funcMap := template.FuncMap{}
+
+ buf := strings.Builder{}
+ tmpl, err := template.New("organizationSyncSettingsResource").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 651dcb4..8c65385 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -2,6 +2,7 @@ package provider
import (
"context"
+ "fmt"
"net/url"
"os"
"strings"
@@ -78,7 +79,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() {
@@ -102,7 +102,16 @@ func (p *CoderdProvider) Configure(ctx context.Context, req provider.ConfigureRe
data.Token = types.StringValue(tokenEnv)
}
- url, err := url.Parse(data.URL.ValueString())
+ rawURL := data.URL.ValueString()
+ if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
+ scheme := "https"
+ if strings.HasPrefix(rawURL, "localhost") {
+ scheme = "http"
+ }
+ rawURL = fmt.Sprintf("%s://%s", scheme, rawURL)
+ }
+
+ url, err := url.Parse(rawURL)
if err != nil {
resp.Diagnostics.AddError("url", "url is not a valid URL: "+err.Error())
return
@@ -138,6 +147,10 @@ func (p *CoderdProvider) Resources(ctx context.Context) []func() resource.Resour
NewGroupResource,
NewTemplateResource,
NewWorkspaceProxyResource,
+ NewLicenseResource,
+ NewOrganizationResource,
+ NewProvisionerKeyResource,
+ NewOrganizationSyncSettingsResource,
}
}
diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go
index 2f8b921..7dab7ff 100644
--- a/internal/provider/provider_test.go
+++ b/internal/provider/provider_test.go
@@ -16,6 +16,7 @@ var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServe
}
func testAccPreCheck(t *testing.T) {
+ t.Helper()
// You can add code here to run prior to any test case execution, for example assertions
// about the appropriate environment variables being set are common to see in a pre-check
// function.
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..3ca4757
--- /dev/null
+++ b/internal/provider/provisioner_key_resource_test.go
@@ -0,0 +1,114 @@
+package provider
+
+import (
+ "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) {
+ t.Parallel()
+ if os.Getenv("TF_ACC") == "" {
+ t.Skip("Acceptance tests are disabled.")
+ }
+ ctx := t.Context()
+ 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.go b/internal/provider/template_data_source.go
index 406ef33..3a5f59a 100644
--- a/internal/provider/template_data_source.go
+++ b/internal/provider/template_data_source.go
@@ -262,6 +262,11 @@ func (d *TemplateDataSource) Read(ctx context.Context, req datasource.ReadReques
template, err = client.TemplateByName(ctx, data.OrganizationID.ValueUUID(), data.Name.ValueString())
}
if err != nil {
+ if isNotFound(err) {
+ resp.Diagnostics.AddWarning("Client Warning", "Template not found. Marking as deleted.")
+ resp.State.RemoveResource(ctx)
+ return
+ }
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get template, got error: %s", err))
return
}
diff --git a/internal/provider/template_data_source_test.go b/internal/provider/template_data_source_test.go
index 79c1f80..91f9368 100644
--- a/internal/provider/template_data_source_test.go
+++ b/internal/provider/template_data_source_test.go
@@ -1,7 +1,6 @@
package provider
import (
- "context"
"os"
"regexp"
"strconv"
@@ -14,21 +13,23 @@ 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"
)
func TestAccTemplateDataSource(t *testing.T) {
+ t.Parallel()
if os.Getenv("TF_ACC") == "" {
t.Skip("Acceptance tests are disabled.")
}
- ctx := context.Background()
+ ctx := t.Context()
client := integration.StartCoder(ctx, t, "template_data_acc", true)
firstUser, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
orgID := firstUser.OrganizationIDs[0]
- version, err := newVersion(ctx, client, newVersionRequest{
+ version, err, _ := newVersion(ctx, client, newVersionRequest{
OrganizationID: orgID,
Version: &TemplateVersion{
Name: types.StringValue("main"),
@@ -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 9255f2f..b95d036 100644
--- a/internal/provider/template_resource.go
+++ b/internal/provider/template_resource.go
@@ -6,11 +6,14 @@ import (
"encoding/json"
"fmt"
"io"
+ "slices"
"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"
@@ -68,6 +71,7 @@ type TemplateResourceModel struct {
TimeTilDormantAutoDeleteMillis types.Int64 `tfsdk:"time_til_dormant_autodelete_ms"`
RequireActiveVersion types.Bool `tfsdk:"require_active_version"`
DeprecationMessage types.String `tfsdk:"deprecation_message"`
+ MaxPortShareLevel types.String `tfsdk:"max_port_share_level"`
// If null, we are not managing ACL via Terraform (such as for AGPL).
ACL types.Object `tfsdk:"acl"`
@@ -91,7 +95,9 @@ func (m *TemplateResourceModel) EqualTemplateMetadata(other *TemplateResourceMod
m.FailureTTLMillis.Equal(other.FailureTTLMillis) &&
m.TimeTilDormantMillis.Equal(other.TimeTilDormantMillis) &&
m.TimeTilDormantAutoDeleteMillis.Equal(other.TimeTilDormantAutoDeleteMillis) &&
- m.RequireActiveVersion.Equal(other.RequireActiveVersion)
+ m.RequireActiveVersion.Equal(other.RequireActiveVersion) &&
+ m.DeprecationMessage.Equal(other.DeprecationMessage) &&
+ m.MaxPortShareLevel.Equal(other.MaxPortShareLevel)
}
func (m *TemplateResourceModel) CheckEntitlements(ctx context.Context, features map[codersdk.FeatureName]codersdk.Feature) (diags diag.Diagnostics) {
@@ -109,7 +115,8 @@ func (m *TemplateResourceModel) CheckEntitlements(ctx context.Context, features
len(m.AutostartPermittedDaysOfWeek.Elements()) != 7
requiresActiveVersion := m.RequireActiveVersion.ValueBool()
requiresACL := !m.ACL.IsNull()
- if requiresScheduling || requiresActiveVersion || requiresACL {
+ requiresSharedPortsControl := m.MaxPortShareLevel.ValueString() != "" && m.MaxPortShareLevel.ValueString() != string(codersdk.WorkspaceAgentPortShareLevelPublic)
+ if requiresScheduling || requiresActiveVersion || requiresACL || requiresSharedPortsControl {
if requiresScheduling && !features[codersdk.FeatureAdvancedTemplateScheduling].Enabled {
diags.AddError(
"Feature not enabled",
@@ -131,6 +138,13 @@ func (m *TemplateResourceModel) CheckEntitlements(ctx context.Context, features
)
return
}
+ if requiresSharedPortsControl && !features[codersdk.FeatureControlSharedPorts].Enabled {
+ diags.AddError(
+ "Feature not enabled",
+ "Your license is not entitled to use port sharing control, so you cannot set max_port_share_level.",
+ )
+ return
+ }
}
return
}
@@ -230,10 +244,8 @@ func (r *TemplateResource) Metadata(ctx context.Context, req resource.MetadataRe
func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
- MarkdownDescription: "A Coder template.\n\nLogs from building template versions are streamed from the provisioner " +
- "when the `TF_LOG` environment variable is `INFO` or higher.\n\n" +
- "When importing, the ID supplied can be either a template UUID retrieved via the API or `/`.",
-
+ MarkdownDescription: "A Coder template.\n\nLogs from building template versions can be optionally streamed from the provisioner " +
+ "by setting the `TF_LOG` environment variable to `INFO` or higher.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
MarkdownDescription: "The ID of the template.",
@@ -247,13 +259,16 @@ 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),
+ codersdkvalidator.Name(),
},
},
"display_name": schema.StringAttribute{
MarkdownDescription: "The display name of the template. Defaults to the template name.",
Optional: true,
Computed: true,
+ Validators: []validator.String{
+ codersdkvalidator.DisplayName(),
+ },
},
"description": schema.StringAttribute{
MarkdownDescription: "A description of the template.",
@@ -271,7 +286,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
},
},
"icon": schema.StringAttribute{
- MarkdownDescription: "Relative path or external URL that specifes an icon to be displayed in the dashboard.",
+ MarkdownDescription: "Relative path or external URL that specifies an icon to be displayed in the dashboard.",
Optional: true,
Computed: true,
Default: stringdefault.StaticString(""),
@@ -334,7 +349,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
Default: booldefault.StaticBool(true),
},
"allow_user_auto_stop": schema.BoolAttribute{
- MarkdownDescription: "(Enterprise) Whether users can auto-start workspaces created from this template. Defaults to true.",
+ MarkdownDescription: "(Enterprise) Whether users can auto-stop workspaces created from this template. Defaults to true.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
@@ -363,6 +378,14 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
Computed: true,
Default: booldefault.StaticBool(false),
},
+ "max_port_share_level": schema.StringAttribute{
+ MarkdownDescription: "(Enterprise) The maximum port share level for workspaces created from this template. Defaults to `owner` on an Enterprise deployment, or `public` otherwise.",
+ Optional: true,
+ Computed: true,
+ Validators: []validator.String{
+ stringvalidator.OneOfCaseInsensitive(string(codersdk.WorkspaceAgentPortShareLevelAuthenticated), string(codersdk.WorkspaceAgentPortShareLevelOwner), string(codersdk.WorkspaceAgentPortShareLevelPublic)),
+ },
+ },
"deprecation_message": schema.StringAttribute{
MarkdownDescription: "If set, the template will be marked as deprecated with the provided message and users will be blocked from creating new workspaces from it. Does nothing if set when the resource is created.",
Optional: true,
@@ -381,7 +404,7 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
Required: true,
Validators: []validator.List{
listvalidator.SizeAtLeast(1),
- NewActiveVersionValidator(),
+ NewVersionsValidator(),
},
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
@@ -390,11 +413,11 @@ func (r *TemplateResource) Schema(ctx context.Context, req resource.SchemaReques
Computed: true,
},
"name": schema.StringAttribute{
- MarkdownDescription: "The name of the template version. Automatically generated if not provided. If provided, the name *must* change each time the directory contents are updated.",
+ MarkdownDescription: "The name of the template version. Automatically generated if not provided. If provided, the name *must* change each time the directory contents, or the `tf_vars` attribute are updated.",
Optional: true,
Computed: true,
Validators: []validator.String{
- stringvalidator.LengthAtLeast(1),
+ codersdkvalidator.TemplateVersionName(),
},
},
"message": schema.StringAttribute{
@@ -489,9 +512,9 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
if idx > 0 {
newVersionRequest.TemplateID = &templateResp.ID
}
- versionResp, err := newVersion(ctx, client, newVersionRequest)
+ versionResp, err, logs := newVersion(ctx, client, newVersionRequest)
if err != nil {
- resp.Diagnostics.AddError("Client Error", err.Error())
+ resp.Diagnostics.AddError("Provisioner Error", formatLogs(err, logs))
return
}
if idx == 0 {
@@ -525,7 +548,7 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
if resp.Diagnostics.HasError() {
return
}
- err = client.UpdateTemplateACL(ctx, templateResp.ID, convertACLToRequest(acl))
+ err = client.UpdateTemplateACL(ctx, templateResp.ID, convertACLToRequest(codersdk.TemplateACL{}, acl))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to create template ACL: %s", err))
return
@@ -546,6 +569,25 @@ func (r *TemplateResource) Create(ctx context.Context, req resource.CreateReques
data.ID = UUIDValue(templateResp.ID)
data.DisplayName = types.StringValue(templateResp.DisplayName)
+ // TODO: Remove this update call once this provider requires a Coder
+ // deployment running `v2.15.0` or later.
+ if data.MaxPortShareLevel.IsUnknown() {
+ data.MaxPortShareLevel = types.StringValue(string(templateResp.MaxPortShareLevel))
+ } else if data.MaxPortShareLevel.ValueString() == string(templateResp.MaxPortShareLevel) {
+ tflog.Info(ctx, "max port share level set to default, not updating")
+ } else {
+ mpslReq := data.toUpdateRequest(ctx, &resp.Diagnostics)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+ mpslResp, err := client.UpdateTemplateMeta(ctx, data.ID.ValueUUID(), *mpslReq)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to set max port share level via update: %s", err))
+ return
+ }
+ data.MaxPortShareLevel = types.StringValue(string(mpslResp.MaxPortShareLevel))
+ }
+
resp.Diagnostics.Append(data.Versions.setPrivateState(ctx, resp.Private)...)
if resp.Diagnostics.HasError() {
return
@@ -570,6 +612,11 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r
template, err := client.Template(ctx, templateID)
if err != nil {
+ if isNotFound(err) {
+ resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("Template with ID %s not found. Marking as deleted.", templateID.String()))
+ resp.State.RemoveResource(ctx)
+ return
+ }
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to get template: %s", err))
return
}
@@ -579,6 +626,7 @@ func (r *TemplateResource) Read(ctx context.Context, req resource.ReadRequest, r
resp.Diagnostics.Append(diag...)
return
}
+ data.MaxPortShareLevel = types.StringValue(string(template.MaxPortShareLevel))
if !data.ACL.IsNull() {
tflog.Info(ctx, "reading template ACL")
@@ -653,11 +701,16 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
client := r.data.Client
+ // TODO(ethanndickson): Remove this once the provider requires a Coder
+ // deployment running `v2.15.0` or later.
+ if newState.MaxPortShareLevel.IsUnknown() {
+ newState.MaxPortShareLevel = curState.MaxPortShareLevel
+ }
templateMetadataChanged := !newState.EqualTemplateMetadata(&curState)
// This is required, as the API will reject no-diff updates.
if templateMetadataChanged {
tflog.Info(ctx, "change in template metadata detected, updating.")
- updateReq := newState.toUpdateRequest(ctx, resp)
+ updateReq := newState.toUpdateRequest(ctx, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@@ -678,7 +731,13 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
if resp.Diagnostics.HasError() {
return
}
- err := client.UpdateTemplateACL(ctx, templateID, convertACLToRequest(acl))
+ curACL, err := client.TemplateACL(ctx, templateID)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to get template ACL: %s", err))
+ return
+ }
+
+ err = client.UpdateTemplateACL(ctx, templateID, convertACLToRequest(curACL, acl))
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to update template ACL: %s", err))
return
@@ -689,13 +748,13 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
for idx := range newState.Versions {
if newState.Versions[idx].ID.IsUnknown() {
tflog.Info(ctx, "discovered a new or modified template version")
- uploadResp, err := newVersion(ctx, client, newVersionRequest{
+ uploadResp, err, logs := newVersion(ctx, client, newVersionRequest{
Version: &newState.Versions[idx],
OrganizationID: orgID,
TemplateID: &templateID,
})
if err != nil {
- resp.Diagnostics.AddError("Client Error", err.Error())
+ resp.Diagnostics.AddError("Provisioner Error", formatLogs(err, logs))
return
}
versionResp, err := client.TemplateVersion(ctx, uploadResp.ID)
@@ -740,6 +799,14 @@ func (r *TemplateResource) Update(ctx context.Context, req resource.UpdateReques
}
}
}
+ // TODO(ethanndickson): Remove this once the provider requires a Coder
+ // deployment running `v2.15.0` or later.
+ templateResp, err := client.Template(ctx, templateID)
+ if err != nil {
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to get template: %s", err))
+ return
+ }
+ newState.MaxPortShareLevel = types.StringValue(string(templateResp.MaxPortShareLevel))
resp.Diagnostics.Append(newState.Versions.setPrivateState(ctx, resp.Private)...)
if resp.Diagnostics.HasError() {
@@ -802,60 +869,66 @@ func (r *TemplateResource) ConfigValidators(context.Context) []resource.ConfigVa
return []resource.ConfigValidator{}
}
-type activeVersionValidator struct{}
+type versionsValidator struct{}
-func NewActiveVersionValidator() validator.List {
- return &activeVersionValidator{}
+func NewVersionsValidator() validator.List {
+ return &versionsValidator{}
}
// Description implements validator.List.
-func (a *activeVersionValidator) Description(ctx context.Context) string {
+func (a *versionsValidator) Description(ctx context.Context) string {
return a.MarkdownDescription(ctx)
}
// MarkdownDescription implements validator.List.
-func (a *activeVersionValidator) MarkdownDescription(context.Context) string {
- return "Validate that exactly one template version has active set to true."
+func (a *versionsValidator) MarkdownDescription(context.Context) string {
+ return "Validate that template version names are unique and that at most one version is active."
}
// ValidateList implements validator.List.
-func (a *activeVersionValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
+func (a *versionsValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
+ if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
+ return
+ }
+
var data []TemplateVersion
resp.Diagnostics.Append(req.ConfigValue.ElementsAs(ctx, &data, false)...)
if resp.Diagnostics.HasError() {
return
}
- // Check if only one item in Version has active set to true
- active := false
- for _, version := range data {
- if version.Active.ValueBool() {
- if active {
- resp.Diagnostics.AddError("Client Error", "Only one template version can be active at a time.")
- return
- }
- active = true
- }
- }
- if !active {
- resp.Diagnostics.AddError("Client Error", "At least one template version must be active.")
- }
-
// Check all versions have unique names
uniqueNames := make(map[string]struct{})
for _, version := range data {
- if version.Name.IsNull() {
+ if version.Name.IsNull() || version.Name.IsUnknown() {
continue
}
if _, ok := uniqueNames[version.Name.ValueString()]; ok {
- resp.Diagnostics.AddError("Client Error", "Template version names must be unique.")
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Template version names must be unique. `%s` appears twice.", version.Name.ValueString()))
return
}
uniqueNames[version.Name.ValueString()] = struct{}{}
}
+
+ // Ensure at most one version is active
+ active := false
+ for _, version := range data {
+ // `active` defaults to false, so if it's null or unknown, this is Terraform
+ // requesting an early validation.
+ if version.Active.IsNull() || version.Active.IsUnknown() {
+ continue
+ }
+ if version.Active.ValueBool() {
+ if active {
+ resp.Diagnostics.AddError("Client Error", "Only one template version can be active at a time.")
+ return
+ }
+ active = true
+ }
+ }
}
-var _ validator.List = &activeVersionValidator{}
+var _ validator.List = &versionsValidator{}
type versionsPlanModifier struct{}
@@ -882,6 +955,12 @@ func (d *versionsPlanModifier) PlanModifyList(ctx context.Context, req planmodif
return
}
+ hasActiveVersion, diag := hasOneActiveVersion(configVersions)
+ if diag.HasError() {
+ resp.Diagnostics.Append(diag...)
+ return
+ }
+
for i := range planVersions {
hash, err := computeDirectoryHash(planVersions[i].Directory.ValueString())
if err != nil {
@@ -900,6 +979,13 @@ func (d *versionsPlanModifier) PlanModifyList(ctx context.Context, req planmodif
// If this is the first read, init the private state value
if lvBytes == nil {
lv = make(LastVersionsByHash)
+ // If there's no prior private state, this might be resource creation,
+ // in which case one version must be active.
+ if !hasActiveVersion {
+ resp.Diagnostics.AddError("Client Error", "At least one template version must be active when creating a"+
+ " `coderd_template` resource.\n(Subsequent resource updates can be made without an active template in the list).")
+ return
+ }
} else {
err := json.Unmarshal(lvBytes, &lv)
if err != nil {
@@ -908,9 +994,34 @@ func (d *versionsPlanModifier) PlanModifyList(ctx context.Context, req planmodif
}
}
- planVersions.reconcileVersionIDs(lv, configVersions)
+ diag = planVersions.reconcileVersionIDs(lv, configVersions, hasActiveVersion)
+ if diag.HasError() {
+ resp.Diagnostics.Append(diag...)
+ return
+ }
- resp.PlanValue, resp.Diagnostics = types.ListValueFrom(ctx, req.PlanValue.ElementType(ctx), planVersions)
+ resp.PlanValue, diag = types.ListValueFrom(ctx, req.PlanValue.ElementType(ctx), planVersions)
+ if diag.HasError() {
+ resp.Diagnostics.Append(diag...)
+ }
+}
+
+func hasOneActiveVersion(data Versions) (hasActiveVersion bool, diags diag.Diagnostics) {
+ active := false
+ for _, version := range data {
+ if version.Active.IsNull() || version.Active.IsUnknown() {
+ // If null or unknown, the value will be defaulted to false
+ continue
+ }
+ if version.Active.ValueBool() {
+ if active {
+ diags.AddError("Client Error", "Only one template version can be active at a time.")
+ return
+ }
+ active = true
+ }
+ }
+ return active, diags
}
func NewVersionsPlanModifier() planmodifier.List {
@@ -938,13 +1049,14 @@ func uploadDirectory(ctx context.Context, client *codersdk.Client, logger slog.L
return &resp, nil
}
-func waitForJob(ctx context.Context, client *codersdk.Client, version *codersdk.TemplateVersion) error {
+func waitForJob(ctx context.Context, client *codersdk.Client, version *codersdk.TemplateVersion) ([]codersdk.ProvisionerJobLog, error) {
const maxRetries = 3
+ var jobLogs []codersdk.ProvisionerJobLog
for retries := 0; retries < maxRetries; retries++ {
logs, closer, err := client.TemplateVersionLogsAfter(ctx, version.ID, 0)
defer closer.Close()
if err != nil {
- return fmt.Errorf("begin streaming logs: %w", err)
+ return jobLogs, fmt.Errorf("begin streaming logs: %w", err)
}
for {
logs, ok := <-logs
@@ -958,21 +1070,24 @@ func waitForJob(ctx context.Context, client *codersdk.Client, version *codersdk.
"level": logs.Level,
"created_at": logs.CreatedAt,
})
+ if logs.Output != "" {
+ jobLogs = append(jobLogs, logs)
+ }
}
latestResp, err := client.TemplateVersion(ctx, version.ID)
if err != nil {
- return err
+ return jobLogs, err
}
if latestResp.Job.Status.Active() {
tflog.Warn(ctx, fmt.Sprintf("provisioner job still active, continuing to wait...: %s", latestResp.Job.Status))
continue
}
if latestResp.Job.Status != codersdk.ProvisionerJobSucceeded {
- return fmt.Errorf("provisioner job did not succeed: %s (%s)", latestResp.Job.Status, latestResp.Job.Error)
+ return jobLogs, fmt.Errorf("provisioner job did not succeed: %s (%s)", latestResp.Job.Status, latestResp.Job.Error)
}
- return nil
+ return jobLogs, nil
}
- return fmt.Errorf("provisioner job did not complete after %d retries", maxRetries)
+ return jobLogs, fmt.Errorf("provisioner job did not complete after %d retries", maxRetries)
}
type newVersionRequest struct {
@@ -981,22 +1096,23 @@ type newVersionRequest struct {
TemplateID *uuid.UUID
}
-func newVersion(ctx context.Context, client *codersdk.Client, req newVersionRequest) (*codersdk.TemplateVersion, error) {
+func newVersion(ctx context.Context, client *codersdk.Client, req newVersionRequest) (*codersdk.TemplateVersion, error, []codersdk.ProvisionerJobLog) {
+ var logs []codersdk.ProvisionerJobLog
directory := req.Version.Directory.ValueString()
tflog.Info(ctx, "uploading directory")
uploadResp, err := uploadDirectory(ctx, client, slog.Make(newTFLogSink(ctx)), directory)
if err != nil {
- return nil, fmt.Errorf("failed to upload directory: %s", err)
+ return nil, fmt.Errorf("failed to upload directory: %s", err), logs
}
tflog.Info(ctx, "successfully uploaded directory")
tflog.Info(ctx, "discovering and parsing vars files")
varFiles, err := codersdk.DiscoverVarsFiles(directory)
if err != nil {
- return nil, fmt.Errorf("failed to discover vars files: %s", err)
+ return nil, fmt.Errorf("failed to discover vars files: %s", err), logs
}
vars, err := codersdk.ParseUserVariableValues(varFiles, "", []string{})
if err != nil {
- return nil, fmt.Errorf("failed to parse user variable values: %s", err)
+ return nil, fmt.Errorf("failed to parse user variable values: %s", err), logs
}
tflog.Info(ctx, "discovered and parsed vars files", map[string]any{
"vars": vars,
@@ -1007,6 +1123,10 @@ func newVersion(ctx context.Context, client *codersdk.Client, req newVersionRequ
Value: variable.Value.ValueString(),
})
}
+ provTags := make(map[string]string, len(req.Version.ProvisionerTags))
+ for _, provisionerTag := range req.Version.ProvisionerTags {
+ provTags[provisionerTag.Name.ValueString()] = provisionerTag.Value.ValueString()
+ }
tmplVerReq := codersdk.CreateTemplateVersionRequest{
Name: req.Version.Name.ValueString(),
Message: req.Version.Message.ValueString(),
@@ -1014,6 +1134,7 @@ func newVersion(ctx context.Context, client *codersdk.Client, req newVersionRequ
Provisioner: codersdk.ProvisionerTypeTerraform,
FileID: uploadResp.ID,
UserVariableValues: vars,
+ ProvisionerTags: provTags,
}
if req.TemplateID != nil {
tmplVerReq.TemplateID = *req.TemplateID
@@ -1021,15 +1142,15 @@ func newVersion(ctx context.Context, client *codersdk.Client, req newVersionRequ
tflog.Info(ctx, "creating template version")
versionResp, err := client.CreateTemplateVersion(ctx, req.OrganizationID, tmplVerReq)
if err != nil {
- return nil, fmt.Errorf("failed to create template version: %s", err)
+ return nil, fmt.Errorf("failed to create template version: %s", err), logs
}
tflog.Info(ctx, "waiting for template version import job.")
- err = waitForJob(ctx, client, &versionResp)
+ logs, err = waitForJob(ctx, client, &versionResp)
if err != nil {
- return nil, fmt.Errorf("failed to wait for job: %s", err)
+ return nil, fmt.Errorf("failed to wait for job: %s", err), logs
}
tflog.Info(ctx, "successfully created template version")
- return &versionResp, nil
+ return &versionResp, nil, logs
}
func markActive(ctx context.Context, client *codersdk.Client, templateID uuid.UUID, versionID uuid.UUID) error {
@@ -1041,21 +1162,33 @@ func markActive(ctx context.Context, client *codersdk.Client, templateID uuid.UU
ID: versionID,
})
if err != nil {
- return fmt.Errorf("Failed to update active template version: %s", err)
+ return fmt.Errorf("failed to update active template version: %s", err)
}
tflog.Info(ctx, "marked template version as active")
return nil
}
-func convertACLToRequest(permissions ACL) codersdk.UpdateTemplateACL {
+func convertACLToRequest(curACL codersdk.TemplateACL, newACL ACL) codersdk.UpdateTemplateACL {
var userPerms = make(map[string]codersdk.TemplateRole)
- for _, perm := range permissions.UserPermissions {
+ for _, perm := range newACL.UserPermissions {
userPerms[perm.ID.ValueString()] = codersdk.TemplateRole(perm.Role.ValueString())
}
var groupPerms = make(map[string]codersdk.TemplateRole)
- for _, perm := range permissions.GroupPermissions {
+ for _, perm := range newACL.GroupPermissions {
groupPerms[perm.ID.ValueString()] = codersdk.TemplateRole(perm.Role.ValueString())
}
+ // For each user or group to remove, we need to set their role to empty
+ // string.
+ for _, perm := range curACL.Users {
+ if _, ok := userPerms[perm.ID.String()]; !ok {
+ userPerms[perm.ID.String()] = ""
+ }
+ }
+ for _, perm := range curACL.Groups {
+ if _, ok := groupPerms[perm.ID.String()]; !ok {
+ groupPerms[perm.ID.String()] = ""
+ }
+ }
return codersdk.UpdateTemplateACL{
UserPerms: userPerms,
GroupPerms: groupPerms,
@@ -1112,25 +1245,27 @@ func (r *TemplateResourceModel) readResponse(ctx context.Context, template *code
r.TimeTilDormantAutoDeleteMillis = types.Int64Value(template.TimeTilDormantAutoDeleteMillis)
r.RequireActiveVersion = types.BoolValue(template.RequireActiveVersion)
r.DeprecationMessage = types.StringValue(template.DeprecationMessage)
+ // TODO(ethanndickson): MaxPortShareLevel deliberately omitted, as it can't
+ // be set during a create request, and we call this during `Create`.
return nil
}
-func (r *TemplateResourceModel) toUpdateRequest(ctx context.Context, resp *resource.UpdateResponse) *codersdk.UpdateTemplateMeta {
+func (r *TemplateResourceModel) toUpdateRequest(ctx context.Context, diag *diag.Diagnostics) *codersdk.UpdateTemplateMeta {
var days []string
- resp.Diagnostics.Append(
+ diag.Append(
r.AutostartPermittedDaysOfWeek.ElementsAs(ctx, &days, false)...,
)
- if resp.Diagnostics.HasError() {
+ if diag.HasError() {
return nil
}
autoStart := &codersdk.TemplateAutostartRequirement{
DaysOfWeek: days,
}
var reqs AutostopRequirement
- resp.Diagnostics.Append(
+ diag.Append(
r.AutostopRequirement.As(ctx, &reqs, basetypes.ObjectAsOptions{})...,
)
- if resp.Diagnostics.HasError() {
+ if diag.HasError() {
return nil
}
autoStop := &codersdk.TemplateAutostopRequirement{
@@ -1154,6 +1289,7 @@ func (r *TemplateResourceModel) toUpdateRequest(ctx context.Context, resp *resou
TimeTilDormantAutoDeleteMillis: r.TimeTilDormantAutoDeleteMillis.ValueInt64(),
RequireActiveVersion: r.RequireActiveVersion.ValueBool(),
DeprecationMessage: r.DeprecationMessage.ValueStringPointer(),
+ MaxPortShareLevel: ptr.Ref(codersdk.WorkspaceAgentPortShareLevel(r.MaxPortShareLevel.ValueString())),
// If we're managing ACL, we want to delete the everyone group
DisableEveryoneGroupAccess: !r.ACL.IsNull(),
}
@@ -1207,8 +1343,10 @@ type LastVersionsByHash = map[string][]PreviousTemplateVersion
var LastVersionsKey = "last_versions"
type PreviousTemplateVersion struct {
- ID uuid.UUID `json:"id"`
- Name string `json:"name"`
+ ID uuid.UUID `json:"id"`
+ Name string `json:"name"`
+ TFVars map[string]string `json:"tf_vars"`
+ Active bool `json:"active"`
}
type privateState interface {
@@ -1220,18 +1358,26 @@ func (v Versions) setPrivateState(ctx context.Context, ps privateState) (diags d
lv := make(LastVersionsByHash)
for _, version := range v {
vbh, ok := lv[version.DirectoryHash.ValueString()]
+ tfVars := make(map[string]string, len(version.TerraformVariables))
+ for _, tfVar := range version.TerraformVariables {
+ tfVars[tfVar.Name.ValueString()] = tfVar.Value.ValueString()
+ }
// Store the IDs and names of all versions with the same directory hash,
// in the order they appear
if ok {
lv[version.DirectoryHash.ValueString()] = append(vbh, PreviousTemplateVersion{
- ID: version.ID.ValueUUID(),
- Name: version.Name.ValueString(),
+ ID: version.ID.ValueUUID(),
+ Name: version.Name.ValueString(),
+ TFVars: tfVars,
+ Active: version.Active.ValueBool(),
})
} else {
lv[version.DirectoryHash.ValueString()] = []PreviousTemplateVersion{
{
- ID: version.ID.ValueUUID(),
- Name: version.Name.ValueString(),
+ ID: version.ID.ValueUUID(),
+ Name: version.Name.ValueString(),
+ TFVars: tfVars,
+ Active: version.Active.ValueBool(),
},
}
}
@@ -1244,7 +1390,14 @@ func (v Versions) setPrivateState(ctx context.Context, ps privateState) (diags d
return ps.SetKey(ctx, LastVersionsKey, lvBytes)
}
-func (planVersions Versions) reconcileVersionIDs(lv LastVersionsByHash, configVersions Versions) {
+func (planVersions Versions) reconcileVersionIDs(lv LastVersionsByHash, configVersions Versions, hasOneActiveVersion bool) (diag diag.Diagnostics) {
+ // We remove versions that we've matched from `lv`, so make a copy for
+ // resolving tfvar changes at the end.
+ fullLv := make(LastVersionsByHash)
+ for k, v := range lv {
+ fullLv[k] = slices.Clone(v)
+ }
+
for i := range planVersions {
prevList, ok := lv[planVersions[i].DirectoryHash.ValueString()]
// If not in state, mark as known after apply since we'll create a new version.
@@ -1284,4 +1437,92 @@ func (planVersions Versions) reconcileVersionIDs(lv LastVersionsByHash, configVe
lv[planVersions[i].DirectoryHash.ValueString()] = prevList[1:]
}
}
+
+ // If only the Terraform variables have changed,
+ // we need to create a new version with the new variables.
+ for i := range planVersions {
+ if !planVersions[i].ID.IsUnknown() {
+ prevs, ok := fullLv[planVersions[i].DirectoryHash.ValueString()]
+ if !ok {
+ continue
+ }
+ if tfVariablesChanged(prevs, &planVersions[i]) {
+ planVersions[i].ID = NewUUIDUnknown()
+ // We could always set the name to unknown here, to generate a
+ // random one (this is what the Web UI currently does when
+ // only updating tfvars).
+ // However, I think it'd be weird if the provider just started
+ // ignoring the name you set in the config, we'll instead
+ // require that users update the name if they update the tfvars.
+ if configVersions[i].Name.IsNull() {
+ planVersions[i].Name = types.StringUnknown()
+ }
+ }
+ }
+ }
+
+ // If a version was deactivated, and no active version was set, we need to
+ // return an error to avoid a post-apply plan being non-empty.
+ if !hasOneActiveVersion {
+ for i := range planVersions {
+ if !planVersions[i].ID.IsUnknown() {
+ prevs, ok := fullLv[planVersions[i].DirectoryHash.ValueString()]
+ if !ok {
+ continue
+ }
+ if versionDeactivated(prevs, &planVersions[i]) {
+ diag.AddError("Client Error", "Plan could not determine which version should be active.\n"+
+ "Either specify an active version or modify the contents of the previously active version before marking it as inactive.")
+ return diag
+ }
+ }
+ }
+ }
+ return diag
+}
+
+func versionDeactivated(prevs []PreviousTemplateVersion, planned *TemplateVersion) bool {
+ for _, prev := range prevs {
+ if prev.ID == planned.ID.ValueUUID() {
+ if prev.Active &&
+ !planned.Active.IsNull() &&
+ !planned.Active.IsUnknown() &&
+ !planned.Active.ValueBool() {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func tfVariablesChanged(prevs []PreviousTemplateVersion, planned *TemplateVersion) bool {
+ for _, prev := range prevs {
+ if prev.ID == planned.ID.ValueUUID() {
+ // If the previous version has no TFVars, then it was created using
+ // an older provider version.
+ if prev.TFVars == nil {
+ return true
+ }
+ for _, tfVar := range planned.TerraformVariables {
+ if prev.TFVars[tfVar.Name.ValueString()] != tfVar.Value.ValueString() {
+ return true
+ }
+ }
+ return false
+ }
+ }
+ return true
+
+}
+
+func formatLogs(err error, logs []codersdk.ProvisionerJobLog) string {
+ var b strings.Builder
+ b.WriteString(err.Error() + "\n")
+ for _, log := range logs {
+ if !log.CreatedAt.IsZero() {
+ b.WriteString(log.CreatedAt.Local().Format("2006-01-02 15:04:05.000Z07:00") + " ")
+ }
+ b.WriteString(log.Output + "\n")
+ }
+ return b.String()
}
diff --git a/internal/provider/template_resource_test.go b/internal/provider/template_resource_test.go
index b8b7f19..f56ff3b 100644
--- a/internal/provider/template_resource_test.go
+++ b/internal/provider/template_resource_test.go
@@ -17,15 +17,17 @@ 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"
)
func TestAccTemplateResource(t *testing.T) {
+ t.Parallel()
if os.Getenv("TF_ACC") == "" {
t.Skip("Acceptance tests are disabled.")
}
- ctx := context.Background()
+ ctx := t.Context()
client := integration.StartCoder(ctx, t, "template_acc", false)
firstUser, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
@@ -42,12 +44,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 +59,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)
@@ -113,6 +115,7 @@ func TestAccTemplateResource(t *testing.T) {
resource.TestCheckResourceAttr("coderd_template.test", "time_til_dormant_ms", "0"),
resource.TestCheckResourceAttr("coderd_template.test", "time_til_dormant_autodelete_ms", "0"),
resource.TestCheckResourceAttr("coderd_template.test", "require_active_version", "false"),
+ resource.TestCheckResourceAttr("coderd_template.test", "max_port_share_level", "public"),
resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "versions.*", map[string]*regexp.Regexp{
"name": regexp.MustCompile(".+"),
"id": regexp.MustCompile(".+"),
@@ -239,28 +242,29 @@ 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: &exTemplateTwo,
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: &exTemplateTwo,
TerraformVariables: []testAccTemplateKeyValueConfig{
{
- Key: PtrTo("name"),
- Value: PtrTo("world"),
+ Key: ptr.Ref("name"),
+ Value: ptr.Ref("world"),
},
},
+ Active: ptr.Ref(false),
},
},
ACL: testAccTemplateACLConfig{
@@ -270,21 +274,30 @@ 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 = &exTemplateOne
cfg5 := cfg4
cfg5.Versions = slices.Clone(cfg5.Versions)
- cfg5.Versions[1].Directory = PtrTo("../../integration/template-test/example-template/")
+ cfg5.Versions[1].Directory = &exTemplateOne
+
+ cfg6 := cfg5
+ cfg6.Versions = slices.Clone(cfg6.Versions)
+ cfg6.Versions[0].TerraformVariables = []testAccTemplateKeyValueConfig{
+ {
+ Key: ptr.Ref("name"),
+ Value: ptr.Ref("world2"),
+ },
+ }
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
@@ -343,177 +356,523 @@ func TestAccTemplateResource(t *testing.T) {
testAccCheckNumTemplateVersions(ctx, client, 4),
),
},
+ // Update the Terraform variables of the first version
+ {
+ Config: cfg6.String(t),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ testAccCheckNumTemplateVersions(ctx, client, 5),
+ ),
+ },
+ },
+ })
+ })
+
+ t.Run("AutoGenNameUpdateTFVars", func(t *testing.T) {
+ cfg1 := testAccTemplateResourceConfig{
+ URL: client.URL.String(),
+ Token: client.SessionToken(),
+ Name: ptr.Ref("example-template3"),
+ Versions: []testAccTemplateVersionConfig{
+ {
+ // Auto-generated version name
+ Directory: &exTemplateTwo,
+ TerraformVariables: []testAccTemplateKeyValueConfig{
+ {
+ Key: ptr.Ref("name"),
+ Value: ptr.Ref("world"),
+ },
+ },
+ Active: ptr.Ref(true),
+ },
+ },
+ ACL: testAccTemplateACLConfig{
+ null: true,
+ },
+ }
+
+ cfg2 := cfg1
+ cfg2.Versions = slices.Clone(cfg2.Versions)
+ cfg2.Versions[0].TerraformVariables = []testAccTemplateKeyValueConfig{
+ {
+ Key: ptr.Ref("name"),
+ Value: ptr.Ref("world2"),
+ },
+ }
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ IsUnitTest: true,
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: cfg1.String(t),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ testAccCheckNumTemplateVersions(ctx, client, 1),
+ ),
+ },
+ {
+ Config: cfg2.String(t),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ testAccCheckNumTemplateVersions(ctx, client, 2),
+ ),
+ },
+ },
+ })
+ })
+
+ t.Run("CreateWithNoActiveVersionErrors", func(t *testing.T) {
+ cfg1 := testAccTemplateResourceConfig{
+ URL: client.URL.String(),
+ Token: client.SessionToken(),
+ Name: ptr.Ref("example-template"),
+ Versions: []testAccTemplateVersionConfig{
+ {
+ // Auto-generated version name
+ Directory: &exTemplateOne,
+ Active: ptr.Ref(false),
+ },
+ },
+ ACL: testAccTemplateACLConfig{
+ null: true,
+ },
+ }
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ IsUnitTest: true,
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: cfg1.String(t),
+ ExpectError: regexp.MustCompile("At least one template version must be active when creating"),
+ },
+ },
+ })
+ })
+
+ t.Run("AmbiguousActiveVersionResolvedByModifying", func(t *testing.T) {
+ cfg1 := testAccTemplateResourceConfig{
+ URL: client.URL.String(),
+ Token: client.SessionToken(),
+ Name: ptr.Ref("example-template"),
+ Versions: []testAccTemplateVersionConfig{
+ {
+ // Auto-generated version name
+ Directory: &exTemplateOne,
+ Active: ptr.Ref(true),
+ },
+ },
+ ACL: testAccTemplateACLConfig{
+ null: true,
+ },
+ }
+
+ cfg2 := cfg1
+ cfg2.Versions = slices.Clone(cfg2.Versions)
+ cfg2.Versions[0].Active = ptr.Ref(false)
+
+ cfg3 := cfg2
+ cfg3.Versions = slices.Clone(cfg3.Versions)
+ cfg3.Versions[0].Directory = &exTemplateTwo
+
+ cfg2b := cfg1
+ cfg2b.Versions = slices.Clone(cfg2b.Versions)
+ cfg2b.Versions = append(cfg2b.Versions, testAccTemplateVersionConfig{
+ Directory: &exTemplateTwo,
+ Active: ptr.Ref(false),
+ })
+
+ cfg3b := cfg2b
+ cfg3b.Versions = slices.Clone(cfg3b.Versions)
+ cfg3b.Versions[1].Active = ptr.Ref(true)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ IsUnitTest: true,
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: cfg1.String(t),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ testAccCheckNumTemplateVersions(ctx, client, 1),
+ ),
+ },
+ // With an unmodified version deactivated, it's not clear what
+ // the active version should be.
+ {
+ Config: cfg2.String(t),
+ ExpectError: regexp.MustCompile("Plan could not determine which version should be active."),
+ },
+ // If we modify the version, a new version will be created on `coderd`,
+ // and the old version can remain active.
+ {
+ Config: cfg3.String(t),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ testAccCheckNumTemplateVersions(ctx, client, 2),
+ resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "versions.*", map[string]*regexp.Regexp{
+ "active": regexp.MustCompile("false"),
+ }),
+ ),
+ },
+ },
+ })
+ })
+
+ t.Run("AmbiguousActiveVersionResolvedByCreatingNewVersion", func(t *testing.T) {
+ cfg1 := testAccTemplateResourceConfig{
+ URL: client.URL.String(),
+ Token: client.SessionToken(),
+ Name: ptr.Ref("example-template"),
+ Versions: []testAccTemplateVersionConfig{
+ {
+ // Auto-generated version name
+ Directory: &exTemplateOne,
+ Active: ptr.Ref(true),
+ },
+ },
+ ACL: testAccTemplateACLConfig{
+ null: true,
+ },
+ }
+
+ cfg2 := cfg1
+ cfg2.Versions = slices.Clone(cfg2.Versions)
+ cfg2.Versions[0].Active = ptr.Ref(false)
+ cfg2.Versions = append(cfg2.Versions, testAccTemplateVersionConfig{
+ Directory: &exTemplateTwo,
+ Active: ptr.Ref(false),
+ })
+
+ cfg3 := cfg2
+ cfg3.Versions = slices.Clone(cfg3.Versions)
+ cfg3.Versions[1].Active = ptr.Ref(true)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ IsUnitTest: true,
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: cfg1.String(t),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ testAccCheckNumTemplateVersions(ctx, client, 1),
+ ),
+ },
+ // Adding a new version that's not active doesn't help
+ {
+ Config: cfg2.String(t),
+ ExpectError: regexp.MustCompile("Plan could not determine which version should be active."),
+ },
+ // Making that new version active will fix the issue
+ {
+ Config: cfg3.String(t),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ testAccCheckNumTemplateVersions(ctx, client, 2),
+ ),
+ },
+ },
+ })
+ })
+
+ t.Run("PushNewInactiveVersion", func(t *testing.T) {
+ cfg1 := testAccTemplateResourceConfig{
+ URL: client.URL.String(),
+ Token: client.SessionToken(),
+ Name: ptr.Ref("example-template"),
+ Versions: []testAccTemplateVersionConfig{
+ {
+ // Auto-generated version name
+ Directory: &exTemplateOne,
+ Active: ptr.Ref(true),
+ },
+ },
+ ACL: testAccTemplateACLConfig{
+ null: true,
+ },
+ }
+
+ cfg2 := cfg1
+ cfg2.Versions = slices.Clone(cfg2.Versions)
+ cfg2.Versions[0].Active = ptr.Ref(false)
+ cfg2.Versions[0].Directory = &exTemplateTwo
+
+ cfg3 := cfg2
+ cfg3.Versions = slices.Clone(cfg3.Versions)
+ cfg3.Versions[0].Active = ptr.Ref(true)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ IsUnitTest: true,
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ // Create one active version
+ {
+ Config: cfg1.String(t),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ testAccCheckNumTemplateVersions(ctx, client, 1),
+ ),
+ },
+ // Modify an existing version, make it inactive
+ {
+ Config: cfg2.String(t),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ testAccCheckNumTemplateVersions(ctx, client, 2),
+ resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "versions.*", map[string]*regexp.Regexp{
+ "active": regexp.MustCompile("false"),
+ }),
+ ),
+ },
+ // Make that modification active
+ {
+ Config: cfg3.String(t),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ testAccCheckNumTemplateVersions(ctx, client, 2),
+ resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "versions.*", map[string]*regexp.Regexp{
+ "active": regexp.MustCompile("true"),
+ }),
+ ),
+ },
},
})
})
}
func TestAccTemplateResourceEnterprise(t *testing.T) {
+ t.Parallel()
if os.Getenv("TF_ACC") == "" {
t.Skip("Acceptance tests are disabled.")
}
- ctx := context.Background()
- client := integration.StartCoder(ctx, t, "template_acc", true)
+ ctx := t.Context()
+ client := integration.StartCoder(ctx, t, "template_resource_acc", true)
firstUser, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
- cfg1 := testAccTemplateResourceConfig{
- URL: client.URL.String(),
- Token: client.SessionToken(),
- Name: PtrTo("example-template"),
- Versions: []testAccTemplateVersionConfig{
- {
- // Auto-generated version name
- Directory: PtrTo("../../integration/template-test/example-template"),
- Active: PtrTo(true),
- // TODO(ethanndickson): Remove this when we add in `*.tfvars` parsing
- TerraformVariables: []testAccTemplateKeyValueConfig{
+ group, err := client.CreateGroup(ctx, firstUser.OrganizationIDs[0], codersdk.CreateGroupRequest{
+ Name: "bosses",
+ QuotaAllowance: 200,
+ })
+ require.NoError(t, err)
+
+ exTemplateOne := t.TempDir()
+ err = cp.Copy("../../integration/template-test/example-template", exTemplateOne)
+ require.NoError(t, err)
+
+ t.Run("BasicUsage", func(t *testing.T) {
+ cfg1 := testAccTemplateResourceConfig{
+ URL: client.URL.String(),
+ Token: client.SessionToken(),
+ Name: ptr.Ref("example-template"),
+ Versions: []testAccTemplateVersionConfig{
+ {
+ // Auto-generated version name
+ Directory: &exTemplateOne,
+ Active: ptr.Ref(true),
+ },
+ },
+ ACL: testAccTemplateACLConfig{
+ GroupACL: []testAccTemplateKeyValueConfig{
+ {
+ Key: ptr.Ref(firstUser.OrganizationIDs[0].String()),
+ Value: ptr.Ref("use"),
+ },
+ {
+ Key: ptr.Ref(group.ID.String()),
+ Value: ptr.Ref("admin"),
+ },
+ },
+ UserACL: []testAccTemplateKeyValueConfig{
{
- Key: PtrTo("name"),
- Value: PtrTo("world"),
+ Key: ptr.Ref(firstUser.ID.String()),
+ Value: ptr.Ref("admin"),
},
},
},
- },
- ACL: testAccTemplateACLConfig{
- GroupACL: []testAccTemplateKeyValueConfig{
+ }
+
+ cfg2 := cfg1
+ cfg2.ACL.GroupACL = slices.Clone(cfg2.ACL.GroupACL[1:])
+ cfg2.MaxPortShareLevel = ptr.Ref("owner")
+
+ cfg3 := cfg2
+ cfg3.ACL.null = true
+ cfg3.MaxPortShareLevel = ptr.Ref("public")
+
+ cfg4 := cfg3
+ cfg4.AllowUserAutostart = ptr.Ref(false)
+ cfg4.AutostopRequirement = testAccAutostopRequirementConfig{
+ DaysOfWeek: ptr.Ref([]string{"monday", "tuesday"}),
+ Weeks: ptr.Ref(int64(2)),
+ }
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ IsUnitTest: true,
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
{
- Key: PtrTo(firstUser.OrganizationIDs[0].String()),
- Value: PtrTo("use"),
+ Config: cfg1.String(t),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("coderd_template.test", "max_port_share_level", "owner"),
+ resource.TestCheckResourceAttr("coderd_template.test", "acl.groups.#", "2"),
+ resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "acl.groups.*", map[string]*regexp.Regexp{
+ "id": regexp.MustCompile(firstUser.OrganizationIDs[0].String()),
+ "role": regexp.MustCompile("^use$"),
+ }),
+ resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "acl.groups.*", map[string]*regexp.Regexp{
+ "id": regexp.MustCompile(group.ID.String()),
+ "role": regexp.MustCompile("^admin$"),
+ }),
+ resource.TestCheckResourceAttr("coderd_template.test", "acl.users.#", "1"),
+ resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "acl.users.*", map[string]*regexp.Regexp{
+ "id": regexp.MustCompile(firstUser.ID.String()),
+ "role": regexp.MustCompile("^admin$"),
+ }),
+ ),
},
- },
- UserACL: []testAccTemplateKeyValueConfig{
{
- Key: PtrTo(firstUser.ID.String()),
- Value: PtrTo("admin"),
+ Config: cfg2.String(t),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("coderd_template.test", "max_port_share_level", "owner"),
+ resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "acl.users.*", map[string]*regexp.Regexp{
+ "id": regexp.MustCompile(firstUser.ID.String()),
+ "role": regexp.MustCompile("^admin$"),
+ }),
+ ),
+ },
+ {
+ Config: cfg3.String(t),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("coderd_template.test", "max_port_share_level", "public"),
+ resource.TestCheckNoResourceAttr("coderd_template.test", "acl"),
+ func(s *terraform.State) error {
+ templates, err := client.Templates(ctx, codersdk.TemplateFilter{})
+ if err != nil {
+ return err
+ }
+ if len(templates) != 1 {
+ return fmt.Errorf("expected 1 template, got %d", len(templates))
+ }
+ acl, err := client.TemplateACL(ctx, templates[0].ID)
+ if err != nil {
+ return err
+ }
+ if len(acl.Groups) != 1 {
+ return fmt.Errorf("expected 1 group ACL, got %d", len(acl.Groups))
+ }
+ if acl.Groups[0].Role != "admin" && acl.Groups[0].ID != group.ID {
+ return fmt.Errorf("expected group ACL to be 'use' for %s, got %s", firstUser.OrganizationIDs[0].String(), acl.Groups[0].Role)
+ }
+ if len(acl.Users) != 1 {
+ return fmt.Errorf("expected 1 user ACL, got %d", len(acl.Users))
+ }
+ if acl.Users[0].Role != "admin" && acl.Users[0].ID != firstUser.ID {
+ return fmt.Errorf("expected user ACL to be 'admin' for %s, got %s", firstUser.ID.String(), acl.Users[0].Role)
+ }
+ return nil
+ },
+ ),
+ },
+ {
+ Config: cfg4.String(t),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttr("coderd_template.test", "allow_user_auto_start", "false"),
+ resource.TestCheckResourceAttr("coderd_template.test", "auto_stop_requirement.days_of_week.#", "2"),
+ resource.TestCheckResourceAttr("coderd_template.test", "auto_stop_requirement.weeks", "2"),
+ ),
},
},
- },
- }
-
- cfg2 := cfg1
- cfg2.ACL.null = true
-
- cfg3 := cfg2
- cfg3.AllowUserAutostart = PtrTo(false)
- cfg3.AutostopRequirement = testAccAutostopRequirementConfig{
- DaysOfWeek: PtrTo([]string{"monday", "tuesday"}),
- Weeks: PtrTo(int64(2)),
- }
+ })
+ })
- resource.Test(t, resource.TestCase{
- PreCheck: func() { testAccPreCheck(t) },
- IsUnitTest: true,
- ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
- Steps: []resource.TestStep{
- {
- Config: cfg1.String(t),
- Check: resource.ComposeAggregateTestCheckFunc(
- resource.TestCheckResourceAttr("coderd_template.test", "acl.groups.#", "1"),
- resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "acl.groups.*", map[string]*regexp.Regexp{
- "id": regexp.MustCompile(".+"),
- "role": regexp.MustCompile("^use$"),
- }),
- resource.TestMatchTypeSetElemNestedAttrs("coderd_template.test", "acl.users.*", map[string]*regexp.Regexp{
- "id": regexp.MustCompile(".+"),
- "role": regexp.MustCompile("^admin$"),
- }),
- ),
- },
- {
- Config: cfg2.String(t),
- Check: resource.ComposeAggregateTestCheckFunc(
- resource.TestCheckNoResourceAttr("coderd_template.test", "acl"),
- func(s *terraform.State) error {
- templates, err := client.Templates(ctx, codersdk.TemplateFilter{})
- if err != nil {
- return err
- }
- if len(templates) != 1 {
- return fmt.Errorf("expected 1 template, got %d", len(templates))
- }
- acl, err := client.TemplateACL(ctx, templates[0].ID)
- if err != nil {
- return err
- }
- if len(acl.Groups) != 1 {
- return fmt.Errorf("expected 1 group ACL, got %d", len(acl.Groups))
- }
- if acl.Groups[0].Role != "use" && acl.Groups[0].ID != firstUser.OrganizationIDs[0] {
- return fmt.Errorf("expected group ACL to be 'use' for %s, got %s", firstUser.OrganizationIDs[0].String(), acl.Groups[0].Role)
- }
- if len(acl.Users) != 1 {
- return fmt.Errorf("expected 1 user ACL, got %d", len(acl.Users))
- }
- if acl.Users[0].Role != "admin" && acl.Users[0].ID != firstUser.ID {
- return fmt.Errorf("expected user ACL to be 'admin' for %s, got %s", firstUser.ID.String(), acl.Users[0].Role)
- }
- return nil
- },
- ),
+ // Verifies that when `max_port_share_level` is set to to the default value,
+ // an update request that would return HTTP Not Modified is not sent.
+ t.Run("DefaultMaxPortShareLevel", func(t *testing.T) {
+ cfg1 := testAccTemplateResourceConfig{
+ URL: client.URL.String(),
+ Token: client.SessionToken(),
+ Name: ptr.Ref("example-template"),
+ Versions: []testAccTemplateVersionConfig{
+ {
+ Directory: &exTemplateOne,
+ Active: ptr.Ref(true),
+ },
},
- {
- Config: cfg3.String(t),
- Check: resource.ComposeAggregateTestCheckFunc(
- resource.TestCheckResourceAttr("coderd_template.test", "allow_user_auto_start", "false"),
- resource.TestCheckResourceAttr("coderd_template.test", "auto_stop_requirement.days_of_week.#", "2"),
- resource.TestCheckResourceAttr("coderd_template.test", "auto_stop_requirement.weeks", "2"),
- ),
+ MaxPortShareLevel: ptr.Ref("owner"),
+ }
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ IsUnitTest: true,
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: cfg1.String(t),
+ Check: resource.TestCheckResourceAttr("coderd_template.test", "max_port_share_level", "owner"),
+ },
},
- },
+ })
})
}
func TestAccTemplateResourceAGPL(t *testing.T) {
+ t.Parallel()
if os.Getenv("TF_ACC") == "" {
t.Skip("Acceptance tests are disabled.")
}
- ctx := context.Background()
- client := integration.StartCoder(ctx, t, "template_acc", false)
+ ctx := t.Context()
+ client := integration.StartCoder(ctx, t, "template_resource_agpl_acc", false)
firstUser, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
+ exTemplateOne := t.TempDir()
+ err = cp.Copy("../../integration/template-test/example-template", exTemplateOne)
+ 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: &exTemplateOne,
+ 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 = ptr.Ref("owner")
+
for _, cfg := range []testAccTemplateResourceConfig{cfg1, cfg2, cfg3, cfg4} {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
@@ -541,6 +900,71 @@ func TestAccTemplateResourceAGPL(t *testing.T) {
Config: cfg6.String(t),
ExpectError: regexp.MustCompile("Your license is not entitled to use template access control"),
},
+ {
+ Config: cfg7.String(t),
+ ExpectError: regexp.MustCompile("Your license is not entitled to use port sharing control"),
+ },
+ },
+ })
+}
+
+func TestAccTemplateResourceVariables(t *testing.T) {
+ t.Parallel()
+ cfg := `
+provider coderd {
+ url = %q
+ token = %q
+}
+
+data "coderd_organization" "default" {
+ is_default = true
+}
+
+variable "PRIOR_GIT_COMMIT_SHA" {
+ default = "abcdef"
+}
+
+variable "CURRENT_GIT_COMMIT_SHA" {
+ default = "ghijkl"
+}
+
+variable "ACTIVE" {
+ default = true
+}
+
+resource "coderd_template" "sample" {
+ name = "example-template"
+ versions = [
+ {
+ name = "${var.PRIOR_GIT_COMMIT_SHA}"
+ directory = %q
+ active = var.ACTIVE
+ },
+ {
+ name = "${var.CURRENT_GIT_COMMIT_SHA}"
+ directory = %q
+ active = false
+ }
+ ]
+}`
+
+ ctx := t.Context()
+ client := integration.StartCoder(ctx, t, "template_resource_variables_acc", false)
+
+ exTemplateOne := t.TempDir()
+ err := cp.Copy("../../integration/template-test/example-template", exTemplateOne)
+ require.NoError(t, err)
+
+ cfg = fmt.Sprintf(cfg, client.URL.String(), client.SessionToken(), exTemplateOne, exTemplateOne)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ IsUnitTest: true,
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: cfg,
+ },
},
})
}
@@ -566,6 +990,7 @@ type testAccTemplateResourceConfig struct {
TimeTilDormantAutodelete *int64
RequireActiveVersion *bool
DeprecationMessage *string
+ MaxPortShareLevel *string
Versions []testAccTemplateVersionConfig
ACL testAccTemplateACLConfig
@@ -672,6 +1097,7 @@ resource "coderd_template" "test" {
time_til_dormant_autodelete_ms = {{orNull .TimeTilDormantAutodelete}}
require_active_version = {{orNull .RequireActiveVersion}}
deprecation_message = {{orNull .DeprecationMessage}}
+ max_port_share_level = {{orNull .MaxPortShareLevel}}
acl = ` + c.ACL.String(t) + `
@@ -746,27 +1172,32 @@ func testAccCheckNumTemplateVersions(ctx context.Context, client *codersdk.Clien
}
func TestReconcileVersionIDs(t *testing.T) {
+ t.Parallel()
aUUID := uuid.New()
bUUID := uuid.New()
cases := []struct {
- Name string
- planVersions Versions
- configVersions Versions
- inputState LastVersionsByHash
- expectedVersions Versions
+ Name string
+ planVersions Versions
+ configVersions Versions
+ inputState LastVersionsByHash
+ expectedVersions Versions
+ cfgHasActiveVersion bool
+ expectError bool
}{
{
Name: "IdenticalDontRename",
planVersions: []TemplateVersion{
{
- Name: types.StringValue("foo"),
- DirectoryHash: types.StringValue("aaa"),
- ID: NewUUIDUnknown(),
+ Name: types.StringValue("foo"),
+ DirectoryHash: types.StringValue("aaa"),
+ ID: NewUUIDUnknown(),
+ TerraformVariables: []Variable{},
},
{
- Name: types.StringValue("bar"),
- DirectoryHash: types.StringValue("aaa"),
- ID: NewUUIDUnknown(),
+ Name: types.StringValue("bar"),
+ DirectoryHash: types.StringValue("aaa"),
+ ID: NewUUIDUnknown(),
+ TerraformVariables: []Variable{},
},
},
configVersions: []TemplateVersion{
@@ -780,21 +1211,24 @@ func TestReconcileVersionIDs(t *testing.T) {
inputState: map[string][]PreviousTemplateVersion{
"aaa": {
{
- ID: aUUID,
- Name: "bar",
+ ID: aUUID,
+ Name: "bar",
+ TFVars: map[string]string{},
},
},
},
expectedVersions: []TemplateVersion{
{
- Name: types.StringValue("foo"),
- DirectoryHash: types.StringValue("aaa"),
- ID: NewUUIDUnknown(),
+ Name: types.StringValue("foo"),
+ DirectoryHash: types.StringValue("aaa"),
+ ID: NewUUIDUnknown(),
+ TerraformVariables: []Variable{},
},
{
- Name: types.StringValue("bar"),
- DirectoryHash: types.StringValue("aaa"),
- ID: UUIDValue(aUUID),
+ Name: types.StringValue("bar"),
+ DirectoryHash: types.StringValue("aaa"),
+ ID: UUIDValue(aUUID),
+ TerraformVariables: []Variable{},
},
},
},
@@ -802,14 +1236,16 @@ func TestReconcileVersionIDs(t *testing.T) {
Name: "IdenticalRenameFirst",
planVersions: []TemplateVersion{
{
- Name: types.StringValue("foo"),
- DirectoryHash: types.StringValue("aaa"),
- ID: NewUUIDUnknown(),
+ Name: types.StringValue("foo"),
+ DirectoryHash: types.StringValue("aaa"),
+ ID: NewUUIDUnknown(),
+ TerraformVariables: []Variable{},
},
{
- Name: types.StringValue("bar"),
- DirectoryHash: types.StringValue("aaa"),
- ID: NewUUIDUnknown(),
+ Name: types.StringValue("bar"),
+ DirectoryHash: types.StringValue("aaa"),
+ ID: NewUUIDUnknown(),
+ TerraformVariables: []Variable{},
},
},
configVersions: []TemplateVersion{
@@ -823,21 +1259,24 @@ func TestReconcileVersionIDs(t *testing.T) {
inputState: map[string][]PreviousTemplateVersion{
"aaa": {
{
- ID: aUUID,
- Name: "baz",
+ ID: aUUID,
+ Name: "baz",
+ TFVars: map[string]string{},
},
},
},
expectedVersions: []TemplateVersion{
{
- Name: types.StringValue("foo"),
- DirectoryHash: types.StringValue("aaa"),
- ID: UUIDValue(aUUID),
+ Name: types.StringValue("foo"),
+ DirectoryHash: types.StringValue("aaa"),
+ ID: UUIDValue(aUUID),
+ TerraformVariables: []Variable{},
},
{
- Name: types.StringValue("bar"),
- DirectoryHash: types.StringValue("aaa"),
- ID: NewUUIDUnknown(),
+ Name: types.StringValue("bar"),
+ DirectoryHash: types.StringValue("aaa"),
+ ID: NewUUIDUnknown(),
+ TerraformVariables: []Variable{},
},
},
},
@@ -845,14 +1284,16 @@ func TestReconcileVersionIDs(t *testing.T) {
Name: "IdenticalHashesInState",
planVersions: []TemplateVersion{
{
- Name: types.StringValue("foo"),
- DirectoryHash: types.StringValue("aaa"),
- ID: NewUUIDUnknown(),
+ Name: types.StringValue("foo"),
+ DirectoryHash: types.StringValue("aaa"),
+ ID: NewUUIDUnknown(),
+ TerraformVariables: []Variable{},
},
{
- Name: types.StringValue("bar"),
- DirectoryHash: types.StringValue("aaa"),
- ID: NewUUIDUnknown(),
+ Name: types.StringValue("bar"),
+ DirectoryHash: types.StringValue("aaa"),
+ ID: NewUUIDUnknown(),
+ TerraformVariables: []Variable{},
},
},
configVersions: []TemplateVersion{
@@ -866,25 +1307,29 @@ func TestReconcileVersionIDs(t *testing.T) {
inputState: map[string][]PreviousTemplateVersion{
"aaa": {
{
- ID: aUUID,
- Name: "qux",
+ ID: aUUID,
+ Name: "qux",
+ TFVars: map[string]string{},
},
{
- ID: bUUID,
- Name: "baz",
+ ID: bUUID,
+ Name: "baz",
+ TFVars: map[string]string{},
},
},
},
expectedVersions: []TemplateVersion{
{
- Name: types.StringValue("foo"),
- DirectoryHash: types.StringValue("aaa"),
- ID: UUIDValue(aUUID),
+ Name: types.StringValue("foo"),
+ DirectoryHash: types.StringValue("aaa"),
+ ID: UUIDValue(aUUID),
+ TerraformVariables: []Variable{},
},
{
- Name: types.StringValue("bar"),
- DirectoryHash: types.StringValue("aaa"),
- ID: UUIDValue(bUUID),
+ Name: types.StringValue("bar"),
+ DirectoryHash: types.StringValue("aaa"),
+ ID: UUIDValue(bUUID),
+ TerraformVariables: []Variable{},
},
},
},
@@ -892,14 +1337,16 @@ func TestReconcileVersionIDs(t *testing.T) {
Name: "UnknownUsesStateInOrder",
planVersions: []TemplateVersion{
{
- Name: types.StringValue("foo"),
- DirectoryHash: types.StringValue("aaa"),
- ID: NewUUIDUnknown(),
+ Name: types.StringValue("foo"),
+ DirectoryHash: types.StringValue("aaa"),
+ ID: NewUUIDUnknown(),
+ TerraformVariables: []Variable{},
},
{
- Name: types.StringUnknown(),
- DirectoryHash: types.StringValue("aaa"),
- ID: NewUUIDUnknown(),
+ Name: types.StringUnknown(),
+ DirectoryHash: types.StringValue("aaa"),
+ ID: NewUUIDUnknown(),
+ TerraformVariables: []Variable{},
},
},
configVersions: []TemplateVersion{
@@ -913,65 +1360,197 @@ func TestReconcileVersionIDs(t *testing.T) {
inputState: map[string][]PreviousTemplateVersion{
"aaa": {
{
- ID: aUUID,
- Name: "qux",
+ ID: aUUID,
+ Name: "qux",
+ TFVars: map[string]string{},
},
{
- ID: bUUID,
- Name: "baz",
+ ID: bUUID,
+ Name: "baz",
+ TFVars: map[string]string{},
+ },
+ },
+ },
+ expectedVersions: []TemplateVersion{
+ {
+ Name: types.StringValue("foo"),
+ DirectoryHash: types.StringValue("aaa"),
+ ID: UUIDValue(aUUID),
+ TerraformVariables: []Variable{},
+ },
+ {
+ Name: types.StringValue("baz"),
+ DirectoryHash: types.StringValue("aaa"),
+ ID: UUIDValue(bUUID),
+ TerraformVariables: []Variable{},
+ },
+ },
+ },
+ {
+ Name: "NewVersionNewRandomName",
+ planVersions: []TemplateVersion{
+ {
+ Name: types.StringValue("weird_draught12"),
+ DirectoryHash: types.StringValue("bbb"),
+ ID: UUIDValue(aUUID),
+ TerraformVariables: []Variable{},
+ },
+ },
+ configVersions: []TemplateVersion{
+ {
+ Name: types.StringNull(),
+ },
+ },
+ inputState: map[string][]PreviousTemplateVersion{
+ "aaa": {
+ {
+ ID: aUUID,
+ Name: "weird_draught12",
+ TFVars: map[string]string{},
},
},
},
expectedVersions: []TemplateVersion{
+ {
+ Name: types.StringUnknown(),
+ DirectoryHash: types.StringValue("bbb"),
+ ID: NewUUIDUnknown(),
+ TerraformVariables: []Variable{},
+ },
+ },
+ },
+ {
+ Name: "IdenticalNewVars",
+ planVersions: []TemplateVersion{
{
Name: types.StringValue("foo"),
DirectoryHash: types.StringValue("aaa"),
ID: UUIDValue(aUUID),
+ TerraformVariables: []Variable{
+ {
+ Name: types.StringValue("foo"),
+ Value: types.StringValue("bar"),
+ },
+ },
},
+ },
+ configVersions: []TemplateVersion{
{
- Name: types.StringValue("baz"),
+ Name: types.StringValue("foo"),
+ },
+ },
+ inputState: map[string][]PreviousTemplateVersion{
+ "aaa": {
+ {
+ ID: aUUID,
+ Name: "foo",
+ TFVars: map[string]string{
+ "foo": "foo",
+ },
+ },
+ },
+ },
+ expectedVersions: []TemplateVersion{
+ {
+ Name: types.StringValue("foo"),
DirectoryHash: types.StringValue("aaa"),
- ID: UUIDValue(bUUID),
+ ID: NewUUIDUnknown(),
+ TerraformVariables: []Variable{
+ {
+ Name: types.StringValue("foo"),
+ Value: types.StringValue("bar"),
+ },
+ },
},
},
},
{
- Name: "NewVersionNewRandomName",
+ Name: "IdenticalSameVars",
planVersions: []TemplateVersion{
{
- Name: types.StringValue("weird_draught12"),
- DirectoryHash: types.StringValue("bbb"),
+ Name: types.StringValue("foo"),
+ DirectoryHash: types.StringValue("aaa"),
ID: UUIDValue(aUUID),
+ TerraformVariables: []Variable{
+ {
+ Name: types.StringValue("foo"),
+ Value: types.StringValue("bar"),
+ },
+ },
},
},
configVersions: []TemplateVersion{
{
- Name: types.StringNull(),
+ Name: types.StringValue("foo"),
},
},
inputState: map[string][]PreviousTemplateVersion{
"aaa": {
{
ID: aUUID,
- Name: "weird_draught12",
+ Name: "foo",
+ TFVars: map[string]string{
+ "foo": "bar",
+ },
},
},
},
expectedVersions: []TemplateVersion{
{
- Name: types.StringUnknown(),
- DirectoryHash: types.StringValue("bbb"),
- ID: NewUUIDUnknown(),
+ Name: types.StringValue("foo"),
+ DirectoryHash: types.StringValue("aaa"),
+ ID: UUIDValue(aUUID),
+ TerraformVariables: []Variable{
+ {
+ Name: types.StringValue("foo"),
+ Value: types.StringValue("bar"),
+ },
+ },
+ },
+ },
+ },
+ {
+ Name: "NoPossibleActiveVersion",
+ planVersions: []TemplateVersion{
+ {
+ Name: types.StringValue("foo"),
+ DirectoryHash: types.StringValue("aaa"),
+ ID: NewUUIDUnknown(),
+ TerraformVariables: []Variable{},
+ Active: types.BoolValue(false),
+ },
+ },
+ configVersions: []TemplateVersion{
+ {
+ Name: types.StringValue("foo"),
},
},
+ inputState: map[string][]PreviousTemplateVersion{
+ "aaa": {
+ {
+ ID: aUUID,
+ Name: "foo",
+ TFVars: map[string]string{},
+ Active: true,
+ },
+ },
+ },
+ cfgHasActiveVersion: false,
+ expectError: true,
},
}
for _, c := range cases {
c := c
t.Run(c.Name, func(t *testing.T) {
- c.planVersions.reconcileVersionIDs(c.inputState, c.configVersions)
- require.Equal(t, c.expectedVersions, c.planVersions)
+ t.Parallel()
+
+ diag := c.planVersions.reconcileVersionIDs(c.inputState, c.configVersions, c.cfgHasActiveVersion)
+ if c.expectError {
+ require.True(t, diag.HasError())
+ } else {
+ require.Equal(t, c.expectedVersions, c.planVersions)
+ }
})
}
diff --git a/internal/provider/user_data_source.go b/internal/provider/user_data_source.go
index 7c5846f..d221a1e 100644
--- a/internal/provider/user_data_source.go
+++ b/internal/provider/user_data_source.go
@@ -41,7 +41,6 @@ type UserDataSourceModel struct {
OrganizationIDs types.Set `tfsdk:"organization_ids"`
CreatedAt types.Int64 `tfsdk:"created_at"` // Unix timestamp
LastSeenAt types.Int64 `tfsdk:"last_seen_at"`
- ThemePreference types.String `tfsdk:"theme_preference"`
}
func (d *UserDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
@@ -72,12 +71,12 @@ func (d *UserDataSource) Schema(ctx context.Context, req datasource.SchemaReques
Computed: true,
},
"roles": schema.SetAttribute{
- MarkdownDescription: "Roles assigned to the user. Valid roles are 'owner', 'template-admin', 'user-admin', and 'auditor'.",
+ MarkdownDescription: "Roles assigned to the user. Valid roles are `owner`, `template-admin`, `user-admin`, and `auditor`.",
Computed: true,
ElementType: types.StringType,
},
"login_type": schema.StringAttribute{
- MarkdownDescription: "Type of login for the user. Valid types are 'none', 'password', 'github', and 'oidc'.",
+ MarkdownDescription: "Type of login for the user. Valid types are `none`, `password', `github`, and `oidc`.",
Computed: true,
},
"suspended": schema.BoolAttribute{
@@ -101,10 +100,6 @@ func (d *UserDataSource) Schema(ctx context.Context, req datasource.SchemaReques
MarkdownDescription: "Unix timestamp of when the user was last seen.",
Computed: true,
},
- "theme_preference": schema.StringAttribute{
- MarkdownDescription: "The user's preferred theme.",
- Computed: true,
- },
},
}
}
@@ -149,6 +144,11 @@ func (d *UserDataSource) Read(ctx context.Context, req datasource.ReadRequest, r
}
user, err := client.User(ctx, ident)
if err != nil {
+ if isNotFound(err) {
+ resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("User with identifier %q not found. Marking as deleted.", ident))
+ resp.State.RemoveResource(ctx)
+ return
+ }
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get current user, got error: %s", err))
return
}
@@ -183,7 +183,6 @@ func (d *UserDataSource) Read(ctx context.Context, req datasource.ReadRequest, r
data.OrganizationIDs = types.SetValueMust(UUIDType, orgIDs)
data.CreatedAt = types.Int64Value(user.CreatedAt.Unix())
data.LastSeenAt = types.Int64Value(user.LastSeenAt.Unix())
- data.ThemePreference = types.StringValue(user.ThemePreference)
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
diff --git a/internal/provider/user_data_source_test.go b/internal/provider/user_data_source_test.go
index 2d69d13..2890381 100644
--- a/internal/provider/user_data_source_test.go
+++ b/internal/provider/user_data_source_test.go
@@ -1,13 +1,13 @@
package provider
import (
- "context"
"os"
"regexp"
"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/hashicorp/terraform-plugin-testing/helper/resource"
@@ -15,10 +15,11 @@ import (
)
func TestAccUserDataSource(t *testing.T) {
+ t.Parallel()
if os.Getenv("TF_ACC") == "" {
t.Skip("Acceptance tests are disabled.")
}
- ctx := context.Background()
+ ctx := t.Context()
client := integration.StartCoder(ctx, t, "user_data_acc", false)
firstUser, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
@@ -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 4e8de49..79d248d 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.
@@ -56,8 +57,7 @@ func (r *UserResource) Metadata(ctx context.Context, req resource.MetadataReques
func (r *UserResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
- MarkdownDescription: "A user on the Coder deployment.\n\n" +
- "When importing, the ID supplied can be either a user UUID or a username.",
+ MarkdownDescription: "A user on the Coder deployment.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
@@ -71,18 +71,24 @@ func (r *UserResource) Schema(ctx context.Context, req resource.SchemaRequest, r
"username": schema.StringAttribute{
MarkdownDescription: "Username of the user.",
Required: true,
+ Validators: []validator.String{
+ codersdkvalidator.Name(),
+ },
},
"name": schema.StringAttribute{
MarkdownDescription: "Display name of the user. Defaults to username.",
Computed: true,
Optional: true,
+ Validators: []validator.String{
+ codersdkvalidator.UserRealName(),
+ },
},
"email": schema.StringAttribute{
MarkdownDescription: "Email address of the user.",
Required: true,
},
"roles": schema.SetAttribute{
- MarkdownDescription: "Roles assigned to the user. Valid roles are 'owner', 'template-admin', 'user-admin', and 'auditor'.",
+ MarkdownDescription: "Roles assigned to the user. Valid roles are `owner`, `template-admin`, `user-admin`, and `auditor`.",
Computed: true,
Optional: true,
ElementType: types.StringType,
@@ -94,7 +100,7 @@ func (r *UserResource) Schema(ctx context.Context, req resource.SchemaRequest, r
Default: setdefault.StaticValue(types.SetValueMust(types.StringType, []attr.Value{})),
},
"login_type": schema.StringAttribute{
- MarkdownDescription: "Type of login for the user. Valid types are 'none', 'password', 'github', and 'oidc'.",
+ MarkdownDescription: "Type of login for the user. Valid types are `none`, `password`, `github`, and `oidc`.",
Computed: true,
Optional: true,
Validators: []validator.String{
@@ -106,7 +112,7 @@ func (r *UserResource) Schema(ctx context.Context, req resource.SchemaRequest, r
},
},
"password": schema.StringAttribute{
- MarkdownDescription: "Password for the user. Required when login_type is 'password'. Passwords are saved into the state as plain text and should only be used for testing purposes.",
+ MarkdownDescription: "Password for the user. Required when `login_type` is `password`. Passwords are saved into the state as plain text and should only be used for testing purposes.",
Optional: true,
Sensitive: true,
},
@@ -242,9 +248,15 @@ func (r *UserResource) Read(ctx context.Context, req resource.ReadRequest, resp
client := r.data.Client
+ // Lookup by ID to handle imports
user, err := client.User(ctx, data.ID.ValueString())
if err != nil {
- resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get current user, got error: %s", err))
+ if isNotFound(err) {
+ resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("User with ID %q not found. Marking resource as deleted.", data.ID.ValueString()))
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get current user by ID, got error: %s", err))
return
}
if len(user.OrganizationIDs) < 1 {
@@ -263,6 +275,30 @@ func (r *UserResource) Read(ctx context.Context, req resource.ReadRequest, resp
data.LoginType = types.StringValue(string(user.LoginType))
data.Suspended = types.BoolValue(user.Status == codersdk.UserStatusSuspended)
+ // The user-by-ID API returns deleted users if the authorized user has
+ // permission. It does not indicate whether the user is deleted or not.
+ // The user-by-username API will never return deleted users.
+ // So, we do another lookup by username.
+ userByName, err := client.User(ctx, data.Username.ValueString())
+ if err != nil {
+ if isNotFound(err) {
+ resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf(
+ "User with username %q not found. Marking resource as deleted.",
+ data.Username.ValueString()))
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get current user by username, got error: %s", err))
+ return
+ }
+ if userByName.ID != data.ID.ValueUUID() {
+ resp.Diagnostics.AddWarning("Client Error", fmt.Sprintf(
+ "The username %q has been reassigned to a new user not managed by this Terraform resource. Marking resource as deleted.",
+ user.Username))
+ resp.State.RemoveResource(ctx)
+ return
+ }
+
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
diff --git a/internal/provider/user_resource_test.go b/internal/provider/user_resource_test.go
index a7bb470..8c78acc 100644
--- a/internal/provider/user_resource_test.go
+++ b/internal/provider/user_resource_test.go
@@ -1,43 +1,45 @@
package provider
import (
- "context"
"os"
"strings"
"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/hashicorp/terraform-plugin-testing/terraform"
"github.com/stretchr/testify/require"
)
func TestAccUserResource(t *testing.T) {
+ t.Parallel()
if os.Getenv("TF_ACC") == "" {
t.Skip("Acceptance tests are disabled.")
}
- ctx := context.Background()
+ ctx := t.Context()
client := integration.StartCoder(ctx, t, "user_acc", false)
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{
@@ -99,6 +101,19 @@ func TestAccUserResource(t *testing.T) {
resource.TestCheckResourceAttr("coderd_user.test", "login_type", "github"),
),
},
+ // Verify config drift via deletion is handled
+ {
+ Config: cfg4.String(t),
+ Check: func(*terraform.State) error {
+ user, err := client.User(ctx, "exampleNew")
+ if err != nil {
+ return err
+ }
+ return client.DeleteUser(ctx, user.ID)
+ },
+ // The Plan should be to create the entire resource
+ ExpectNonEmptyPlan: true,
+ },
},
})
}
diff --git a/internal/provider/util.go b/internal/provider/util.go
index 03d899f..3f35a25 100644
--- a/internal/provider/util.go
+++ b/internal/provider/util.go
@@ -3,17 +3,17 @@ package provider
import (
"crypto/sha256"
"encoding/hex"
+ "errors"
"fmt"
+ "net/http"
"os"
"path/filepath"
+ "strings"
+ "github.com/coder/coder/v2/codersdk"
"github.com/google/uuid"
)
-func PtrTo[T any](v T) *T {
- return &v
-}
-
func PrintOrNull(v any) string {
if v == nil {
return "null"
@@ -84,13 +84,15 @@ func computeDirectoryHash(directory string) (string, error) {
return hex.EncodeToString(hash.Sum(nil)), nil
}
-// 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))
+// 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(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 {
@@ -99,10 +101,25 @@ 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())
}
}
return add, remove
}
+
+func isNotFound(err error) bool {
+ var sdkErr *codersdk.Error
+ if !errors.As(err, &sdkErr) {
+ return false
+ }
+ if sdkErr.StatusCode() == http.StatusNotFound {
+ return true
+ }
+ // `httpmw/ExtractUserContext` returns a 400 w/ this message if the user is not found
+ if sdkErr.StatusCode() == http.StatusBadRequest && strings.Contains(sdkErr.Message, "must be an existing uuid or username") {
+ return true
+ }
+ return false
+}
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..4b2fe05 100644
--- a/internal/provider/uuid_internal_test.go
+++ b/internal/provider/uuid_internal_test.go
@@ -1,12 +1,12 @@
package provider
import (
- "context"
"testing"
"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 +37,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,
+ },
},
}
@@ -47,11 +50,10 @@ func TestUUIDTypeValueFromTerraform(t *testing.T) {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
- ctx := context.Background()
+ ctx := t.Context()
actual, err := uuidType.ValueFromTerraform(UUIDType, ctx, test.input)
require.NoError(t, err)
-
require.Equal(t, test.expected, actual)
})
}
@@ -82,10 +84,9 @@ func TestUUIDToStringValue(t *testing.T) {
name, test := name, test
t.Run(name, func(t *testing.T) {
t.Parallel()
- ctx := context.Background()
+ ctx := t.Context()
s, _ := test.uuid.ToStringValue(ctx)
-
require.Equal(t, test.expected, s)
})
}
diff --git a/internal/provider/workspace_proxy_resource.go b/internal/provider/workspace_proxy_resource.go
index a95dc68..211c778 100644
--- a/internal/provider/workspace_proxy_resource.go
+++ b/internal/provider/workspace_proxy_resource.go
@@ -60,7 +60,7 @@ func (r *WorkspaceProxyResource) Schema(ctx context.Context, req resource.Schema
Computed: true,
},
"icon": schema.StringAttribute{
- MarkdownDescription: "Relative path or external URL that specifes an icon to be displayed in the dashboard.",
+ MarkdownDescription: "Relative path or external URL that specifies an icon to be displayed in the dashboard.",
Required: true,
},
"session_token": schema.StringAttribute{
@@ -142,6 +142,11 @@ func (r *WorkspaceProxyResource) Read(ctx context.Context, req resource.ReadRequ
client := r.data.Client
wsp, err := client.WorkspaceProxyByID(ctx, data.ID.ValueUUID())
if err != nil {
+ if isNotFound(err) {
+ resp.Diagnostics.AddWarning("Client Warning", fmt.Sprintf("Workspace proxy with ID %s not found. Marking as deleted.", data.ID.ValueString()))
+ resp.State.RemoveResource(ctx)
+ return
+ }
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to read workspace proxy: %v", err))
return
}
diff --git a/internal/provider/workspace_proxy_resource_test.go b/internal/provider/workspace_proxy_resource_test.go
index a2447ea..cecad70 100644
--- a/internal/provider/workspace_proxy_resource_test.go
+++ b/internal/provider/workspace_proxy_resource_test.go
@@ -1,36 +1,37 @@
package provider
import (
- "context"
"os"
"regexp"
"strings"
"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"
)
func TestAccWorkspaceProxyResource(t *testing.T) {
+ t.Parallel()
if os.Getenv("TF_ACC") == "" {
t.Skip("Acceptance tests are disabled.")
}
- ctx := context.Background()
+ ctx := t.Context()
client := integration.StartCoder(ctx, t, "ws_proxy_acc", true)
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,
@@ -55,18 +56,19 @@ func TestAccWorkspaceProxyResource(t *testing.T) {
}
func TestAccWorkspaceProxyResourceAGPL(t *testing.T) {
+ t.Parallel()
if os.Getenv("TF_ACC") == "" {
t.Skip("Acceptance tests are disabled.")
}
- ctx := context.Background()
- client := integration.StartCoder(ctx, t, "ws_proxy_acc", false)
+ ctx := t.Context()
+ client := integration.StartCoder(ctx, t, "ws_proxy_acc_agpl", false)
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{