Files
coder/coderd/parameters_test.go
Steven Masley f6e4ba6ed9 chore: remove per request dynamic parameters opt in and rely on template (#18505)
When in experimental this was used as an escape hatch. Removed to be
consistent with the template author's intentions

Backwards compatible, removing an experimental api field that is no longer used.
2025-06-23 15:04:09 -05:00

418 lines
14 KiB
Go

package coderd_test
import (
"context"
"os"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/wsjson"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/provisioner/terraform"
provProto "github.com/coder/coder/v2/provisionerd/proto"
"github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/coder/v2/testutil"
"github.com/coder/websocket"
)
func TestDynamicParametersOwnerSSHPublicKey(t *testing.T) {
t.Parallel()
ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, ownerClient)
templateAdmin, _ := 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, codersdk.Me, 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)
}
func TestDynamicParametersWithTerraformValues(t *testing.T) {
t.Parallel()
t.Run("OK_Modules", func(t *testing.T) {
t.Parallel()
dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/modules/main.tf")
require.NoError(t, err)
modulesArchive, err := terraform.GetModulesArchive(os.DirFS("testdata/parameters/modules"))
require.NoError(t, err)
setup := setupDynamicParamsTest(t, setupDynamicParamsTestParams{
provisionerDaemonVersion: provProto.CurrentVersion.String(),
mainTF: dynamicParametersTerraformSource,
modulesArchive: modulesArchive,
plan: nil,
static: nil,
})
ctx := testutil.Context(t, testutil.WaitShort)
stream := setup.stream
previews := stream.Chan()
// Should see the output of the module represented
preview := testutil.RequireReceive(ctx, t, previews)
require.Equal(t, -1, preview.ID)
require.Empty(t, preview.Diagnostics)
require.Len(t, preview.Parameters, 2)
coderdtest.AssertParameter(t, "jetbrains_ide", preview.Parameters).
Exists().Value("CL")
coderdtest.AssertParameter(t, "region", preview.Parameters).
Exists().Value("na")
})
// OldProvisioners use the static parameters in the dynamic param flow
t.Run("OldProvisioner", func(t *testing.T) {
t.Parallel()
const defaultValue = "PS"
setup := setupDynamicParamsTest(t, setupDynamicParamsTestParams{
provisionerDaemonVersion: "1.4",
mainTF: nil,
modulesArchive: nil,
plan: nil,
static: []*proto.RichParameter{
{
Name: "jetbrains_ide",
Type: "string",
DefaultValue: defaultValue,
Icon: "",
Options: []*proto.RichParameterOption{
{
Name: "PHPStorm",
Description: "",
Value: defaultValue,
Icon: "",
},
{
Name: "Golang",
Description: "",
Value: "GO",
Icon: "",
},
},
ValidationRegex: "[PG][SO]",
ValidationError: "Regex check",
},
},
})
ctx := testutil.Context(t, testutil.WaitShort)
stream := setup.stream
previews := stream.Chan()
// Assert the initial state
preview := testutil.RequireReceive(ctx, t, previews)
diagCount := len(preview.Diagnostics)
require.Equal(t, 1, diagCount)
require.Contains(t, preview.Diagnostics[0].Summary, "required metadata to support dynamic parameters")
require.Len(t, preview.Parameters, 1)
require.Equal(t, "jetbrains_ide", preview.Parameters[0].Name)
require.True(t, preview.Parameters[0].Value.Valid)
require.Equal(t, defaultValue, preview.Parameters[0].Value.Value)
// Test some inputs
for _, exp := range []string{defaultValue, "GO", "Invalid", defaultValue} {
inputs := map[string]string{}
if exp != defaultValue {
// Let the default value be the default without being explicitly set
inputs["jetbrains_ide"] = exp
}
err := stream.Send(codersdk.DynamicParametersRequest{
ID: 1,
Inputs: inputs,
})
require.NoError(t, err)
preview := testutil.RequireReceive(ctx, t, previews)
diagCount := len(preview.Diagnostics)
require.Equal(t, 1, diagCount)
require.Contains(t, preview.Diagnostics[0].Summary, "required metadata to support dynamic parameters")
require.Len(t, preview.Parameters, 1)
if exp == "Invalid" { // Try an invalid option
require.Len(t, preview.Parameters[0].Diagnostics, 1)
} else {
require.Len(t, preview.Parameters[0].Diagnostics, 0)
}
require.Equal(t, "jetbrains_ide", preview.Parameters[0].Name)
require.True(t, preview.Parameters[0].Value.Valid)
require.Equal(t, exp, preview.Parameters[0].Value.Value)
}
})
t.Run("FileError", func(t *testing.T) {
// Verify files close even if the websocket terminates from an error
t.Parallel()
db, ps := dbtestutil.NewDB(t)
dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/modules/main.tf")
require.NoError(t, err)
modulesArchive, err := terraform.GetModulesArchive(os.DirFS("testdata/parameters/modules"))
require.NoError(t, err)
setup := setupDynamicParamsTest(t, setupDynamicParamsTestParams{
db: &dbRejectGitSSHKey{Store: db},
ps: ps,
provisionerDaemonVersion: provProto.CurrentVersion.String(),
mainTF: dynamicParametersTerraformSource,
modulesArchive: modulesArchive,
})
stream := setup.stream
previews := stream.Chan()
// Assert the failed owner
ctx := testutil.Context(t, testutil.WaitShort)
preview := testutil.RequireReceive(ctx, t, previews)
require.Len(t, preview.Diagnostics, 1)
require.Equal(t, preview.Diagnostics[0].Summary, "Failed to fetch workspace owner")
})
t.Run("RebuildParameters", func(t *testing.T) {
t.Parallel()
dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/modules/main.tf")
require.NoError(t, err)
modulesArchive, err := terraform.GetModulesArchive(os.DirFS("testdata/parameters/modules"))
require.NoError(t, err)
setup := setupDynamicParamsTest(t, setupDynamicParamsTestParams{
provisionerDaemonVersion: provProto.CurrentVersion.String(),
mainTF: dynamicParametersTerraformSource,
modulesArchive: modulesArchive,
plan: nil,
static: nil,
})
ctx := testutil.Context(t, testutil.WaitMedium)
stream := setup.stream
previews := stream.Chan()
// Should see the output of the module represented
preview := testutil.RequireReceive(ctx, t, previews)
require.Equal(t, -1, preview.ID)
require.Empty(t, preview.Diagnostics)
require.Len(t, preview.Parameters, 2)
coderdtest.AssertParameter(t, "jetbrains_ide", preview.Parameters).
Exists().Value("CL")
coderdtest.AssertParameter(t, "region", preview.Parameters).
Exists().Value("na")
_ = stream.Close(websocket.StatusGoingAway)
wrk := coderdtest.CreateWorkspace(t, setup.client, setup.template.ID, func(request *codersdk.CreateWorkspaceRequest) {
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
{
Name: preview.Parameters[0].Name,
Value: "GO",
},
{
Name: preview.Parameters[1].Name,
Value: "eu",
},
}
})
coderdtest.AwaitWorkspaceBuildJobCompleted(t, setup.client, wrk.LatestBuild.ID)
params, err := setup.client.WorkspaceBuildParameters(ctx, wrk.LatestBuild.ID)
require.NoError(t, err)
require.ElementsMatch(t, []codersdk.WorkspaceBuildParameter{
{Name: "jetbrains_ide", Value: "GO"}, {Name: "region", Value: "eu"},
}, params)
regionOptions := []string{"na", "af", "sa", "as"}
// A helper function to assert params
doTransition := func(t *testing.T, trans codersdk.WorkspaceTransition) {
t.Helper()
regionVal := regionOptions[0]
regionOptions = regionOptions[1:] // Choose the next region on the next build
bld, err := setup.client.CreateWorkspaceBuild(ctx, wrk.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: setup.template.ActiveVersionID,
Transition: trans,
RichParameterValues: []codersdk.WorkspaceBuildParameter{
{Name: "region", Value: regionVal},
},
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, setup.client, bld.ID)
latestParams, err := setup.client.WorkspaceBuildParameters(ctx, bld.ID)
require.NoError(t, err)
require.ElementsMatch(t, latestParams, []codersdk.WorkspaceBuildParameter{
{Name: "jetbrains_ide", Value: "GO"},
{Name: "region", Value: regionVal},
})
}
// Restart the workspace, then delete. Asserting params on all builds.
doTransition(t, codersdk.WorkspaceTransitionStop)
doTransition(t, codersdk.WorkspaceTransitionStart)
doTransition(t, codersdk.WorkspaceTransitionDelete)
})
t.Run("BadOwner", func(t *testing.T) {
t.Parallel()
dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/modules/main.tf")
require.NoError(t, err)
modulesArchive, err := terraform.GetModulesArchive(os.DirFS("testdata/parameters/modules"))
require.NoError(t, err)
setup := setupDynamicParamsTest(t, setupDynamicParamsTestParams{
provisionerDaemonVersion: provProto.CurrentVersion.String(),
mainTF: dynamicParametersTerraformSource,
modulesArchive: modulesArchive,
plan: nil,
static: nil,
})
ctx := testutil.Context(t, testutil.WaitShort)
stream := setup.stream
previews := stream.Chan()
// Should see the output of the module represented
preview := testutil.RequireReceive(ctx, t, previews)
require.Equal(t, -1, preview.ID)
require.Empty(t, preview.Diagnostics)
err = stream.Send(codersdk.DynamicParametersRequest{
ID: 1,
Inputs: map[string]string{
"jetbrains_ide": "GO",
},
OwnerID: uuid.New(),
})
require.NoError(t, err)
preview = testutil.RequireReceive(ctx, t, previews)
require.Equal(t, 1, preview.ID)
require.Len(t, preview.Diagnostics, 1)
require.Equal(t, preview.Diagnostics[0].Extra.Code, "owner_not_found")
})
}
type setupDynamicParamsTestParams struct {
db database.Store
ps pubsub.Pubsub
provisionerDaemonVersion string
mainTF []byte
modulesArchive []byte
plan []byte
static []*proto.RichParameter
expectWebsocketError bool
}
type dynamicParamsTest struct {
client *codersdk.Client
api *coderd.API
stream *wsjson.Stream[codersdk.DynamicParametersResponse, codersdk.DynamicParametersRequest]
template codersdk.Template
}
func setupDynamicParamsTest(t *testing.T, args setupDynamicParamsTestParams) dynamicParamsTest {
ownerClient, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
Database: args.db,
Pubsub: args.ps,
IncludeProvisionerDaemon: true,
ProvisionerDaemonVersion: args.provisionerDaemonVersion,
})
owner := coderdtest.CreateFirstUser(t, ownerClient)
templateAdmin, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
tpl, version := coderdtest.DynamicParameterTemplate(t, templateAdmin, owner.OrganizationID, coderdtest.DynamicParameterTemplateParams{
MainTF: string(args.mainTF),
Plan: args.plan,
ModulesArchive: args.modulesArchive,
StaticParams: args.static,
})
ctx := testutil.Context(t, testutil.WaitShort)
stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, codersdk.Me, version.ID)
if args.expectWebsocketError {
require.Errorf(t, err, "expected error forming websocket")
} else {
require.NoError(t, err)
}
t.Cleanup(func() {
if stream != nil {
_ = stream.Close(websocket.StatusGoingAway)
}
// Cache should always have 0 files when the only stream is closed
require.Eventually(t, func() bool {
return api.FileCache.Count() == 0
}, testutil.WaitShort/5, testutil.IntervalMedium)
})
return dynamicParamsTest{
client: ownerClient,
api: api,
stream: stream,
template: tpl,
}
}
// dbRejectGitSSHKey is a cheeky way to force an error to occur in a place
// that is generally impossible to force an error.
type dbRejectGitSSHKey struct {
database.Store
}
func (*dbRejectGitSSHKey) GetGitSSHKey(_ context.Context, _ uuid.UUID) (database.GitSSHKey, error) {
return database.GitSSHKey{}, xerrors.New("forcing a fake error")
}