feat: add secret requirement contract to dynamic parameters (#24785)

Adds structured `secret_requirements` to dynamic parameter responses and
enforces missing required secrets during workspace start.

Stop, delete, and tag rendering paths skip secret requirement
enforcement so unmet secrets do not prevent cleanup. The SDK, generated
API docs/types, and backend render/resolver/wsbuilder tests are updated
for the new contract.
This commit is contained in:
dylanhuff-at-coder
2026-04-29 16:38:26 -07:00
committed by GitHub
parent fe6c8771df
commit fb84e72319
28 changed files with 1382 additions and 100 deletions
+19 -4
View File
@@ -17,6 +17,7 @@ import (
"github.com/sqlc-dev/pqtype"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
@@ -59,6 +60,7 @@ type Builder struct {
deploymentValues *codersdk.DeploymentValues
experiments codersdk.Experiments
usageChecker UsageChecker
logger slog.Logger
richParameterValues []codersdk.WorkspaceBuildParameter
initiator uuid.UUID
@@ -195,6 +197,12 @@ func (b Builder) Experiments(exp codersdk.Experiments) Builder {
return b
}
func (b Builder) Logger(log slog.Logger) Builder {
// nolint: revive
b.logger = log
return b
}
func (b Builder) Initiator(u uuid.UUID) Builder {
// nolint: revive
b.initiator = u
@@ -759,6 +767,7 @@ func (b *Builder) getDynamicParameterRenderer() (dynamicparameters.Renderer, err
dynamicparameters.WithProvisionerJob(*job),
dynamicparameters.WithTerraformValues(*tfVals),
dynamicparameters.WithTemplateVariableValues(variableValues),
dynamicparameters.WithLogger(b.logger.Named("dynamicparameters")),
)
if err != nil {
return nil, xerrors.Errorf("get template version renderer: %w", err)
@@ -884,10 +893,16 @@ func (b *Builder) getDynamicParameters() (names, values []string, err error) {
return nil, nil, BuildError{http.StatusInternalServerError, "failed to check if first build", err}
}
// Don't let missing secrets block stop or delete.
var resolveOpts []dynamicparameters.ResolveOption
if b.trans != database.WorkspaceTransitionStart {
resolveOpts = append(resolveOpts, dynamicparameters.SkipSecretRequirements())
}
buildValues, err := dynamicparameters.ResolveParameters(b.ctx, b.workspace.OwnerID, render, firstBuild,
lastBuildParameters,
b.richParameterValues,
presetParameterValues)
presetParameterValues,
resolveOpts...)
if err != nil {
return nil, nil, BuildError{http.StatusBadRequest, "resolve parameters", err}
}
@@ -1123,13 +1138,13 @@ func (b *Builder) getDynamicProvisionerTags() (map[string]string, error) {
vals[name] = values[i]
}
output, diags := render.Render(b.ctx, b.workspace.OwnerID, vals)
tagErr := dynamicparameters.CheckTags(output, diags)
result, diags := render.Render(b.ctx, b.workspace.OwnerID, vals)
tagErr := dynamicparameters.CheckTags(result.Output, diags)
if tagErr != nil {
return nil, BuildError{http.StatusBadRequest, "workspace tags validation failed", tagErr}
}
for k, v := range output.WorkspaceTags.Tags() {
for k, v := range result.Output.WorkspaceTags.Tags() {
tags[k] = v
}
@@ -0,0 +1,70 @@
package wsbuilder
import (
"context"
"testing"
"github.com/google/uuid"
"github.com/hashicorp/hcl/v2"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/dynamicparameters"
"github.com/coder/coder/v2/provisionersdk"
"github.com/coder/preview"
previewtypes "github.com/coder/preview/types"
)
func TestBuilderDynamicProvisionerTagsDoesNotRequestSecretRequirements(t *testing.T) {
t.Parallel()
ownerID := uuid.New()
names := []string{"region"}
values := []string{"us-east"}
render := &tagsPathRenderer{
result: &dynamicparameters.RenderResult{
Output: &preview.Output{
WorkspaceTags: previewtypes.TagBlocks{{
Tags: previewtypes.Tags{{
Key: previewtypes.StringLiteral("region"),
Value: previewtypes.StringLiteral("us-east"),
}},
}},
},
},
}
builder := New(database.Workspace{
ID: uuid.New(),
OwnerID: ownerID,
}, database.WorkspaceTransitionStart, NoopUsageChecker{})
builder.ctx = t.Context()
builder.parameterRender = render
builder.parameterNames = &names
builder.parameterValues = &values
builder.templateVersionJob = &database.ProvisionerJob{
Tags: database.StringMap{
provisionersdk.TagScope: provisionersdk.ScopeUser,
},
}
tags, err := builder.getDynamicProvisionerTags()
require.NoError(t, err)
require.Equal(t, "us-east", tags["region"])
require.Equal(t, ownerID.String(), tags[provisionersdk.TagOwner])
require.Empty(t, render.opts, "tags path should not request secret requirements")
}
type tagsPathRenderer struct {
result *dynamicparameters.RenderResult
diags hcl.Diagnostics
opts []dynamicparameters.RenderOption
}
func (r *tagsPathRenderer) Render(_ context.Context, _ uuid.UUID, _ map[string]string, opts ...dynamicparameters.RenderOption) (*dynamicparameters.RenderResult, hcl.Diagnostics) {
r.opts = opts
return r.result, r.diags
}
func (*tagsPathRenderer) Close() {}