mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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() {}
|
||||
Reference in New Issue
Block a user