feat: implement dynamic parameter validation (#18482)

# What does this do?

This does parameter validation for dynamic parameters in `wsbuilder`. All input parameters are validated in `coder/coder` before being sent to terraform.

The heart of this PR is [`ResolveParameters`](b65001e89c/coderd/dynamicparameters/resolver.go (L30-L30)).

# What else changes?

`wsbuilder` now needs to load the terraform files into memory to succeed. This does add a larger memory requirement to workspace builds.

# Future work

- Sort autostart handling workspaces by template version id. So workspaces with the same template version only load the terraform files once from the db, and store them in the cache.
This commit is contained in:
Steven Masley
2025-06-23 12:35:15 -05:00
committed by GitHub
parent 7254c08af4
commit 82af2e019d
23 changed files with 961 additions and 159 deletions

View File

@ -1164,7 +1164,7 @@ func (api *API) setupPrebuilds(featureEnabled bool) (agplprebuilds.Reconciliatio
return agplprebuilds.DefaultReconciler, agplprebuilds.DefaultClaimer
}
reconciler := prebuilds.NewStoreReconciler(api.Database, api.Pubsub, api.DeploymentValues.Prebuilds,
reconciler := prebuilds.NewStoreReconciler(api.Database, api.Pubsub, api.AGPL.FileCache, api.DeploymentValues.Prebuilds,
api.Logger.Named("prebuilds"), quartz.NewReal(), api.PrometheusRegistry, api.NotificationsEnqueuer)
return reconciler, prebuilds.NewEnterpriseClaimer(api.Database)
}

View File

@ -1,15 +1,19 @@
package coderd_test
import (
"context"
_ "embed"
"os"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"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/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
"github.com/coder/coder/v2/enterprise/coderd/license"
@ -17,6 +21,289 @@ import (
"github.com/coder/websocket"
)
func TestDynamicParameterBuild(t *testing.T) {
t.Parallel()
owner, _, _, first := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
Options: &coderdtest.Options{IncludeProvisionerDaemon: true},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureTemplateRBAC: 1,
},
},
})
orgID := first.OrganizationID
templateAdmin, templateAdminData := coderdtest.CreateAnotherUser(t, owner, orgID, rbac.ScopedRoleOrgTemplateAdmin(orgID))
coderdtest.CreateGroup(t, owner, orgID, "developer")
coderdtest.CreateGroup(t, owner, orgID, "admin", templateAdminData)
coderdtest.CreateGroup(t, owner, orgID, "auditor")
// Create a set of templates to test with
numberValidation, _ := coderdtest.DynamicParameterTemplate(t, templateAdmin, orgID, coderdtest.DynamicParameterTemplateParams{
MainTF: string(must(os.ReadFile("testdata/parameters/numbers/main.tf"))),
})
regexValidation, _ := coderdtest.DynamicParameterTemplate(t, templateAdmin, orgID, coderdtest.DynamicParameterTemplateParams{
MainTF: string(must(os.ReadFile("testdata/parameters/regex/main.tf"))),
})
ephemeralValidation, _ := coderdtest.DynamicParameterTemplate(t, templateAdmin, orgID, coderdtest.DynamicParameterTemplateParams{
MainTF: string(must(os.ReadFile("testdata/parameters/ephemeral/main.tf"))),
})
// complexValidation does conditional parameters, conditional options, and more.
complexValidation, _ := coderdtest.DynamicParameterTemplate(t, templateAdmin, orgID, coderdtest.DynamicParameterTemplateParams{
MainTF: string(must(os.ReadFile("testdata/parameters/dynamic/main.tf"))),
})
t.Run("NumberValidation", func(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
wrk, err := templateAdmin.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateID: numberValidation.ID,
Name: coderdtest.RandomUsername(t),
RichParameterValues: []codersdk.WorkspaceBuildParameter{
{Name: "number", Value: `7`},
},
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, templateAdmin, wrk.LatestBuild.ID)
})
t.Run("TooLow", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
_, err := templateAdmin.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateID: numberValidation.ID,
Name: coderdtest.RandomUsername(t),
RichParameterValues: []codersdk.WorkspaceBuildParameter{
{Name: "number", Value: `-10`},
},
})
require.ErrorContains(t, err, "Number must be between 0 and 10")
})
t.Run("TooHigh", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
_, err := templateAdmin.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateID: numberValidation.ID,
Name: coderdtest.RandomUsername(t),
RichParameterValues: []codersdk.WorkspaceBuildParameter{
{Name: "number", Value: `15`},
},
})
require.ErrorContains(t, err, "Number must be between 0 and 10")
})
})
t.Run("RegexValidation", func(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
wrk, err := templateAdmin.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateID: regexValidation.ID,
Name: coderdtest.RandomUsername(t),
RichParameterValues: []codersdk.WorkspaceBuildParameter{
{Name: "string", Value: `Hello World!`},
},
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, templateAdmin, wrk.LatestBuild.ID)
})
t.Run("NoValue", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
_, err := templateAdmin.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateID: regexValidation.ID,
Name: coderdtest.RandomUsername(t),
RichParameterValues: []codersdk.WorkspaceBuildParameter{},
})
require.ErrorContains(t, err, "All messages must start with 'Hello'")
})
t.Run("Invalid", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
_, err := templateAdmin.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateID: regexValidation.ID,
Name: coderdtest.RandomUsername(t),
RichParameterValues: []codersdk.WorkspaceBuildParameter{
{Name: "string", Value: `Goodbye!`},
},
})
require.ErrorContains(t, err, "All messages must start with 'Hello'")
})
})
t.Run("EphemeralValidation", func(t *testing.T) {
t.Parallel()
t.Run("OK_EphemeralNoPrevious", func(t *testing.T) {
t.Parallel()
// Ephemeral params do not take the previous values into account.
ctx := testutil.Context(t, testutil.WaitShort)
wrk, err := templateAdmin.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateID: ephemeralValidation.ID,
Name: coderdtest.RandomUsername(t),
RichParameterValues: []codersdk.WorkspaceBuildParameter{
{Name: "required", Value: `Hello World!`},
{Name: "defaulted", Value: `Changed`},
},
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, templateAdmin, wrk.LatestBuild.ID)
assertWorkspaceBuildParameters(ctx, t, templateAdmin, wrk.LatestBuild.ID, map[string]string{
"required": "Hello World!",
"defaulted": "Changed",
})
bld, err := templateAdmin.CreateWorkspaceBuild(ctx, wrk.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
RichParameterValues: []codersdk.WorkspaceBuildParameter{
{Name: "required", Value: `Hello World, Again!`},
},
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, templateAdmin, bld.ID)
assertWorkspaceBuildParameters(ctx, t, templateAdmin, bld.ID, map[string]string{
"required": "Hello World, Again!",
"defaulted": "original", // Reverts back to the original default value.
})
})
t.Run("Immutable", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
wrk, err := templateAdmin.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateID: numberValidation.ID,
Name: coderdtest.RandomUsername(t),
RichParameterValues: []codersdk.WorkspaceBuildParameter{
{Name: "number", Value: `7`},
},
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, templateAdmin, wrk.LatestBuild.ID)
assertWorkspaceBuildParameters(ctx, t, templateAdmin, wrk.LatestBuild.ID, map[string]string{
"number": "7",
})
_, err = templateAdmin.CreateWorkspaceBuild(ctx, wrk.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
RichParameterValues: []codersdk.WorkspaceBuildParameter{
{Name: "number", Value: `8`},
},
})
require.ErrorContains(t, err, `Parameter "number" is not mutable`)
})
t.Run("RequiredMissing", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
_, err := templateAdmin.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateID: ephemeralValidation.ID,
Name: coderdtest.RandomUsername(t),
RichParameterValues: []codersdk.WorkspaceBuildParameter{},
})
require.ErrorContains(t, err, "Required parameter not provided")
})
})
t.Run("ComplexValidation", func(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
wrk, err := templateAdmin.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateID: complexValidation.ID,
Name: coderdtest.RandomUsername(t),
RichParameterValues: []codersdk.WorkspaceBuildParameter{
{Name: "groups", Value: `["admin"]`},
{Name: "colors", Value: `["red"]`},
{Name: "thing", Value: "apple"},
},
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, templateAdmin, wrk.LatestBuild.ID)
})
t.Run("BadGroup", func(t *testing.T) {
// Template admin is not in the "auditor" group, so this should fail.
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
_, err := templateAdmin.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateID: complexValidation.ID,
Name: coderdtest.RandomUsername(t),
RichParameterValues: []codersdk.WorkspaceBuildParameter{
{Name: "groups", Value: `["auditor", "admin"]`},
{Name: "colors", Value: `["red"]`},
{Name: "thing", Value: "apple"},
},
})
require.ErrorContains(t, err, "is not a valid option")
})
t.Run("BadColor", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
_, err := templateAdmin.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateID: complexValidation.ID,
Name: coderdtest.RandomUsername(t),
RichParameterValues: []codersdk.WorkspaceBuildParameter{
{Name: "groups", Value: `["admin"]`},
{Name: "colors", Value: `["purple"]`},
},
})
require.ErrorContains(t, err, "is not a valid option")
require.ErrorContains(t, err, "purple")
})
t.Run("BadThing", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
_, err := templateAdmin.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateID: complexValidation.ID,
Name: coderdtest.RandomUsername(t),
RichParameterValues: []codersdk.WorkspaceBuildParameter{
{Name: "groups", Value: `["admin"]`},
{Name: "colors", Value: `["red"]`},
{Name: "thing", Value: "leaf"},
},
})
require.ErrorContains(t, err, "must be defined as one of options")
require.ErrorContains(t, err, "leaf")
})
t.Run("BadNumber", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
_, err := templateAdmin.CreateUserWorkspace(ctx, codersdk.Me, codersdk.CreateWorkspaceRequest{
TemplateID: complexValidation.ID,
Name: coderdtest.RandomUsername(t),
RichParameterValues: []codersdk.WorkspaceBuildParameter{
{Name: "groups", Value: `["admin"]`},
{Name: "colors", Value: `["green"]`},
{Name: "thing", Value: "leaf"},
{Name: "number", Value: "100"},
},
})
require.ErrorContains(t, err, "Number must be between 0 and 10")
})
})
}
// TestDynamicParameterTemplate uses a template with some dynamic elements, and
// tests the parameters, values, etc are all as expected.
func TestDynamicParameterTemplate(t *testing.T) {
@ -127,3 +414,27 @@ func TestDynamicParameterTemplate(t *testing.T) {
coderdtest.AssertParameter(t, "thing", resp.Parameters).
Exists().Value("banana").Options("banana", "ocean", "sky")
}
func assertWorkspaceBuildParameters(ctx context.Context, t *testing.T, client *codersdk.Client, buildID uuid.UUID, values map[string]string) {
t.Helper()
params, err := client.WorkspaceBuildParameters(ctx, buildID)
require.NoError(t, err)
for name, value := range values {
param, ok := slice.Find(params, func(parameter codersdk.WorkspaceBuildParameter) bool {
return parameter.Name == name
})
if !ok {
assert.Failf(t, "parameter not found", "expected parameter %q to exist with value %q", name, value)
continue
}
assert.Equalf(t, value, param.Value, "parameter %q should have value %q", name, value)
}
for _, param := range params {
if _, ok := values[param.Name]; !ok {
assert.Failf(t, "unexpected parameter", "parameter %q should not exist", param.Name)
}
}
}

View File

@ -15,6 +15,7 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/files"
"github.com/coder/quartz"
"github.com/coder/coder/v2/coderd/coderdtest"
@ -164,7 +165,8 @@ func TestClaimPrebuild(t *testing.T) {
})
defer provisionerCloser.Close()
reconciler := prebuilds.NewStoreReconciler(spy, pubsub, codersdk.PrebuildsConfig{}, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer())
cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
reconciler := prebuilds.NewStoreReconciler(spy, pubsub, cache, codersdk.PrebuildsConfig{}, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer())
var claimer agplprebuilds.Claimer = prebuilds.NewEnterpriseClaimer(spy)
api.AGPL.PrebuildsClaimer.Store(&claimer)

View File

@ -13,6 +13,8 @@ import (
prometheus_client "github.com/prometheus/client_model/go"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/files"
"github.com/coder/quartz"
"github.com/coder/coder/v2/coderd/database"
@ -198,7 +200,8 @@ func TestMetricsCollector(t *testing.T) {
})
clock := quartz.NewMock(t)
db, pubsub := dbtestutil.NewDB(t)
reconciler := prebuilds.NewStoreReconciler(db, pubsub, codersdk.PrebuildsConfig{}, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer())
cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
reconciler := prebuilds.NewStoreReconciler(db, pubsub, cache, codersdk.PrebuildsConfig{}, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer())
ctx := testutil.Context(t, testutil.WaitLong)
createdUsers := []uuid.UUID{database.PrebuildsSystemUserID}
@ -334,7 +337,8 @@ func TestMetricsCollector_DuplicateTemplateNames(t *testing.T) {
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
clock := quartz.NewMock(t)
db, pubsub := dbtestutil.NewDB(t)
reconciler := prebuilds.NewStoreReconciler(db, pubsub, codersdk.PrebuildsConfig{}, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer())
cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
reconciler := prebuilds.NewStoreReconciler(db, pubsub, cache, codersdk.PrebuildsConfig{}, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer())
ctx := testutil.Context(t, testutil.WaitLong)
collector := prebuilds.NewMetricsCollector(db, logger, reconciler)

View File

@ -14,6 +14,7 @@ import (
"github.com/hashicorp/go-multierror"
"github.com/prometheus/client_golang/prometheus"
"github.com/coder/coder/v2/coderd/files"
"github.com/coder/quartz"
"github.com/coder/coder/v2/coderd/audit"
@ -40,6 +41,7 @@ type StoreReconciler struct {
store database.Store
cfg codersdk.PrebuildsConfig
pubsub pubsub.Pubsub
fileCache *files.Cache
logger slog.Logger
clock quartz.Clock
registerer prometheus.Registerer
@ -57,6 +59,7 @@ var _ prebuilds.ReconciliationOrchestrator = &StoreReconciler{}
func NewStoreReconciler(store database.Store,
ps pubsub.Pubsub,
fileCache *files.Cache,
cfg codersdk.PrebuildsConfig,
logger slog.Logger,
clock quartz.Clock,
@ -66,6 +69,7 @@ func NewStoreReconciler(store database.Store,
reconciler := &StoreReconciler{
store: store,
pubsub: ps,
fileCache: fileCache,
logger: logger,
cfg: cfg,
clock: clock,
@ -780,6 +784,7 @@ func (c *StoreReconciler) provision(
_, provisionerJob, _, err := builder.Build(
ctx,
db,
c.fileCache,
func(_ policy.Action, _ rbac.Objecter) bool {
return true // TODO: harden?
},

View File

@ -13,7 +13,9 @@ import (
"github.com/stretchr/testify/assert"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/files"
"github.com/coder/coder/v2/coderd/notifications"
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
"github.com/coder/coder/v2/coderd/util/slice"
@ -53,7 +55,8 @@ func TestNoReconciliationActionsIfNoPresets(t *testing.T) {
ReconciliationInterval: serpent.Duration(testutil.WaitLong),
}
logger := testutil.Logger(t)
controller := prebuilds.NewStoreReconciler(db, ps, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer())
cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
controller := prebuilds.NewStoreReconciler(db, ps, cache, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer())
// given a template version with no presets
org := dbgen.Organization(t, db, database.Organization{})
@ -98,7 +101,8 @@ func TestNoReconciliationActionsIfNoPrebuilds(t *testing.T) {
ReconciliationInterval: serpent.Duration(testutil.WaitLong),
}
logger := testutil.Logger(t)
controller := prebuilds.NewStoreReconciler(db, ps, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer())
cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
controller := prebuilds.NewStoreReconciler(db, ps, cache, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer())
// given there are presets, but no prebuilds
org := dbgen.Organization(t, db, database.Organization{})
@ -375,7 +379,8 @@ func TestPrebuildReconciliation(t *testing.T) {
if useBrokenPubsub {
pubSub = &brokenPublisher{Pubsub: pubSub}
}
controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer())
cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
controller := prebuilds.NewStoreReconciler(db, pubSub, cache, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer())
// Run the reconciliation multiple times to ensure idempotency
// 8 was arbitrary, but large enough to reasonably trust the result
@ -452,7 +457,8 @@ func TestMultiplePresetsPerTemplateVersion(t *testing.T) {
t, &slogtest.Options{IgnoreErrors: true},
).Leveled(slog.LevelDebug)
db, pubSub := dbtestutil.NewDB(t)
controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer())
cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
controller := prebuilds.NewStoreReconciler(db, pubSub, cache, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer())
ownerID := uuid.New()
dbgen.User(t, db, database.User{
@ -577,7 +583,8 @@ func TestPrebuildScheduling(t *testing.T) {
t, &slogtest.Options{IgnoreErrors: true},
).Leveled(slog.LevelDebug)
db, pubSub := dbtestutil.NewDB(t)
controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, clock, prometheus.NewRegistry(), newNoopEnqueuer())
cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
controller := prebuilds.NewStoreReconciler(db, pubSub, cache, cfg, logger, clock, prometheus.NewRegistry(), newNoopEnqueuer())
ownerID := uuid.New()
dbgen.User(t, db, database.User{
@ -681,7 +688,8 @@ func TestInvalidPreset(t *testing.T) {
t, &slogtest.Options{IgnoreErrors: true},
).Leveled(slog.LevelDebug)
db, pubSub := dbtestutil.NewDB(t)
controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer())
cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
controller := prebuilds.NewStoreReconciler(db, pubSub, cache, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer())
ownerID := uuid.New()
dbgen.User(t, db, database.User{
@ -745,7 +753,8 @@ func TestDeletionOfPrebuiltWorkspaceWithInvalidPreset(t *testing.T) {
t, &slogtest.Options{IgnoreErrors: true},
).Leveled(slog.LevelDebug)
db, pubSub := dbtestutil.NewDB(t)
controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer())
cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
controller := prebuilds.NewStoreReconciler(db, pubSub, cache, cfg, logger, quartz.NewMock(t), prometheus.NewRegistry(), newNoopEnqueuer())
ownerID := uuid.New()
dbgen.User(t, db, database.User{
@ -841,7 +850,8 @@ func TestSkippingHardLimitedPresets(t *testing.T) {
db, pubSub := dbtestutil.NewDB(t)
fakeEnqueuer := newFakeEnqueuer()
registry := prometheus.NewRegistry()
controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, clock, registry, fakeEnqueuer)
cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
controller := prebuilds.NewStoreReconciler(db, pubSub, cache, cfg, logger, clock, registry, fakeEnqueuer)
// Template admin to receive a notification.
templateAdmin := dbgen.User(t, db, database.User{
@ -1003,7 +1013,8 @@ func TestHardLimitedPresetShouldNotBlockDeletion(t *testing.T) {
db, pubSub := dbtestutil.NewDB(t)
fakeEnqueuer := newFakeEnqueuer()
registry := prometheus.NewRegistry()
controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, clock, registry, fakeEnqueuer)
cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
controller := prebuilds.NewStoreReconciler(db, pubSub, cache, cfg, logger, clock, registry, fakeEnqueuer)
// Template admin to receive a notification.
templateAdmin := dbgen.User(t, db, database.User{
@ -1215,7 +1226,8 @@ func TestRunLoop(t *testing.T) {
t, &slogtest.Options{IgnoreErrors: true},
).Leveled(slog.LevelDebug)
db, pubSub := dbtestutil.NewDB(t)
reconciler := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, clock, prometheus.NewRegistry(), newNoopEnqueuer())
cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
reconciler := prebuilds.NewStoreReconciler(db, pubSub, cache, cfg, logger, clock, prometheus.NewRegistry(), newNoopEnqueuer())
ownerID := uuid.New()
dbgen.User(t, db, database.User{
@ -1345,7 +1357,8 @@ func TestFailedBuildBackoff(t *testing.T) {
t, &slogtest.Options{IgnoreErrors: true},
).Leveled(slog.LevelDebug)
db, ps := dbtestutil.NewDB(t)
reconciler := prebuilds.NewStoreReconciler(db, ps, cfg, logger, clock, prometheus.NewRegistry(), newNoopEnqueuer())
cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
reconciler := prebuilds.NewStoreReconciler(db, ps, cache, cfg, logger, clock, prometheus.NewRegistry(), newNoopEnqueuer())
// Given: an active template version with presets and prebuilds configured.
const desiredInstances = 2
@ -1461,9 +1474,11 @@ func TestReconciliationLock(t *testing.T) {
wg.Add(1)
go func() {
defer wg.Done()
cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
reconciler := prebuilds.NewStoreReconciler(
db,
ps,
cache,
codersdk.PrebuildsConfig{},
slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug),
quartz.NewMock(t),
@ -1501,7 +1516,8 @@ func TestTrackResourceReplacement(t *testing.T) {
fakeEnqueuer := newFakeEnqueuer()
registry := prometheus.NewRegistry()
reconciler := prebuilds.NewStoreReconciler(db, ps, codersdk.PrebuildsConfig{}, logger, clock, registry, fakeEnqueuer)
cache := files.New(registry, &coderdtest.FakeAuthorizer{})
reconciler := prebuilds.NewStoreReconciler(db, ps, cache, codersdk.PrebuildsConfig{}, logger, clock, registry, fakeEnqueuer)
// Given: a template admin to receive a notification.
templateAdmin := dbgen.User(t, db, database.User{
@ -1656,7 +1672,8 @@ func TestExpiredPrebuildsMultipleActions(t *testing.T) {
db, pubSub := dbtestutil.NewDB(t)
fakeEnqueuer := newFakeEnqueuer()
registry := prometheus.NewRegistry()
controller := prebuilds.NewStoreReconciler(db, pubSub, cfg, logger, clock, registry, fakeEnqueuer)
cache := files.New(registry, &coderdtest.FakeAuthorizer{})
controller := prebuilds.NewStoreReconciler(db, pubSub, cache, cfg, logger, clock, registry, fakeEnqueuer)
// Set up test environment with a template, version, and preset
ownerID := uuid.New()

View File

@ -1,8 +1,7 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
version = "2.5.3"
source = "coder/coder"
}
}
}
@ -52,6 +51,7 @@ locals {
"red" : ["apple", "ruby"]
"yellow" : ["banana"]
"blue" : ["ocean", "sky"]
"green" : ["grass", "leaf"]
}
}
@ -101,3 +101,15 @@ data "coder_parameter" "cool" {
order = 102
default = "true"
}
data "coder_parameter" "number" {
count = contains(local.selected, "green") ? 1 : 0
name = "number"
type = "number"
order = 103
validation {
error = "Number must be between 0 and 10"
min = 0
max = 10
}
}

View File

@ -0,0 +1,25 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
}
}
data "coder_workspace_owner" "me" {}
data "coder_parameter" "required" {
name = "required"
type = "string"
mutable = true
ephemeral = true
}
data "coder_parameter" "defaulted" {
name = "defaulted"
type = "string"
mutable = true
ephemeral = true
default = "original"
}

View File

@ -0,0 +1,20 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
}
}
data "coder_workspace_owner" "me" {}
data "coder_parameter" "number" {
name = "number"
type = "number"
mutable = false
validation {
error = "Number must be between 0 and 10"
min = 0
max = 10
}
}

View File

@ -0,0 +1,18 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
}
}
data "coder_workspace_owner" "me" {}
data "coder_parameter" "string" {
name = "string"
type = "string"
validation {
error = "All messages must start with 'Hello'"
regex = "^Hello"
}
}