chore: fill out workspace owner data for dynamic parameters (#17366)

This commit is contained in:
ケイラ
2025-04-17 14:51:50 -06:00
committed by GitHub
parent ea65ddc17d
commit 2cc56ab515
17 changed files with 635 additions and 345 deletions
+37 -29
View File
@@ -5686,35 +5686,6 @@ 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": [
@@ -7570,6 +7541,43 @@ const docTemplate = `{
}
}
},
"/users/{user}/templateversions/{templateversion}/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": "user",
"in": "path",
"required": true
},
{
"type": "string",
"format": "uuid",
"description": "Template version ID",
"name": "templateversion",
"in": "path",
"required": true
}
],
"responses": {
"101": {
"description": "Switching Protocols"
}
}
}
},
"/users/{user}/webpush/subscription": {
"post": {
"security": [
+35 -27
View File
@@ -5029,33 +5029,6 @@
}
}
},
"/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": [
@@ -6693,6 +6666,41 @@
}
}
},
"/users/{user}/templateversions/{templateversion}/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": "user",
"in": "path",
"required": true
},
{
"type": "string",
"format": "uuid",
"description": "Template version ID",
"name": "templateversion",
"in": "path",
"required": true
}
],
"responses": {
"101": {
"description": "Switching Protocols"
}
}
}
},
"/users/{user}/webpush/subscription": {
"post": {
"security": [
+11 -4
View File
@@ -1108,10 +1108,6 @@ 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)
@@ -1177,6 +1173,17 @@ func New(options *Options) *API {
// organization member. This endpoint should match the authz story of
// postWorkspacesByOrganization
r.Post("/workspaces", api.postUserWorkspaces)
// Similarly to creating a workspace, evaluating parameters for a
// new workspace should also match the authz story of
// postWorkspacesByOrganization
r.Route("/templateversions/{templateversion}", func(r chi.Router) {
r.Use(
httpmw.ExtractTemplateVersionParam(options.Database),
httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentDynamicParameters),
)
r.Get("/parameters", api.templateVersionDynamicParameters)
})
})
r.Group(func(r chi.Router) {
+250
View File
@@ -0,0 +1,250 @@
package coderd
import (
"context"
"database/sql"
"encoding/json"
"net/http"
"time"
"github.com/google/uuid"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/wsjson"
"github.com/coder/preview"
previewtypes "github.com/coder/preview/types"
"github.com/coder/websocket"
)
// @Summary Open dynamic parameters WebSocket by template version
// @ID open-dynamic-parameters-websocket-by-template-version
// @Security CoderSessionToken
// @Tags Templates
// @Param user path string true "Template version ID" format(uuid)
// @Param templateversion path string true "Template version ID" format(uuid)
// @Success 101
// @Router /users/{user}/templateversions/{templateversion}/parameters [get]
func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Minute)
defer cancel()
user := httpmw.UserParam(r)
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
}
// 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
}
// 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
} else if !xerrors.Is(err, sql.ErrNoRows) {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to retrieve Terraform values for template version",
Detail: err.Error(),
})
return
}
owner, err := api.getWorkspaceOwnerData(ctx, user, templateVersion.OrganizationID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace owner.",
Detail: err.Error(),
})
return
}
input := preview.Input{
PlanJSON: plan,
ParameterValues: map[string]string{},
Owner: owner,
}
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
}
}
}
}
func (api *API) getWorkspaceOwnerData(
ctx context.Context,
user database.User,
organizationID uuid.UUID,
) (previewtypes.WorkspaceOwner, error) {
var g errgroup.Group
var ownerRoles []previewtypes.WorkspaceOwnerRBACRole
g.Go(func() error {
// nolint:gocritic // This is kind of the wrong query to use here, but it
// matches how the provisioner currently works. We should figure out
// something that needs less escalation but has the correct behavior.
row, err := api.Database.GetAuthorizationUserRoles(dbauthz.AsSystemRestricted(ctx), user.ID)
if err != nil {
return err
}
roles, err := row.RoleNames()
if err != nil {
return err
}
ownerRoles = make([]previewtypes.WorkspaceOwnerRBACRole, 0, len(roles))
for _, it := range roles {
if it.OrganizationID != uuid.Nil && it.OrganizationID != organizationID {
continue
}
var orgID string
if it.OrganizationID != uuid.Nil {
orgID = it.OrganizationID.String()
}
ownerRoles = append(ownerRoles, previewtypes.WorkspaceOwnerRBACRole{
Name: it.Name,
OrgID: orgID,
})
}
return nil
})
var publicKey string
g.Go(func() error {
key, err := api.Database.GetGitSSHKey(ctx, user.ID)
if err != nil {
return err
}
publicKey = key.PublicKey
return nil
})
var groupNames []string
g.Go(func() error {
groups, err := api.Database.GetGroups(ctx, database.GetGroupsParams{
OrganizationID: organizationID,
HasMemberID: user.ID,
})
if err != nil {
return err
}
groupNames = make([]string, 0, len(groups))
for _, it := range groups {
groupNames = append(groupNames, it.Group.Name)
}
return nil
})
err := g.Wait()
if err != nil {
return previewtypes.WorkspaceOwner{}, err
}
return previewtypes.WorkspaceOwner{
ID: user.ID.String(),
Name: user.Username,
FullName: user.Name,
Email: user.Email,
LoginType: string(user.LoginType),
RBACRoles: ownerRoles,
SSHPublicKey: publicKey,
Groups: groupNames,
}, nil
}
+134
View File
@@ -0,0 +1,134 @@
package coderd_test
import (
"os"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/coder/v2/testutil"
"github.com/coder/websocket"
)
func TestDynamicParametersOwnerGroups(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, templateAdminUser := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/groups/main.tf")
require.NoError(t, err)
dynamicParametersTerraformPlan, err := os.ReadFile("testdata/parameters/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, templateAdminUser.ID, 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.RequireReceive(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, "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.RequireReceive(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.RequireReceive(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())
}
func TestDynamicParametersOwnerSSHPublicKey(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, templateAdminUser := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/public_key/main.tf")
require.NoError(t, err)
dynamicParametersTerraformPlan, err := os.ReadFile("testdata/parameters/public_key/plan.json")
require.NoError(t, err)
sshKey, err := templateAdmin.GitSSHKey(t.Context(), "me")
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, templateAdminUser.ID, 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.RequireReceive(ctx, t, previews)
require.Equal(t, -1, preview.ID)
require.Empty(t, preview.Diagnostics)
require.Equal(t, "public_key", preview.Parameters[0].Name)
require.True(t, preview.Parameters[0].Value.Valid())
require.Equal(t, sshKey.PublicKey, preview.Parameters[0].Value.Value.AsString())
}
-133
View File
@@ -35,14 +35,10 @@ 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
@@ -270,135 +266,6 @@ 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
-72
View File
@@ -4,7 +4,6 @@ import (
"bytes"
"context"
"net/http"
"os"
"regexp"
"strings"
"testing"
@@ -28,7 +27,6 @@ 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) {
@@ -2134,73 +2132,3 @@ 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.TryReceive(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.TryReceive(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.TryReceive(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())
}
@@ -8,10 +8,6 @@ terraform {
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], "")
@@ -17,12 +17,12 @@
"provider_name": "registry.terraform.io/coder/coder",
"schema_version": 0,
"values": {
"id": "25e81ec3-0eb9-4ee3-8b6d-738b8552f7a9",
"name": "default",
"email": "default@example.com",
"id": "",
"name": "",
"email": "",
"groups": [],
"full_name": "default",
"login_type": null,
"full_name": "",
"login_type": "",
"rbac_roles": [],
"session_token": "",
"ssh_public_key": "",
@@ -74,19 +74,7 @@
"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"]
"attribute": ["groups"]
}
]
}
+14
View File
@@ -0,0 +1,14 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
}
}
data "coder_workspace_owner" "me" {}
data "coder_parameter" "public_key" {
name = "public_key"
default = data.coder_workspace_owner.me.ssh_public_key
}
+80
View File
@@ -0,0 +1,80 @@
{
"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": "",
"name": "",
"email": "",
"groups": [],
"full_name": "",
"login_type": "",
"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": ["ssh_public_key"]
}
]
}
+28
View File
@@ -0,0 +1,28 @@
package codersdk
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/coder/coder/v2/codersdk/wsjson"
previewtypes "github.com/coder/preview/types"
"github.com/coder/websocket"
)
// FriendlyDiagnostic is included to guarantee it is generated in the output
// types. This is used as the type override for `previewtypes.Diagnostic`.
type FriendlyDiagnostic = previewtypes.FriendlyDiagnostic
// NullHCLString is included to guarantee it is generated in the output
// types. This is used as the type override for `previewtypes.HCLString`.
type NullHCLString = previewtypes.NullHCLString
func (c *Client) TemplateVersionDynamicParameters(ctx context.Context, userID, version uuid.UUID) (*wsjson.Stream[DynamicParametersResponse, DynamicParametersRequest], error) {
conn, err := c.Dial(ctx, fmt.Sprintf("/api/v2/users/%s/templateversions/%s/parameters", userID, version), nil)
if err != nil {
return nil, err
}
return wsjson.NewStream[DynamicParametersResponse, DynamicParametersRequest](conn, websocket.MessageText, websocket.MessageText, c.Logger()), nil
}
-18
View File
@@ -10,9 +10,7 @@ import (
"github.com/google/uuid"
"github.com/coder/coder/v2/codersdk/wsjson"
previewtypes "github.com/coder/preview/types"
"github.com/coder/websocket"
)
type TemplateVersionWarning string
@@ -141,22 +139,6 @@ type DynamicParametersResponse struct {
// TODO: Workspace tags
}
// FriendlyDiagnostic is included to guarantee it is generated in the output
// types. This is used as the type override for `previewtypes.Diagnostic`.
type FriendlyDiagnostic = previewtypes.FriendlyDiagnostic
// NullHCLString is included to guarantee it is generated in the output
// types. This is used as the type override for `previewtypes.HCLString`.
type NullHCLString = previewtypes.NullHCLString
func (c *Client) TemplateVersionDynamicParameters(ctx context.Context, version uuid.UUID) (*wsjson.Stream[DynamicParametersResponse, DynamicParametersRequest], error) {
conn, err := c.Dial(ctx, fmt.Sprintf("/api/v2/templateversions/%s/dynamic-parameters", version), nil)
if err != nil {
return nil, err
}
return wsjson.NewStream[DynamicParametersResponse, DynamicParametersRequest](conn, websocket.MessageText, websocket.MessageText, c.Logger()), nil
}
// TemplateVersionParameters returns parameters a template version exposes.
func (c *Client) TemplateVersionRichParameters(ctx context.Context, version uuid.UUID) ([]TemplateVersionParameter, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s/rich-parameters", version), nil)
+27 -26
View File
@@ -2541,32 +2541,6 @@ Status Code **200**
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Open dynamic parameters WebSocket by template version
### Code samples
```shell
# Example request using curl
curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/dynamic-parameters \
-H 'Coder-Session-Token: API_KEY'
```
`GET /templateversions/{templateversion}/dynamic-parameters`
### Parameters
| Name | In | Type | Required | Description |
|-------------------|------|--------------|----------|---------------------|
| `templateversion` | path | string(uuid) | true | Template version ID |
### Responses
| Status | Meaning | Description | Schema |
|--------|--------------------------------------------------------------------------|---------------------|--------|
| 101 | [Switching Protocols](https://tools.ietf.org/html/rfc7231#section-6.2.2) | Switching Protocols | |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get external auth by template version
### Code samples
@@ -3325,3 +3299,30 @@ Status Code **200**
| `type` | `bool` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Open dynamic parameters WebSocket by template version
### Code samples
```shell
# Example request using curl
curl -X GET http://coder-server:8080/api/v2/users/{user}/templateversions/{templateversion}/parameters \
-H 'Coder-Session-Token: API_KEY'
```
`GET /users/{user}/templateversions/{templateversion}/parameters`
### Parameters
| Name | In | Type | Required | Description |
|-------------------|------|--------------|----------|---------------------|
| `user` | path | string(uuid) | true | Template version ID |
| `templateversion` | path | string(uuid) | true | Template version ID |
### Responses
| Status | Meaning | Description | Schema |
|--------|--------------------------------------------------------------------------|---------------------|--------|
| 101 | [Switching Protocols](https://tools.ietf.org/html/rfc7231#section-6.2.2) | Switching Protocols | |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
+3 -4
View File
@@ -139,7 +139,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/go-reap v0.0.0-20170704170343-bf58d8a43e7b
github.com/hashicorp/go-version v1.7.0
github.com/hashicorp/hc-install v0.9.1
github.com/hashicorp/hc-install v0.9.2
github.com/hashicorp/terraform-config-inspect v0.0.0-20211115214459-90acf1ca460f
github.com/hashicorp/terraform-json v0.24.0
github.com/hashicorp/yamux v0.1.2
@@ -245,7 +245,7 @@ require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/ProtonMail/go-crypto v1.1.5 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/akutz/memconn v0.1.0 // indirect
@@ -488,7 +488,7 @@ require (
)
require (
github.com/coder/preview v0.0.0-20250409162646-62939c63c71a
github.com/coder/preview v0.0.0-20250417203026-7edcb9e02d99
github.com/kylecarbs/aisdk-go v0.0.5
github.com/mark3labs/mcp-go v0.20.1
)
@@ -514,7 +514,6 @@ require (
github.com/hashicorp/go-getter v1.7.8 // indirect
github.com/hashicorp/go-safetemp v1.0.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/liamg/memoryfs v1.6.0 // indirect
github.com/moby/sys/user v0.3.0 // indirect
github.com/openai/openai-go v0.1.0-beta.6 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
+8 -8
View File
@@ -680,8 +680,8 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s=
github.com/SherClockHolmes/webpush-go v1.4.0/go.mod h1:XSq8pKX11vNV8MJEMwjrlTkxhAj1zKfxmyhdV7Pd6UA=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
@@ -907,8 +907,8 @@ github.com/coder/pq v1.10.5-0.20240813183442-0c420cb5a048 h1:3jzYUlGH7ZELIH4XggX
github.com/coder/pq v1.10.5-0.20240813183442-0c420cb5a048/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
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/preview v0.0.0-20250409162646-62939c63c71a h1:1fvDm7hpNwKDQhHpStp7p1W05/33nBwptGorugNaE94=
github.com/coder/preview v0.0.0-20250409162646-62939c63c71a/go.mod h1:H9BInar+i5VALTTQ9Ulxmn94Eo2fWEhoxd0S9WakDIs=
github.com/coder/preview v0.0.0-20250417203026-7edcb9e02d99 h1:ek8akG49hG304Dsj6T+k8qd4t4rEjUyJ97EiQ9xqkYA=
github.com/coder/preview v0.0.0-20250417203026-7edcb9e02d99/go.mod h1:nyq3UKfaBu4jmA6ddJH05kD5K6paHEMLpRmwLdYJctU=
github.com/coder/quartz v0.1.2 h1:PVhc9sJimTdKd3VbygXtS4826EOCpB1fXoRlLnCrE+s=
github.com/coder/quartz v0.1.2/go.mod h1:vsiCc+AHViMKH2CQpGIpFgdHIEQsxwm8yCscqKmzbRA=
github.com/coder/retry v1.5.1 h1:iWu8YnD8YqHs3XwqrqsjoBTAVqT9ml6z9ViJ2wlMiqc=
@@ -1369,16 +1369,16 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
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/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24=
github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I=
github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
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.22.0 h1:G5+4Sz6jYZfRYUCg6eQgDsqTzkNXV+fP8l+uRmZHj64=
github.com/hashicorp/terraform-exec v0.22.0/go.mod h1:bjVbsncaeh8jVdhttWYZuBGj21FcYw6Ia/XfHcNO7lQ=
github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEsiXmzATMW/I=
github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY=
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-go v0.26.0 h1:cuIzCv4qwigug3OS7iKhpGAbZTiypAfFQmw8aE65O2M=
+2 -2
View File
@@ -917,7 +917,7 @@ export const FeatureSets: FeatureSet[] = ["enterprise", "", "premium"];
// From codersdk/files.go
export const FormatZip = "zip";
// From codersdk/templateversions.go
// From codersdk/parameters.go
export interface FriendlyDiagnostic {
readonly severity: PreviewDiagnosticSeverityString;
readonly summary: string;
@@ -1401,7 +1401,7 @@ export interface NotificationsWebhookConfig {
readonly endpoint: string;
}
// From codersdk/templateversions.go
// From codersdk/parameters.go
export interface NullHCLString {
readonly value: string;
readonly valid: boolean;