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.
This commit is contained in:
Zach
2026-05-13 14:20:02 -06:00
committed by GitHub
parent 0f505aa4da
commit e0be9bf213
10 changed files with 461 additions and 5 deletions
+192
View File
@@ -649,6 +649,167 @@ func TestDynamicParametersWithTerraformValues(t *testing.T) {
})
}
// TestResolveAutostartPreservesParameterMismatchOnSecretEvalError exercises
// the handler's default switch arm: when EvaluateSecretMismatch returns a
// non-ErrTemplateVersionNotReady error, the handler must log and treat
// SecretMismatch as "unknown" without dropping the already-computed
// ParameterMismatch signal.
func TestResolveAutostartPreservesParameterMismatchOnSecretEvalError(t *testing.T) {
t.Parallel()
db, ps := dbtestutil.NewDB(t)
// Wrap the DB so we can fail the renderer's GetTemplateVersionTerraformValues
// call. Toggle is flipped on after setup so initial template version
// processing succeeds.
reject := &dbRejectTemplateVersionTerraformValues{Store: db}
noRequirementsTF := []byte(`terraform {
required_providers {
coder = {
source = "coder/coder"
}
}
}
`)
setup := setupDynamicParamsTest(t, setupDynamicParamsTestParams{
db: reject,
ps: ps,
provisionerDaemonVersion: provProto.CurrentVersion.String(),
mainTF: noRequirementsTF,
})
_ = setup.stream.Close(websocket.StatusGoingAway)
wrk := coderdtest.CreateWorkspace(t, setup.client, setup.template.ID,
func(req *codersdk.CreateWorkspaceRequest) {
req.AutomaticUpdates = codersdk.AutomaticUpdatesAlways
})
coderdtest.AwaitWorkspaceBuildJobCompleted(t, setup.client, wrk.LatestBuild.ID)
// Push a v2 that adds a required-no-default parameter so resolve-autostart
// computes ParameterMismatch=true. The new version becomes active via
// DynamicParameterTemplate's UpdateActiveTemplateVersion call.
paramRequiredTF := []byte(`terraform {
required_providers {
coder = {
source = "coder/coder"
}
}
}
data "coder_parameter" "required_param" {
name = "required_param"
type = "string"
}
`)
// StaticParams populates the legacy template_version_parameters table via
// the GraphComplete response. resolve-autostart reads from this table to
// determine ParameterMismatch.
_, _ = coderdtest.DynamicParameterTemplate(t, setup.dynamicParamsClient,
wrk.OrganizationID,
coderdtest.DynamicParameterTemplateParams{
MainTF: string(paramRequiredTF),
TemplateID: setup.template.ID,
StaticParams: []*proto.RichParameter{{
Name: "required_param",
Type: "string",
Required: true,
}},
})
// Arm the rejection only for the resolve-autostart call. Setup has
// already completed, so all earlier calls passed through.
reject.SetReject(true)
ctx := testutil.Context(t, testutil.WaitLong)
resp, err := setup.client.ResolveAutostart(ctx, wrk.ID.String())
require.NoError(t, err, "resolve-autostart must not 500 when secret evaluation fails")
require.True(t, resp.ParameterMismatch, "ParameterMismatch should be preserved across secret evaluation failure")
require.False(t, resp.SecretMismatch, "SecretMismatch should be unknown (false) when evaluation fails")
}
// TestResolveAutostartSecretRequirements is the PLAT-81 backend coverage:
// resolve-autostart must surface coder_secret requirements declared by the
// active template version that the workspace owner's secrets do not
// satisfy. The dashboard banner uses this to tell the user autostart
// cannot run the auto-update build until they create the missing secrets.
func TestResolveAutostartSecretRequirements(t *testing.T) {
t.Parallel()
noRequirementsTF := []byte(`terraform {
required_providers {
coder = {
source = "coder/coder"
}
}
}
`)
secretRequiredTF, err := os.ReadFile("testdata/parameters/secret_required/main.tf")
require.NoError(t, err)
// v1 has no secret requirements; we need a workspace to exist so
// resolve-autostart enters its version-mismatch branch.
setup := setupDynamicParamsTest(t, setupDynamicParamsTestParams{
provisionerDaemonVersion: provProto.CurrentVersion.String(),
mainTF: noRequirementsTF,
})
_ = setup.stream.Close(websocket.StatusGoingAway)
wrk := coderdtest.CreateWorkspace(t, setup.client, setup.template.ID,
func(req *codersdk.CreateWorkspaceRequest) {
req.AutomaticUpdates = codersdk.AutomaticUpdatesAlways
})
coderdtest.AwaitWorkspaceBuildJobCompleted(t, setup.client, wrk.LatestBuild.ID)
// Push v2 with a coder_secret requirement and make it active.
_, _ = coderdtest.DynamicParameterTemplate(t, setup.dynamicParamsClient,
wrk.OrganizationID,
coderdtest.DynamicParameterTemplateParams{
MainTF: string(secretRequiredTF),
TemplateID: setup.template.ID,
})
t.Run("OwnerSeesMismatchThenSatisfies", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
// Owner has no GITHUB_TOKEN secret; resolve-autostart must surface
// the unsatisfied requirement.
resp, err := setup.client.ResolveAutostart(ctx, wrk.ID.String())
require.NoError(t, err)
require.False(t, resp.ParameterMismatch)
require.True(t, resp.SecretMismatch)
// Creating the matching secret must clear the entry without further
// template changes.
_, err = setup.client.CreateUserSecret(ctx, codersdk.Me, codersdk.CreateUserSecretRequest{
Name: "github-token",
Value: "ghp_test",
EnvName: "GITHUB_TOKEN",
})
require.NoError(t, err)
resp, err = setup.client.ResolveAutostart(ctx, wrk.ID.String())
require.NoError(t, err)
require.False(t, resp.ParameterMismatch)
require.False(t, resp.SecretMismatch)
})
t.Run("ForbiddenCallerSeesNoMismatch", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
// The template-admin client can read the workspace but lacks
// user_secret:read on the workspace owner. The renderer's secret
// fetch produces a forbidden diagnostic, and the handler must
// treat that as "unknown" and report SecretMismatch=false rather
// than leaking a 500 or a stale true value.
resp, err := setup.dynamicParamsClient.ResolveAutostart(ctx, wrk.ID.String())
require.NoError(t, err)
require.False(t, resp.SecretMismatch)
})
}
type setupDynamicParamsTestParams struct {
db database.Store
ps pubsub.Pubsub
@@ -749,3 +910,34 @@ func (d *dbRejectGitSSHKey) GetGitSSHKey(ctx context.Context, userID uuid.UUID)
return d.Store.GetGitSSHKey(ctx, userID)
}
// dbRejectTemplateVersionTerraformValues wraps a Store so the dynamic
// parameter renderer's GetTemplateVersionTerraformValues call can be made
// to fail on demand. This forces the resolve-autostart handler into the
// default switch arm where EvaluateSecretMismatch returns a non-
// ErrTemplateVersionNotReady error.
type dbRejectTemplateVersionTerraformValues struct {
database.Store
rejectMu sync.RWMutex
reject bool
}
// SetReject toggles whether GetTemplateVersionTerraformValues should
// return an error or passthrough to the underlying store.
func (d *dbRejectTemplateVersionTerraformValues) SetReject(reject bool) {
d.rejectMu.Lock()
defer d.rejectMu.Unlock()
d.reject = reject
}
func (d *dbRejectTemplateVersionTerraformValues) GetTemplateVersionTerraformValues(ctx context.Context, templateVersionID uuid.UUID) (database.TemplateVersionTerraformValue, error) {
d.rejectMu.RLock()
reject := d.reject
d.rejectMu.RUnlock()
if reject {
return database.TemplateVersionTerraformValue{}, xerrors.New("forcing a fake error")
}
return d.Store.GetTemplateVersionTerraformValues(ctx, templateVersionID)
}