Files
coder/coderd/dynamicparameters/secrets.go
T
Zach e0be9bf213 feat: surface missing coder_secret requirements on resolve-autostart (#25081)
Adds `dynamicparameters.EvaluateSecretMismatch` as a shared helper on
top of the existing renderer, then wires it into the resolve-autostart
handler so the UI can surface unsatisfied `coder_secret` requirements in
a template alongside parameter mismatch for autostart.

The lifecycle executor changes will land in a follow-up that depend
on this helper. The UI changes that consume the new `secret_mismatch`
field is also a follow-up.

Generated with assistance from Coder Agents.
2026-05-13 14:20:02 -06:00

101 lines
3.6 KiB
Go

package dynamicparameters
import (
"context"
"slices"
"github.com/google/uuid"
"github.com/hashicorp/hcl/v2"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/files"
"github.com/coder/coder/v2/coderd/util/slice"
"github.com/coder/coder/v2/codersdk"
previewtypes "github.com/coder/preview/types"
)
// EvaluateSecretMismatch reports whether the given template version
// declares coder_secret requirements that the workspace owner's secrets
// do not satisfy. Returns false (no mismatch) when the renderer cannot
// authoritatively evaluate the requirements; the reason is logged at the
// appropriate level so operators can distinguish a forbidden caller
// (expected for template admins) from a genuine renderer or DB failure.
// Returns ErrTemplateVersionNotReady when the version's provisioner job
// has not yet completed; callers should treat that as "unknown" and
// leave SecretMismatch false.
func EvaluateSecretMismatch(
ctx context.Context,
logger slog.Logger,
db database.Store,
cache files.FileAcquirer,
version database.TemplateVersion,
ownerID uuid.UUID,
buildParams []database.WorkspaceBuildParameter,
) (bool, error) {
paramValues := slice.ToMapFunc(buildParams, func(p database.WorkspaceBuildParameter) (string, string) {
return p.Name, p.Value
})
renderer, err := Prepare(ctx, db, cache, version.ID,
WithTemplateVersion(version),
WithLogger(logger))
if err != nil {
return false, err
}
defer renderer.Close()
result, diags := renderer.Render(ctx, ownerID, paramValues, IncludeSecretRequirements())
// Three distinct "unknown" cases. Returning false from any of them
// matches the resolve-autostart handler's semantics, but they have
// very different operator implications, so we log accordingly. The
// renderer already logs its own diagnostics through the same logger,
// so we omit them here to avoid duplication.
if result.Output == nil {
logger.Warn(ctx,
"secret requirement evaluation produced no preview output; treating as unknown",
slog.F("template_version_id", version.ID),
)
return false, nil
}
switch secretValidationBlockerCode(diags) {
case DiagCodeOwnerSecretsFetchFailed:
logger.Warn(ctx,
"failed to fetch owner secrets during requirement evaluation; treating as unknown",
slog.F("template_version_id", version.ID),
)
return false, nil
case DiagCodeSecretValidationForbidden:
// Expected when a caller without user_secret:read on the owner
// hits the renderer, e.g. a template admin viewing another user's
// workspace. Debug-level keeps production volume sane while
// preserving visibility under trace logging.
logger.Debug(ctx,
"secret requirement evaluation forbidden for caller; treating as unknown",
slog.F("template_version_id", version.ID),
)
return false, nil
}
return slices.ContainsFunc(result.SecretRequirements,
func(s codersdk.SecretRequirementStatus) bool { return !s.Satisfied }), nil
}
// secretValidationBlockerCode returns the first diagnostic code among the
// codes that indicate secret-requirement evaluation could not be
// performed. Returns the empty string if no such diagnostic is present.
//
// ExtractDiagnosticExtra walks the wrapped-extra chain so we still
// detect our marker when another extra has been chained on top by
// preview's SetDiagnosticExtra.
func secretValidationBlockerCode(diags hcl.Diagnostics) string {
for _, d := range diags {
extra := previewtypes.ExtractDiagnosticExtra(d)
switch extra.Code {
case DiagCodeOwnerSecretsFetchFailed, DiagCodeSecretValidationForbidden:
return extra.Code
}
}
return ""
}