chore: refactor dynamic parameters into dedicated package (#18420)

This PR extracts dynamic parameter rendering logic from
coderd/parameters.go into a new coderd/dynamicparameters package. Partly
for organization and maintainability, but primarily to be reused in
`wsbuilder` to be leveraged as validation.
This commit is contained in:
Steven Masley
2025-06-20 13:00:39 -05:00
committed by GitHub
parent 72f7d70bab
commit 9b5d49967c
10 changed files with 942 additions and 410 deletions

View File

@ -0,0 +1,129 @@
package coderd_test
import (
_ "embed"
"os"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
"github.com/coder/coder/v2/enterprise/coderd/license"
"github.com/coder/coder/v2/testutil"
"github.com/coder/websocket"
)
// TestDynamicParameterTemplate uses a template with some dynamic elements, and
// tests the parameters, values, etc are all as expected.
func TestDynamicParameterTemplate(t *testing.T) {
t.Parallel()
owner, _, api, first := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
Options: &coderdtest.Options{IncludeProvisionerDaemon: true},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureTemplateRBAC: 1,
},
},
})
orgID := first.OrganizationID
_, userData := coderdtest.CreateAnotherUser(t, owner, orgID)
templateAdmin, templateAdminData := coderdtest.CreateAnotherUser(t, owner, orgID, rbac.ScopedRoleOrgTemplateAdmin(orgID))
userAdmin, userAdminData := coderdtest.CreateAnotherUser(t, owner, orgID, rbac.ScopedRoleOrgUserAdmin(orgID))
_, auditorData := coderdtest.CreateAnotherUser(t, owner, orgID, rbac.ScopedRoleOrgAuditor(orgID))
coderdtest.CreateGroup(t, owner, orgID, "developer", auditorData, userData)
coderdtest.CreateGroup(t, owner, orgID, "admin", templateAdminData, userAdminData)
coderdtest.CreateGroup(t, owner, orgID, "auditor", auditorData, templateAdminData, userAdminData)
dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/dynamic/main.tf")
require.NoError(t, err)
_, version := coderdtest.DynamicParameterTemplate(t, templateAdmin, orgID, coderdtest.DynamicParameterTemplateParams{
MainTF: string(dynamicParametersTerraformSource),
Plan: nil,
ModulesArchive: nil,
StaticParams: nil,
})
_ = userAdmin
ctx := testutil.Context(t, testutil.WaitLong)
stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, userData.ID.String(), version.ID)
require.NoError(t, err)
defer func() {
_ = stream.Close(websocket.StatusNormalClosure)
// Wait until the cache ends up empty. This verifies the cache does not
// leak any files.
require.Eventually(t, func() bool {
return api.AGPL.FileCache.Count() == 0
}, testutil.WaitShort, testutil.IntervalFast, "file cache should be empty after the test")
}()
// Initial response
preview, pop := coderdtest.SynchronousStream(stream)
init := pop()
require.Len(t, init.Diagnostics, 0, "no top level diags")
coderdtest.AssertParameter(t, "isAdmin", init.Parameters).
Exists().Value("false")
coderdtest.AssertParameter(t, "adminonly", init.Parameters).
NotExists()
coderdtest.AssertParameter(t, "groups", init.Parameters).
Exists().Options(database.EveryoneGroup, "developer")
// Switch to an admin
resp, err := preview(codersdk.DynamicParametersRequest{
ID: 1,
Inputs: map[string]string{
"colors": `["red"]`,
"thing": "apple",
},
OwnerID: userAdminData.ID,
})
require.NoError(t, err)
require.Equal(t, resp.ID, 1)
require.Len(t, resp.Diagnostics, 0, "no top level diags")
coderdtest.AssertParameter(t, "isAdmin", resp.Parameters).
Exists().Value("true")
coderdtest.AssertParameter(t, "adminonly", resp.Parameters).
Exists()
coderdtest.AssertParameter(t, "groups", resp.Parameters).
Exists().Options(database.EveryoneGroup, "admin", "auditor")
coderdtest.AssertParameter(t, "colors", resp.Parameters).
Exists().Value(`["red"]`)
coderdtest.AssertParameter(t, "thing", resp.Parameters).
Exists().Value("apple").Options("apple", "ruby")
coderdtest.AssertParameter(t, "cool", resp.Parameters).
NotExists()
// Try some other colors
resp, err = preview(codersdk.DynamicParametersRequest{
ID: 2,
Inputs: map[string]string{
"colors": `["yellow", "blue"]`,
"thing": "banana",
},
OwnerID: userAdminData.ID,
})
require.NoError(t, err)
require.Equal(t, resp.ID, 2)
require.Len(t, resp.Diagnostics, 0, "no top level diags")
coderdtest.AssertParameter(t, "cool", resp.Parameters).
Exists()
coderdtest.AssertParameter(t, "isAdmin", resp.Parameters).
Exists().Value("true")
coderdtest.AssertParameter(t, "colors", resp.Parameters).
Exists().Value(`["yellow", "blue"]`)
coderdtest.AssertParameter(t, "thing", resp.Parameters).
Exists().Value("banana").Options("banana", "ocean", "sky")
}

View File

@ -31,7 +31,7 @@ func TestDynamicParametersOwnerGroups(t *testing.T) {
Options: &coderdtest.Options{IncludeProvisionerDaemon: true},
},
)
templateAdmin, templateAdminUser := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
templateAdmin, templateAdminUser := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.ScopedRoleOrgTemplateAdmin(owner.OrganizationID))
_, noGroupUser := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
// Create the group to be asserted
@ -79,10 +79,10 @@ func TestDynamicParametersOwnerGroups(t *testing.T) {
require.NoError(t, err)
defer stream.Close(websocket.StatusGoingAway)
previews := stream.Chan()
previews, pop := coderdtest.SynchronousStream(stream)
// Should automatically send a form state with all defaulted/empty values
preview := testutil.RequireReceive(ctx, t, previews)
preview := pop()
require.Equal(t, -1, preview.ID)
require.Empty(t, preview.Diagnostics)
require.Equal(t, "group", preview.Parameters[0].Name)
@ -90,12 +90,11 @@ func TestDynamicParametersOwnerGroups(t *testing.T) {
require.Equal(t, database.EveryoneGroup, preview.Parameters[0].Value.Value)
// Send a new value, and see it reflected
err = stream.Send(codersdk.DynamicParametersRequest{
preview, err = previews(codersdk.DynamicParametersRequest{
ID: 1,
Inputs: map[string]string{"group": group.Name},
})
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)
@ -103,12 +102,11 @@ func TestDynamicParametersOwnerGroups(t *testing.T) {
require.Equal(t, group.Name, preview.Parameters[0].Value.Value)
// Back to default
err = stream.Send(codersdk.DynamicParametersRequest{
preview, err = previews(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)

View File

@ -0,0 +1,103 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
version = "2.5.3"
}
}
}
data "coder_workspace_owner" "me" {}
locals {
isAdmin = contains(data.coder_workspace_owner.me.groups, "admin")
}
data "coder_parameter" "isAdmin" {
name = "isAdmin"
type = "bool"
form_type = "switch"
default = local.isAdmin
order = 1
}
data "coder_parameter" "adminonly" {
count = local.isAdmin ? 1 : 0
name = "adminonly"
form_type = "input"
type = "string"
default = "I am an admin!"
order = 2
}
data "coder_parameter" "groups" {
name = "groups"
type = "list(string)"
form_type = "multi-select"
default = jsonencode([data.coder_workspace_owner.me.groups[0]])
order = 50
dynamic "option" {
for_each = data.coder_workspace_owner.me.groups
content {
name = option.value
value = option.value
}
}
}
locals {
colors = {
"red" : ["apple", "ruby"]
"yellow" : ["banana"]
"blue" : ["ocean", "sky"]
}
}
data "coder_parameter" "colors" {
name = "colors"
type = "list(string)"
form_type = "multi-select"
order = 100
dynamic "option" {
for_each = keys(local.colors)
content {
name = option.value
value = option.value
}
}
}
locals {
selected = jsondecode(data.coder_parameter.colors.value)
things = flatten([
for color in local.selected : local.colors[color]
])
}
data "coder_parameter" "thing" {
name = "thing"
type = "string"
form_type = "dropdown"
order = 101
dynamic "option" {
for_each = local.things
content {
name = option.value
value = option.value
}
}
}
// Cool people like blue. Idk what to tell you.
data "coder_parameter" "cool" {
count = contains(local.selected, "blue") ? 1 : 0
name = "cool"
type = "bool"
form_type = "switch"
order = 102
default = "true"
}