mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
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:
129
enterprise/coderd/dynamicparameters_test.go
Normal file
129
enterprise/coderd/dynamicparameters_test.go
Normal 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")
|
||||
}
|
@ -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)
|
||||
|
103
enterprise/coderd/testdata/parameters/dynamic/main.tf
vendored
Normal file
103
enterprise/coderd/testdata/parameters/dynamic/main.tf
vendored
Normal 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"
|
||||
}
|
Reference in New Issue
Block a user