mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
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:
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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?
|
||||
},
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
25
enterprise/coderd/testdata/parameters/ephemeral/main.tf
vendored
Normal file
25
enterprise/coderd/testdata/parameters/ephemeral/main.tf
vendored
Normal 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"
|
||||
}
|
20
enterprise/coderd/testdata/parameters/numbers/main.tf
vendored
Normal file
20
enterprise/coderd/testdata/parameters/numbers/main.tf
vendored
Normal 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
|
||||
}
|
||||
}
|
18
enterprise/coderd/testdata/parameters/regex/main.tf
vendored
Normal file
18
enterprise/coderd/testdata/parameters/regex/main.tf
vendored
Normal 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"
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user