chore: disable parameter validatation for dynamic params for all transitions (#17926)

Dynamic params skip parameter validation in coder/coder.
This is because conditional parameters cannot be validated 
with the static parameters in the database.
This commit is contained in:
Steven Masley
2025-05-20 10:09:53 -05:00
committed by GitHub
parent 93f17bc73e
commit e76d58f2b6
15 changed files with 258 additions and 17 deletions

4
coderd/apidoc/docs.go generated
View File

@ -11998,6 +11998,10 @@ const docTemplate = `{
"dry_run": {
"type": "boolean"
},
"enable_dynamic_parameters": {
"description": "EnableDynamicParameters skips some of the static parameter checking.\nIt will default to whatever the template has marked as the default experience.\nRequires the \"dynamic-experiment\" to be used.",
"type": "boolean"
},
"log_level": {
"description": "Log level changes the default logging verbosity of a provider (\"info\" if empty).",
"enum": [

View File

@ -10716,6 +10716,10 @@
"dry_run": {
"type": "boolean"
},
"enable_dynamic_parameters": {
"description": "EnableDynamicParameters skips some of the static parameter checking.\nIt will default to whatever the template has marked as the default experience.\nRequires the \"dynamic-experiment\" to be used.",
"type": "boolean"
},
"log_level": {
"description": "Log level changes the default logging verbosity of a provider (\"info\" if empty).",
"enum": ["debug"],

View File

@ -27,6 +27,7 @@ import (
"github.com/coder/coder/v2/coderd/notifications"
"github.com/coder/coder/v2/coderd/schedule"
"github.com/coder/coder/v2/coderd/wsbuilder"
"github.com/coder/coder/v2/codersdk"
)
// Executor automatically starts or stops workspaces.
@ -43,6 +44,7 @@ type Executor struct {
// NotificationsEnqueuer handles enqueueing notifications for delivery by SMTP, webhook, etc.
notificationsEnqueuer notifications.Enqueuer
reg prometheus.Registerer
experiments codersdk.Experiments
metrics executorMetrics
}
@ -59,7 +61,7 @@ type Stats struct {
}
// New returns a new wsactions executor.
func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, reg prometheus.Registerer, tss *atomic.Pointer[schedule.TemplateScheduleStore], auditor *atomic.Pointer[audit.Auditor], acs *atomic.Pointer[dbauthz.AccessControlStore], log slog.Logger, tick <-chan time.Time, enqueuer notifications.Enqueuer) *Executor {
func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, reg prometheus.Registerer, tss *atomic.Pointer[schedule.TemplateScheduleStore], auditor *atomic.Pointer[audit.Auditor], acs *atomic.Pointer[dbauthz.AccessControlStore], log slog.Logger, tick <-chan time.Time, enqueuer notifications.Enqueuer, exp codersdk.Experiments) *Executor {
factory := promauto.With(reg)
le := &Executor{
//nolint:gocritic // Autostart has a limited set of permissions.
@ -73,6 +75,7 @@ func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, reg p
accessControlStore: acs,
notificationsEnqueuer: enqueuer,
reg: reg,
experiments: exp,
metrics: executorMetrics{
autobuildExecutionDuration: factory.NewHistogram(prometheus.HistogramOpts{
Namespace: "coderd",
@ -258,6 +261,7 @@ func (e *Executor) runOnce(t time.Time) Stats {
builder := wsbuilder.New(ws, nextTransition).
SetLastWorkspaceBuildInTx(&latestBuild).
SetLastWorkspaceBuildJobInTx(&latestJob).
Experiments(e.experiments).
Reason(reason)
log.Debug(e.ctx, "auto building workspace", slog.F("transition", nextTransition))
if nextTransition == database.WorkspaceTransitionStart &&

View File

@ -354,6 +354,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
auditor.Store(&options.Auditor)
ctx, cancelFunc := context.WithCancel(context.Background())
experiments := coderd.ReadExperiments(*options.Logger, options.DeploymentValues.Experiments)
lifecycleExecutor := autobuild.NewExecutor(
ctx,
options.Database,
@ -365,6 +366,7 @@ func NewOptions(t testing.TB, options *Options) (func(http.Handler), context.Can
*options.Logger,
options.AutobuildTicker,
options.NotificationsEnqueuer,
experiments,
).WithStatsChannel(options.AutobuildStats)
lifecycleExecutor.Run()

View File

@ -12,13 +12,13 @@ import (
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/apiversion"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/files"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/coderd/wsbuilder"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/wsjson"
sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
@ -69,13 +69,10 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http
return
}
major, minor, err := apiversion.Parse(tf.ProvisionerdVersion)
// If the api version is not valid or less than 1.5, we need to use the static parameters
useStaticParams := err != nil || major < 1 || (major == 1 && minor < 6)
if useStaticParams {
api.handleStaticParameters(rw, r, templateVersion.ID)
} else {
if wsbuilder.ProvisionerVersionSupportsDynamicParameters(tf.ProvisionerdVersion) {
api.handleDynamicParameters(rw, r, tf, templateVersion)
} else {
api.handleStaticParameters(rw, r, templateVersion.ID)
}
}

View File

@ -338,6 +338,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
RichParameterValues(createBuild.RichParameterValues).
LogLevel(string(createBuild.LogLevel)).
DeploymentValues(api.Options.DeploymentValues).
Experiments(api.Experiments).
TemplateVersionPresetID(createBuild.TemplateVersionPresetID)
var (
@ -383,6 +384,22 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
builder = builder.State(createBuild.ProvisionerState)
}
// Only defer to dynamic parameters if the experiment is enabled.
if api.Experiments.Enabled(codersdk.ExperimentDynamicParameters) {
if createBuild.EnableDynamicParameters != nil {
// Explicit opt-in
builder = builder.DynamicParameters(*createBuild.EnableDynamicParameters)
}
} else {
if createBuild.EnableDynamicParameters != nil {
api.Logger.Warn(ctx, "ignoring dynamic parameter field sent by request, the experiment is not enabled",
slog.F("field", *createBuild.EnableDynamicParameters),
slog.F("user", apiKey.UserID.String()),
slog.F("transition", string(createBuild.Transition)),
)
}
}
workspaceBuild, provisionerJob, provisionerDaemons, err = builder.Build(
ctx,
tx,

View File

@ -704,6 +704,8 @@ func createWorkspace(
Reason(database.BuildReasonInitiator).
Initiator(initiatorID).
ActiveVersion().
Experiments(api.Experiments).
DeploymentValues(api.DeploymentValues).
RichParameterValues(req.RichParameterValues)
if req.TemplateVersionID != uuid.Nil {
builder = builder.VersionID(req.TemplateVersionID)
@ -716,7 +718,7 @@ func createWorkspace(
}
if req.EnableDynamicParameters && api.Experiments.Enabled(codersdk.ExperimentDynamicParameters) {
builder = builder.UsingDynamicParameters()
builder = builder.DynamicParameters(req.EnableDynamicParameters)
}
workspaceBuild, provisionerJob, provisionerDaemons, err = builder.Build(

View File

@ -13,7 +13,9 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/coder/coder/v2/apiversion"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/provisioner/terraform/tfparse"
"github.com/coder/coder/v2/provisionersdk"
sdkproto "github.com/coder/coder/v2/provisionersdk/proto"
@ -51,9 +53,11 @@ type Builder struct {
state stateTarget
logLevel string
deploymentValues *codersdk.DeploymentValues
experiments codersdk.Experiments
richParameterValues []codersdk.WorkspaceBuildParameter
dynamicParametersEnabled bool
richParameterValues []codersdk.WorkspaceBuildParameter
// dynamicParametersEnabled is non-nil if set externally
dynamicParametersEnabled *bool
initiator uuid.UUID
reason database.BuildReason
templateVersionPresetID uuid.UUID
@ -66,6 +70,7 @@ type Builder struct {
template *database.Template
templateVersion *database.TemplateVersion
templateVersionJob *database.ProvisionerJob
terraformValues *database.TemplateVersionTerraformValue
templateVersionParameters *[]database.TemplateVersionParameter
templateVersionVariables *[]database.TemplateVersionVariable
templateVersionWorkspaceTags *[]database.TemplateVersionWorkspaceTag
@ -155,6 +160,14 @@ func (b Builder) DeploymentValues(dv *codersdk.DeploymentValues) Builder {
return b
}
func (b Builder) Experiments(exp codersdk.Experiments) Builder {
// nolint: revive
cpy := make(codersdk.Experiments, len(exp))
copy(cpy, exp)
b.experiments = cpy
return b
}
func (b Builder) Initiator(u uuid.UUID) Builder {
// nolint: revive
b.initiator = u
@ -187,8 +200,9 @@ func (b Builder) MarkPrebuiltWorkspaceClaim() Builder {
return b
}
func (b Builder) UsingDynamicParameters() Builder {
b.dynamicParametersEnabled = true
func (b Builder) DynamicParameters(using bool) Builder {
// nolint: revive
b.dynamicParametersEnabled = ptr.Ref(using)
return b
}
@ -516,6 +530,22 @@ func (b *Builder) getTemplateVersionID() (uuid.UUID, error) {
return bld.TemplateVersionID, nil
}
func (b *Builder) getTemplateTerraformValues() (*database.TemplateVersionTerraformValue, error) {
if b.terraformValues != nil {
return b.terraformValues, nil
}
v, err := b.getTemplateVersion()
if err != nil {
return nil, xerrors.Errorf("get template version so we can get terraform values: %w", err)
}
vals, err := b.store.GetTemplateVersionTerraformValues(b.ctx, v.ID)
if err != nil {
return nil, xerrors.Errorf("get template version terraform values %s: %w", v.JobID, err)
}
b.terraformValues = &vals
return b.terraformValues, err
}
func (b *Builder) getLastBuild() (*database.WorkspaceBuild, error) {
if b.lastBuild != nil {
return b.lastBuild, nil
@ -593,9 +623,10 @@ func (b *Builder) getParameters() (names, values []string, err error) {
return nil, nil, BuildError{http.StatusBadRequest, "Unable to build workspace with unsupported parameters", err}
}
if b.dynamicParametersEnabled {
// Dynamic parameters skip all parameter validation.
// Pass the user's input as is.
// Dynamic parameters skip all parameter validation.
// Deleting a workspace also should skip parameter validation.
// Pass the user's input as is.
if b.usingDynamicParameters() {
// TODO: The previous behavior was only to pass param values
// for parameters that exist. Since dynamic params can have
// conditional parameter existence, the static frame of reference
@ -989,3 +1020,36 @@ func (b *Builder) checkRunningBuild() error {
}
return nil
}
func (b *Builder) usingDynamicParameters() bool {
if !b.experiments.Enabled(codersdk.ExperimentDynamicParameters) {
// Experiment required
return false
}
vals, err := b.getTemplateTerraformValues()
if err != nil {
return false
}
if !ProvisionerVersionSupportsDynamicParameters(vals.ProvisionerdVersion) {
return false
}
if b.dynamicParametersEnabled != nil {
return *b.dynamicParametersEnabled
}
tpl, err := b.getTemplate()
if err != nil {
return false // Let another part of the code get this error
}
return !tpl.UseClassicParameterFlow
}
func ProvisionerVersionSupportsDynamicParameters(version string) bool {
major, minor, err := apiversion.Parse(version)
// If the api version is not valid or less than 1.6, we need to use the static parameters
useStaticParams := err != nil || major < 1 || (major == 1 && minor < 6)
return !useStaticParams
}

View File

@ -839,6 +839,32 @@ func TestWorkspaceBuildWithPreset(t *testing.T) {
req.NoError(err)
}
func TestProvisionerVersionSupportsDynamicParameters(t *testing.T) {
t.Parallel()
for v, dyn := range map[string]bool{
"": false,
"na": false,
"0.0": false,
"0.10": false,
"1.4": false,
"1.5": false,
"1.6": true,
"1.7": true,
"1.8": true,
"2.0": true,
"2.17": true,
"4.0": true,
} {
t.Run(v, func(t *testing.T) {
t.Parallel()
does := wsbuilder.ProvisionerVersionSupportsDynamicParameters(v)
require.Equal(t, dyn, does)
})
}
}
type txExpect func(mTx *dbmock.MockStore)
func expectDB(t *testing.T, opts ...txExpect) *dbmock.MockStore {