mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
feat: add dynamic parameters websocket endpoint (#17165)
This commit is contained in:
97
coderd/apidoc/docs.go
generated
97
coderd/apidoc/docs.go
generated
@ -5764,6 +5764,35 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/dynamic-parameters": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Templates"
|
||||
],
|
||||
"summary": "Open dynamic parameters WebSocket by template version",
|
||||
"operationId": "open-dynamic-parameters-websocket-by-template-version",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template version ID",
|
||||
"name": "templateversion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"101": {
|
||||
"description": "Switching Protocols"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/external-auth": {
|
||||
"get": {
|
||||
"security": [
|
||||
@ -11332,73 +11361,7 @@ const docTemplate = `{
|
||||
}
|
||||
},
|
||||
"codersdk.CreateTestAuditLogRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"enum": [
|
||||
"create",
|
||||
"write",
|
||||
"delete",
|
||||
"start",
|
||||
"stop"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.AuditAction"
|
||||
}
|
||||
]
|
||||
},
|
||||
"additional_fields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"build_reason": {
|
||||
"enum": [
|
||||
"autostart",
|
||||
"autostop",
|
||||
"initiator"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.BuildReason"
|
||||
}
|
||||
]
|
||||
},
|
||||
"organization_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"request_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"resource_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"resource_type": {
|
||||
"enum": [
|
||||
"template",
|
||||
"template_version",
|
||||
"user",
|
||||
"workspace",
|
||||
"workspace_build",
|
||||
"git_ssh_key",
|
||||
"auditable_group"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.ResourceType"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
"type": "object"
|
||||
},
|
||||
"codersdk.CreateTokenRequest": {
|
||||
"type": "object",
|
||||
|
85
coderd/apidoc/swagger.json
generated
85
coderd/apidoc/swagger.json
generated
@ -5097,6 +5097,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/dynamic-parameters": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"tags": ["Templates"],
|
||||
"summary": "Open dynamic parameters WebSocket by template version",
|
||||
"operationId": "open-dynamic-parameters-websocket-by-template-version",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Template version ID",
|
||||
"name": "templateversion",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"101": {
|
||||
"description": "Switching Protocols"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/external-auth": {
|
||||
"get": {
|
||||
"security": [
|
||||
@ -10100,63 +10127,7 @@
|
||||
}
|
||||
},
|
||||
"codersdk.CreateTestAuditLogRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"enum": ["create", "write", "delete", "start", "stop"],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.AuditAction"
|
||||
}
|
||||
]
|
||||
},
|
||||
"additional_fields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"build_reason": {
|
||||
"enum": ["autostart", "autostop", "initiator"],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.BuildReason"
|
||||
}
|
||||
]
|
||||
},
|
||||
"organization_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"request_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"resource_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"resource_type": {
|
||||
"enum": [
|
||||
"template",
|
||||
"template_version",
|
||||
"user",
|
||||
"workspace",
|
||||
"workspace_build",
|
||||
"git_ssh_key",
|
||||
"auditable_group"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.ResourceType"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
"type": "object"
|
||||
},
|
||||
"codersdk.CreateTokenRequest": {
|
||||
"type": "object",
|
||||
|
@ -43,6 +43,7 @@ import (
|
||||
|
||||
"github.com/coder/coder/v2/coderd/cryptokeys"
|
||||
"github.com/coder/coder/v2/coderd/entitlements"
|
||||
"github.com/coder/coder/v2/coderd/files"
|
||||
"github.com/coder/coder/v2/coderd/idpsync"
|
||||
"github.com/coder/coder/v2/coderd/runtimeconfig"
|
||||
"github.com/coder/coder/v2/coderd/webpush"
|
||||
@ -557,6 +558,7 @@ func New(options *Options) *API {
|
||||
TemplateScheduleStore: options.TemplateScheduleStore,
|
||||
UserQuietHoursScheduleStore: options.UserQuietHoursScheduleStore,
|
||||
AccessControlStore: options.AccessControlStore,
|
||||
FileCache: files.NewFromStore(options.Database),
|
||||
Experiments: experiments,
|
||||
WebpushDispatcher: options.WebPushDispatcher,
|
||||
healthCheckGroup: &singleflight.Group[string, *healthsdk.HealthcheckReport]{},
|
||||
@ -1096,6 +1098,10 @@ func New(options *Options) *API {
|
||||
// The idea is to return an empty [], so that the coder CLI won't get blocked accidentally.
|
||||
r.Get("/schema", templateVersionSchemaDeprecated)
|
||||
r.Get("/parameters", templateVersionParametersDeprecated)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentDynamicParameters))
|
||||
r.Get("/dynamic-parameters", api.templateVersionDynamicParameters)
|
||||
})
|
||||
r.Get("/rich-parameters", api.templateVersionRichParameters)
|
||||
r.Get("/external-auth", api.templateVersionExternalAuth)
|
||||
r.Get("/variables", api.templateVersionVariables)
|
||||
@ -1545,6 +1551,7 @@ type API struct {
|
||||
// passed to dbauthz.
|
||||
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
|
||||
PortSharer atomic.Pointer[portsharing.PortSharer]
|
||||
FileCache files.Cache
|
||||
|
||||
UpdatesProvider tailnet.WorkspaceUpdatesProvider
|
||||
|
||||
|
@ -35,10 +35,14 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/tracing"
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/wsjson"
|
||||
"github.com/coder/coder/v2/examples"
|
||||
"github.com/coder/coder/v2/provisioner/terraform/tfparse"
|
||||
"github.com/coder/coder/v2/provisionersdk"
|
||||
sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
|
||||
"github.com/coder/preview"
|
||||
previewtypes "github.com/coder/preview/types"
|
||||
"github.com/coder/websocket"
|
||||
)
|
||||
|
||||
// @Summary Get template version by ID
|
||||
@ -266,6 +270,135 @@ func (api *API) patchCancelTemplateVersion(rw http.ResponseWriter, r *http.Reque
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Open dynamic parameters WebSocket by template version
|
||||
// @ID open-dynamic-parameters-websocket-by-template-version
|
||||
// @Security CoderSessionToken
|
||||
// @Tags Templates
|
||||
// @Param templateversion path string true "Template version ID" format(uuid)
|
||||
// @Success 101
|
||||
// @Router /templateversions/{templateversion}/dynamic-parameters [get]
|
||||
func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
templateVersion := httpmw.TemplateVersionParam(r)
|
||||
|
||||
// Check that the job has completed successfully
|
||||
job, err := api.Database.GetProvisionerJobByID(ctx, templateVersion.JobID)
|
||||
if httpapi.Is404Error(err) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching provisioner job.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if !job.CompletedAt.Valid {
|
||||
httpapi.Write(ctx, rw, http.StatusTooEarly, codersdk.Response{
|
||||
Message: "Template version job has not finished",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Having the Terraform plan available for the evaluation engine is helpful
|
||||
// for populating values from data blocks, but isn't strictly required. If
|
||||
// we don't have a cached plan available, we just use an empty one instead.
|
||||
plan := json.RawMessage("{}")
|
||||
tf, err := api.Database.GetTemplateVersionTerraformValues(ctx, templateVersion.ID)
|
||||
if err == nil {
|
||||
plan = tf.CachedPlan
|
||||
}
|
||||
|
||||
input := preview.Input{
|
||||
PlanJSON: plan,
|
||||
ParameterValues: map[string]string{},
|
||||
// TODO: write a db query that fetches all of the data needed to fill out
|
||||
// this owner value
|
||||
Owner: previewtypes.WorkspaceOwner{
|
||||
Groups: []string{"Everyone"},
|
||||
},
|
||||
}
|
||||
|
||||
// nolint:gocritic // We need to fetch the templates files for the Terraform
|
||||
// evaluator, and the user likely does not have permission.
|
||||
fileCtx := dbauthz.AsProvisionerd(ctx)
|
||||
fileID, err := api.Database.GetFileIDByTemplateVersionID(fileCtx, templateVersion.ID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error finding template version Terraform.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
fs, err := api.FileCache.Acquire(fileCtx, fileID)
|
||||
defer api.FileCache.Release(fileID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
||||
Message: "Internal error fetching template version Terraform.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := websocket.Accept(rw, r, nil)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusUpgradeRequired, codersdk.Response{
|
||||
Message: "Failed to accept WebSocket.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
stream := wsjson.NewStream[codersdk.DynamicParametersRequest, codersdk.DynamicParametersResponse](conn, websocket.MessageText, websocket.MessageText, api.Logger)
|
||||
|
||||
// Send an initial form state, computed without any user input.
|
||||
result, diagnostics := preview.Preview(ctx, input, fs)
|
||||
response := codersdk.DynamicParametersResponse{
|
||||
ID: -1,
|
||||
Diagnostics: previewtypes.Diagnostics(diagnostics),
|
||||
}
|
||||
if result != nil {
|
||||
response.Parameters = result.Parameters
|
||||
}
|
||||
err = stream.Send(response)
|
||||
if err != nil {
|
||||
stream.Drop()
|
||||
return
|
||||
}
|
||||
|
||||
// As the user types into the form, reprocess the state using their input,
|
||||
// and respond with updates.
|
||||
updates := stream.Chan()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
stream.Close(websocket.StatusGoingAway)
|
||||
return
|
||||
case update, ok := <-updates:
|
||||
if !ok {
|
||||
// The connection has been closed, so there is no one to write to
|
||||
return
|
||||
}
|
||||
input.ParameterValues = update.Inputs
|
||||
result, diagnostics := preview.Preview(ctx, input, fs)
|
||||
response := codersdk.DynamicParametersResponse{
|
||||
ID: update.ID,
|
||||
Diagnostics: previewtypes.Diagnostics(diagnostics),
|
||||
}
|
||||
if result != nil {
|
||||
response.Parameters = result.Parameters
|
||||
}
|
||||
err = stream.Send(response)
|
||||
if err != nil {
|
||||
stream.Drop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @Summary Get rich parameters by template version
|
||||
// @ID get-rich-parameters-by-template-version
|
||||
// @Security CoderSessionToken
|
||||
@ -287,8 +420,8 @@ func (api *API) templateVersionRichParameters(rw http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
if !job.CompletedAt.Valid {
|
||||
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
|
||||
Message: "Job hasn't completed!",
|
||||
httpapi.Write(ctx, rw, http.StatusTooEarly, codersdk.Response{
|
||||
Message: "Template version job has not finished",
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -428,7 +561,7 @@ func (api *API) templateVersionVariables(rw http.ResponseWriter, r *http.Request
|
||||
}
|
||||
if !job.CompletedAt.Valid {
|
||||
httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
|
||||
Message: "Job hasn't completed!",
|
||||
Message: "Template version job has not finished",
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -483,7 +616,7 @@ func (api *API) postTemplateVersionDryRun(rw http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
if !job.CompletedAt.Valid {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
httpapi.Write(ctx, rw, http.StatusTooEarly, codersdk.Response{
|
||||
Message: "Template version import job hasn't completed!",
|
||||
})
|
||||
return
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -27,6 +28,7 @@ import (
|
||||
"github.com/coder/coder/v2/provisionersdk"
|
||||
"github.com/coder/coder/v2/provisionersdk/proto"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/websocket"
|
||||
)
|
||||
|
||||
func TestTemplateVersion(t *testing.T) {
|
||||
@ -1207,7 +1209,7 @@ func TestTemplateVersionDryRun(t *testing.T) {
|
||||
_, err := client.CreateTemplateVersionDryRun(ctx, version.ID, codersdk.CreateTemplateVersionDryRunRequest{})
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
||||
require.Equal(t, http.StatusTooEarly, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("Cancel", func(t *testing.T) {
|
||||
@ -2056,11 +2058,7 @@ func TestTemplateArchiveVersions(t *testing.T) {
|
||||
|
||||
// Create some unused versions
|
||||
for i := 0; i < 2; i++ {
|
||||
unused := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.PlanComplete,
|
||||
ProvisionApply: echo.ApplyComplete,
|
||||
}, func(req *codersdk.CreateTemplateVersionRequest) {
|
||||
unused := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, func(req *codersdk.CreateTemplateVersionRequest) {
|
||||
req.TemplateID = template.ID
|
||||
})
|
||||
expArchived = append(expArchived, unused.ID)
|
||||
@ -2069,11 +2067,7 @@ func TestTemplateArchiveVersions(t *testing.T) {
|
||||
|
||||
// Create some used template versions
|
||||
for i := 0; i < 2; i++ {
|
||||
used := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.PlanComplete,
|
||||
ProvisionApply: echo.ApplyComplete,
|
||||
}, func(req *codersdk.CreateTemplateVersionRequest) {
|
||||
used := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, func(req *codersdk.CreateTemplateVersionRequest) {
|
||||
req.TemplateID = template.ID
|
||||
})
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, used.ID)
|
||||
@ -2140,3 +2134,73 @@ func TestTemplateArchiveVersions(t *testing.T) {
|
||||
require.NoError(t, err, "fetch all versions")
|
||||
require.Len(t, remaining, totalVersions-len(expArchived)-len(allFailed)+1, "remaining versions")
|
||||
}
|
||||
|
||||
func TestTemplateVersionDynamicParameters(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := coderdtest.DeploymentValues(t)
|
||||
cfg.Experiments = []string{string(codersdk.ExperimentDynamicParameters)}
|
||||
ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, DeploymentValues: cfg})
|
||||
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||||
templateAdmin, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
||||
|
||||
dynamicParametersTerraformSource, err := os.ReadFile("testdata/dynamicparameters/groups/main.tf")
|
||||
require.NoError(t, err)
|
||||
dynamicParametersTerraformPlan, err := os.ReadFile("testdata/dynamicparameters/groups/plan.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
files := echo.WithExtraFiles(map[string][]byte{
|
||||
"main.tf": dynamicParametersTerraformSource,
|
||||
})
|
||||
files.ProvisionPlan = []*proto.Response{{
|
||||
Type: &proto.Response_Plan{
|
||||
Plan: &proto.PlanComplete{
|
||||
Plan: dynamicParametersTerraformPlan,
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
version := coderdtest.CreateTemplateVersion(t, templateAdmin, owner.OrganizationID, files)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdmin, version.ID)
|
||||
_ = coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, version.ID)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, version.ID)
|
||||
require.NoError(t, err)
|
||||
defer stream.Close(websocket.StatusGoingAway)
|
||||
|
||||
previews := stream.Chan()
|
||||
|
||||
// Should automatically send a form state with all defaulted/empty values
|
||||
preview := testutil.RequireRecvCtx(ctx, t, previews)
|
||||
require.Empty(t, preview.Diagnostics)
|
||||
require.Equal(t, "group", preview.Parameters[0].Name)
|
||||
require.True(t, preview.Parameters[0].Value.Valid())
|
||||
require.Equal(t, "Everyone", preview.Parameters[0].Value.Value.AsString())
|
||||
|
||||
// Send a new value, and see it reflected
|
||||
err = stream.Send(codersdk.DynamicParametersRequest{
|
||||
ID: 1,
|
||||
Inputs: map[string]string{"group": "Bloob"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
preview = testutil.RequireRecvCtx(ctx, t, previews)
|
||||
require.Equal(t, 1, preview.ID)
|
||||
require.Empty(t, preview.Diagnostics)
|
||||
require.Equal(t, "group", preview.Parameters[0].Name)
|
||||
require.True(t, preview.Parameters[0].Value.Valid())
|
||||
require.Equal(t, "Bloob", preview.Parameters[0].Value.Value.AsString())
|
||||
|
||||
// Back to default
|
||||
err = stream.Send(codersdk.DynamicParametersRequest{
|
||||
ID: 3,
|
||||
Inputs: map[string]string{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
preview = testutil.RequireRecvCtx(ctx, t, previews)
|
||||
require.Equal(t, 3, preview.ID)
|
||||
require.Empty(t, preview.Diagnostics)
|
||||
require.Equal(t, "group", preview.Parameters[0].Name)
|
||||
require.True(t, preview.Parameters[0].Value.Valid())
|
||||
require.Equal(t, "Everyone", preview.Parameters[0].Value.Value.AsString())
|
||||
}
|
||||
|
25
coderd/testdata/dynamicparameters/groups/main.tf
vendored
Normal file
25
coderd/testdata/dynamicparameters/groups/main.tf
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
output "groups" {
|
||||
value = data.coder_workspace_owner.me.groups
|
||||
}
|
||||
|
||||
data "coder_parameter" "group" {
|
||||
name = "group"
|
||||
default = try(data.coder_workspace_owner.me.groups[0], "")
|
||||
dynamic "option" {
|
||||
for_each = data.coder_workspace_owner.me.groups
|
||||
content {
|
||||
name = option.value
|
||||
value = option.value
|
||||
}
|
||||
}
|
||||
}
|
92
coderd/testdata/dynamicparameters/groups/plan.json
vendored
Normal file
92
coderd/testdata/dynamicparameters/groups/plan.json
vendored
Normal file
@ -0,0 +1,92 @@
|
||||
{
|
||||
"terraform_version": "1.11.2",
|
||||
"format_version": "1.2",
|
||||
"checks": [],
|
||||
"complete": true,
|
||||
"timestamp": "2025-04-02T01:29:59Z",
|
||||
"variables": {},
|
||||
"prior_state": {
|
||||
"values": {
|
||||
"root_module": {
|
||||
"resources": [
|
||||
{
|
||||
"mode": "data",
|
||||
"name": "me",
|
||||
"type": "coder_workspace_owner",
|
||||
"address": "data.coder_workspace_owner.me",
|
||||
"provider_name": "registry.terraform.io/coder/coder",
|
||||
"schema_version": 0,
|
||||
"values": {
|
||||
"id": "25e81ec3-0eb9-4ee3-8b6d-738b8552f7a9",
|
||||
"name": "default",
|
||||
"email": "default@example.com",
|
||||
"groups": [],
|
||||
"full_name": "default",
|
||||
"login_type": null,
|
||||
"rbac_roles": [],
|
||||
"session_token": "",
|
||||
"ssh_public_key": "",
|
||||
"ssh_private_key": "",
|
||||
"oidc_access_token": ""
|
||||
},
|
||||
"sensitive_values": {
|
||||
"groups": [],
|
||||
"rbac_roles": [],
|
||||
"ssh_private_key": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"child_modules": []
|
||||
}
|
||||
},
|
||||
"format_version": "1.0",
|
||||
"terraform_version": "1.11.2"
|
||||
},
|
||||
"configuration": {
|
||||
"root_module": {
|
||||
"resources": [
|
||||
{
|
||||
"mode": "data",
|
||||
"name": "me",
|
||||
"type": "coder_workspace_owner",
|
||||
"address": "data.coder_workspace_owner.me",
|
||||
"schema_version": 0,
|
||||
"provider_config_key": "coder"
|
||||
}
|
||||
],
|
||||
"variables": {},
|
||||
"module_calls": {}
|
||||
},
|
||||
"provider_config": {
|
||||
"coder": {
|
||||
"name": "coder",
|
||||
"full_name": "registry.terraform.io/coder/coder"
|
||||
}
|
||||
}
|
||||
},
|
||||
"planned_values": {
|
||||
"root_module": {
|
||||
"resources": [],
|
||||
"child_modules": []
|
||||
}
|
||||
},
|
||||
"resource_changes": [],
|
||||
"relevant_attributes": [
|
||||
{
|
||||
"resource": "data.coder_workspace_owner.me",
|
||||
"attribute": ["full_name"]
|
||||
},
|
||||
{
|
||||
"resource": "data.coder_workspace_owner.me",
|
||||
"attribute": ["email"]
|
||||
},
|
||||
{
|
||||
"resource": "data.coder_workspace_owner.me",
|
||||
"attribute": ["id"]
|
||||
},
|
||||
{
|
||||
"resource": "data.coder_workspace_owner.me",
|
||||
"attribute": ["name"]
|
||||
}
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user