chore: remove coder_secret Terraform integration (#25512)

Removes the coder_secret Terraform integration: the data.coder_secret
consumption path through provisionerdserver → provisioner.proto →
provisioner/terraform, the dynamic-parameter secret-requirement
validation, and the workspace-update / resolve-autostart surfaces that
depended on it. This is being done due to a product/feature direction
change (see PLAT-243). User-secret CRUD (DB, REST, CLI, UI, telemetry, audit)
and the agent-manifest secret-injection path are untouched.

The provisionerd API is bumped from v1.17 to v1.18 rather than rolled
back: v1.17 shipped in v2.33.x, so user_secrets field numbers are
reserved and the changelog documents both versions.

Generated with assistance from Coder Agents.
This commit is contained in:
Zach
2026-05-21 09:19:29 -06:00
committed by GitHub
parent 26a0805dcd
commit ddc0e99c69
45 changed files with 835 additions and 3859 deletions
+16 -196
View File
@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"io/fs"
"log/slog"
"sync"
"time"
@@ -12,27 +13,14 @@ import (
"github.com/zclconf/go-cty/cty"
"golang.org/x/xerrors"
"cdr.dev/slog/v3"
"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/codersdk"
"github.com/coder/preview"
previewtypes "github.com/coder/preview/types"
)
// RenderResult is the structured output of Renderer.Render. The outer
// pointer is always non-nil; inner fields may be nil.
// SecretRequirements is nil when no coder_secret blocks are declared,
// when fetch was forbidden, or when fetch failed. Output may be nil
// when underlying rendering fails (matches preview.Preview's existing
// convention).
type RenderResult struct {
Output *preview.Output
SecretRequirements []codersdk.SecretRequirementStatus
}
// Renderer is able to execute and evaluate terraform with the given inputs.
// It may use the database to fetch additional state, such as a user's groups,
// roles, etc. Therefore, it requires an authenticated `ctx`.
@@ -40,40 +28,17 @@ type RenderResult struct {
// 'Close()' **must** be called once the renderer is no longer needed.
// Forgetting to do so will result in a memory leak.
type Renderer interface {
Render(ctx context.Context, ownerID uuid.UUID, values map[string]string, opts ...RenderOption) (*RenderResult, hcl.Diagnostics)
Render(ctx context.Context, ownerID uuid.UUID, values map[string]string) (*preview.Output, hcl.Diagnostics)
Close()
}
var ErrTemplateVersionNotReady = xerrors.New("template version job not finished")
// RenderOption configures optional behavior for Renderer.Render.
type RenderOption func(*renderOptions)
type renderOptions struct {
includeSecretRequirements bool
}
// IncludeSecretRequirements returns structured secret-requirement statuses and
// diagnostics for the rendered template.
func IncludeSecretRequirements() RenderOption {
return func(o *renderOptions) {
o.includeSecretRequirements = true
}
}
// Diagnostic extra codes for secret-requirement validation.
const (
DiagCodeMissingSecret = "missing_secret"
DiagCodeOwnerSecretsFetchFailed = "owner_secrets_fetch_failed"
DiagCodeSecretValidationForbidden = "secret_validation_forbidden"
)
// loader is used to load the necessary coder objects for rendering a template
// version's parameters. The output is a Renderer, which is the object that uses
// the cached objects to render the template version's parameters.
type loader struct {
templateVersionID uuid.UUID
logger slog.Logger
// cache of objects
templateVersion *database.TemplateVersion
@@ -125,13 +90,6 @@ func WithTerraformValues(values database.TemplateVersionTerraformValue) func(r *
}
}
// WithLogger sets the logger used by the renderer.
func WithLogger(logger slog.Logger) func(r *loader) {
return func(r *loader) {
r.logger = logger
}
}
func (r *loader) loadData(ctx context.Context, db database.Store) error {
if r.templateVersion == nil {
tv, err := db.GetTemplateVersionByID(ctx, r.templateVersionID)
@@ -245,14 +203,12 @@ func (r *loader) dynamicRenderer(ctx context.Context, db database.Store, cache *
closeFiles = false // Caller will have to call close
return &dynamicRenderer{
data: r,
templateFS: templateFS,
db: db,
logger: r.logger,
ownerErrors: make(map[uuid.UUID]error),
ownerSecretErrors: make(map[uuid.UUID]error),
close: cache.Close,
tfvarValues: tfVarValues,
data: r,
templateFS: templateFS,
db: db,
ownerErrors: make(map[uuid.UUID]error),
close: cache.Close,
tfvarValues: tfVarValues,
}, nil
}
@@ -260,26 +216,16 @@ type dynamicRenderer struct {
db database.Store
data *loader
templateFS fs.FS
logger slog.Logger
ownerErrors map[uuid.UUID]error
currentOwner *previewtypes.WorkspaceOwner
// ownerSecretErrors caches NotAuthorized denials per owner.
ownerSecretErrors map[uuid.UUID]error
tfvarValues map[string]cty.Value
tfvarValues map[string]cty.Value
once sync.Once
close func()
}
func (r *dynamicRenderer) Render(ctx context.Context, ownerID uuid.UUID, values map[string]string, opts ...RenderOption) (*RenderResult, hcl.Diagnostics) {
options := renderOptions{}
for _, opt := range opts {
opt(&options)
}
func (r *dynamicRenderer) Render(ctx context.Context, ownerID uuid.UUID, values map[string]string) (*preview.Output, hcl.Diagnostics) {
// Always start with the cached error, if we have one.
ownerErr := r.ownerErrors[ownerID]
if ownerErr == nil {
@@ -288,7 +234,7 @@ func (r *dynamicRenderer) Render(ctx context.Context, ownerID uuid.UUID, values
if ownerErr != nil || r.currentOwner == nil {
r.ownerErrors[ownerID] = ownerErr
return &RenderResult{}, hcl.Diagnostics{
return nil, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Failed to fetch workspace owner",
@@ -305,122 +251,13 @@ func (r *dynamicRenderer) Render(ctx context.Context, ownerID uuid.UUID, values
ParameterValues: values,
Owner: *r.currentOwner,
TFVars: r.tfvarValues,
// Leave Logger nil so preview discards parser logs. Returning
// those logs to callers would be useful, but they may be large.
// Do not emit parser logs to coderd output logs.
// TODO: Returning this logs in the output would benefit the caller.
// Unsure how large the logs can be, so for now we just discard them.
Logger: slog.New(slog.DiscardHandler),
}
output, diags := preview.Preview(ctx, input, r.templateFS)
if output == nil {
return &RenderResult{}, diags
}
var secretRequirements []codersdk.SecretRequirementStatus
if options.includeSecretRequirements && len(output.SecretRequirements) > 0 {
var secretDiags hcl.Diagnostics
secretRequirements, secretDiags = r.checkSecretRequirements(ctx, ownerID, output.SecretRequirements)
diags = diags.Extend(secretDiags)
}
return &RenderResult{
Output: output,
SecretRequirements: secretRequirements,
}, diags
}
// checkSecretRequirements returns structured requirement statuses. Callers
// without user_secret:read on the owner get a single
// secret_validation_forbidden warning instead, to avoid leaking the target's
// secret names via structured status presence.
func (r *dynamicRenderer) checkSecretRequirements(ctx context.Context, ownerID uuid.UUID, reqs []previewtypes.SecretRequirement) ([]codersdk.SecretRequirementStatus, hcl.Diagnostics) {
secrets, err := r.getOwnerSecrets(ctx, ownerID)
if err != nil {
if dbauthz.IsNotAuthorizedError(err) {
// Warning keeps the Create Workspace button enabled.
return nil, hcl.Diagnostics{{
Severity: hcl.DiagWarning,
Summary: "Cannot validate secret requirements",
Detail: "You are not permitted to read secret metadata for this user. The workspace may fail to build if required secrets are not set.",
Extra: previewtypes.DiagnosticExtra{
Code: DiagCodeSecretValidationForbidden,
},
}}
}
r.logger.Warn(ctx, "failed to fetch owner secrets for secret-requirement validation",
slog.F("owner_id", ownerID),
slog.Error(err),
)
return nil, hcl.Diagnostics{{
Severity: hcl.DiagError,
Summary: "Failed to fetch owner secrets",
Detail: "Could not validate template secret requirements. Please try again.",
Extra: previewtypes.DiagnosticExtra{
Code: DiagCodeOwnerSecretsFetchFailed,
},
}}
}
envSet := make(map[string]struct{}, len(secrets))
fileSet := make(map[string]struct{}, len(secrets))
for _, s := range secrets {
if s.EnvName != "" {
envSet[s.EnvName] = struct{}{}
}
if s.FilePath != "" {
fileSet[s.FilePath] = struct{}{}
}
}
statuses := make([]codersdk.SecretRequirementStatus, 0, len(reqs))
type secretRequirementDedupKey struct {
env string
file string
}
seen := make(map[secretRequirementDedupKey]int, len(reqs))
for _, req := range reqs {
kind := secretRequirementKind(req.Env, req.File)
if kind == "" {
// Defensive: SecretFromBlock should reject invalid inputs upstream.
continue
}
var env string
var file string
satisfied := false
switch kind {
case secretRequirementKindEnv:
env = req.Env
_, satisfied = envSet[req.Env]
case secretRequirementKindFile:
file = req.File
_, satisfied = fileSet[req.File]
}
// Dedup by Env/File. On collision, keep the
// lexicographically smallest non-empty HelpMessage. This is
// deterministic across runs; preview's SortSecretRequirements
// sorts on (Env, File) and does not guarantee a stable order
// when multiple coder_secret blocks declare the same value, so
// we cannot rely on "first source wins."
key := secretRequirementDedupKey{
env: env,
file: file,
}
if i, ok := seen[key]; ok {
statuses[i].Satisfied = statuses[i].Satisfied || satisfied
if req.HelpMessage != "" && (statuses[i].HelpMessage == "" || req.HelpMessage < statuses[i].HelpMessage) {
statuses[i].HelpMessage = req.HelpMessage
}
continue
}
seen[key] = len(statuses)
statuses = append(statuses, codersdk.SecretRequirementStatus{
Env: env,
File: file,
HelpMessage: req.HelpMessage,
Satisfied: satisfied,
})
}
return statuses, nil
return preview.Preview(ctx, input, r.templateFS)
}
func (r *dynamicRenderer) getWorkspaceOwnerData(ctx context.Context, ownerID uuid.UUID) error {
@@ -437,23 +274,6 @@ func (r *dynamicRenderer) getWorkspaceOwnerData(ctx context.Context, ownerID uui
return nil
}
// getOwnerSecrets fetches the owner's secrets under the caller's auth
// context. Only NotAuthorized denials are cached; successes re-fetch so
// newly-created secrets are picked up on the next render.
func (r *dynamicRenderer) getOwnerSecrets(ctx context.Context, ownerID uuid.UUID) ([]database.ListUserSecretsRow, error) {
if err, cached := r.ownerSecretErrors[ownerID]; cached {
return nil, err
}
rows, err := r.db.ListUserSecrets(ctx, ownerID)
if err != nil {
if dbauthz.IsNotAuthorizedError(err) {
r.ownerSecretErrors[ownerID] = err
}
return nil, err
}
return rows, nil
}
func (r *dynamicRenderer) Close() {
r.once.Do(r.close)
}