mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: refresh dynamic parameters on secret changes (#24786)
Publishes user secret create, update, and delete events and subscribes dynamic parameter websockets to authorized owner secret changes. Secret changes trigger fresh renders with monotonic response IDs, with backend tests covering subscription authorization and websocket refresh behavior.
This commit is contained in:
+189
-8
@@ -414,6 +414,183 @@ func TestDynamicParametersWithTerraformValues(t *testing.T) {
|
||||
}}, preview.SecretRequirements)
|
||||
})
|
||||
|
||||
t.Run("SecretRequirementPushesOnSecretChange", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/secret_required/main.tf")
|
||||
require.NoError(t, err)
|
||||
|
||||
setup := setupDynamicParamsTest(t, setupDynamicParamsTestParams{
|
||||
provisionerDaemonVersion: provProto.CurrentVersion.String(),
|
||||
mainTF: dynamicParametersTerraformSource,
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
previews := setup.stream.Chan()
|
||||
|
||||
preview := testutil.RequireReceive(ctx, t, previews)
|
||||
require.Equal(t, -1, preview.ID)
|
||||
require.Len(t, preview.SecretRequirements, 1)
|
||||
require.False(t, preview.SecretRequirements[0].Satisfied)
|
||||
|
||||
_, err = setup.dynamicParamsClient.CreateUserSecret(ctx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
||||
Name: "github-token",
|
||||
Value: "ghp_test",
|
||||
EnvName: "GITHUB_TOKEN",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
preview = testutil.RequireReceive(ctx, t, previews)
|
||||
require.Equal(t, 0, preview.ID)
|
||||
require.Len(t, preview.SecretRequirements, 1)
|
||||
require.True(t, preview.SecretRequirements[0].Satisfied)
|
||||
|
||||
err = setup.dynamicParamsClient.DeleteUserSecret(ctx, codersdk.Me, "github-token")
|
||||
require.NoError(t, err)
|
||||
|
||||
preview = testutil.RequireReceive(ctx, t, previews)
|
||||
require.Equal(t, 1, preview.ID)
|
||||
require.Len(t, preview.SecretRequirements, 1)
|
||||
require.False(t, preview.SecretRequirements[0].Satisfied)
|
||||
|
||||
_, err = setup.dynamicParamsClient.CreateUserSecret(ctx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
||||
Name: "github-token",
|
||||
Value: "ghp_test",
|
||||
EnvName: "GITHUB_TOKEN",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
preview = testutil.RequireReceive(ctx, t, previews)
|
||||
require.Equal(t, 2, preview.ID)
|
||||
require.Len(t, preview.SecretRequirements, 1)
|
||||
require.True(t, preview.SecretRequirements[0].Satisfied)
|
||||
|
||||
otherEnvName := "OTHER_GITHUB_TOKEN"
|
||||
_, err = setup.dynamicParamsClient.UpdateUserSecret(ctx, codersdk.Me, "github-token", codersdk.UpdateUserSecretRequest{
|
||||
EnvName: &otherEnvName,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
preview = testutil.RequireReceive(ctx, t, previews)
|
||||
require.Equal(t, 3, preview.ID)
|
||||
require.Len(t, preview.SecretRequirements, 1)
|
||||
require.False(t, preview.SecretRequirements[0].Satisfied)
|
||||
})
|
||||
|
||||
t.Run("SecretRequirementPushesAfterOwnerSwitch", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/secret_required/main.tf")
|
||||
require.NoError(t, err)
|
||||
|
||||
setup := setupDynamicParamsTest(t, setupDynamicParamsTestParams{
|
||||
// No production role grants cross-user user_secret:read today,
|
||||
// so use an allow-all authorizer for lifecycle coverage.
|
||||
authorizer: &coderdtest.FakeAuthorizer{},
|
||||
provisionerDaemonVersion: provProto.CurrentVersion.String(),
|
||||
mainTF: dynamicParametersTerraformSource,
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
previews := setup.stream.Chan()
|
||||
targetClient, target := coderdtest.CreateAnotherUser(t, setup.client, setup.template.OrganizationID)
|
||||
|
||||
preview := testutil.RequireReceive(ctx, t, previews)
|
||||
require.Equal(t, -1, preview.ID)
|
||||
require.Len(t, preview.SecretRequirements, 1)
|
||||
require.False(t, preview.SecretRequirements[0].Satisfied)
|
||||
|
||||
err = setup.stream.Send(codersdk.DynamicParametersRequest{
|
||||
ID: 0,
|
||||
Inputs: map[string]string{},
|
||||
OwnerID: target.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
preview = testutil.RequireReceive(ctx, t, previews)
|
||||
require.Equal(t, 0, preview.ID)
|
||||
require.Len(t, preview.SecretRequirements, 1)
|
||||
require.False(t, preview.SecretRequirements[0].Satisfied)
|
||||
|
||||
_, err = targetClient.CreateUserSecret(ctx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
||||
Name: "github-token",
|
||||
Value: "ghp_target",
|
||||
EnvName: "GITHUB_TOKEN",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
preview = testutil.RequireReceive(ctx, t, previews)
|
||||
require.Equal(t, 1, preview.ID)
|
||||
require.Len(t, preview.SecretRequirements, 1)
|
||||
require.True(t, preview.SecretRequirements[0].Satisfied)
|
||||
|
||||
_, err = setup.dynamicParamsClient.CreateUserSecret(ctx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
||||
Name: "github-token",
|
||||
Value: "ghp_initial",
|
||||
EnvName: "GITHUB_TOKEN",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Never(t, func() bool {
|
||||
select {
|
||||
case <-previews:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}, testutil.WaitShort/5, testutil.IntervalFast)
|
||||
})
|
||||
|
||||
t.Run("SecretRequirementDoesNotSubscribeWhenOwnerUnauthorized", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/secret_required/main.tf")
|
||||
require.NoError(t, err)
|
||||
|
||||
setup := setupDynamicParamsTest(t, setupDynamicParamsTestParams{
|
||||
provisionerDaemonVersion: provProto.CurrentVersion.String(),
|
||||
mainTF: dynamicParametersTerraformSource,
|
||||
})
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
previews := setup.stream.Chan()
|
||||
targetClient, target := coderdtest.CreateAnotherUser(t, setup.client, setup.template.OrganizationID)
|
||||
|
||||
preview := testutil.RequireReceive(ctx, t, previews)
|
||||
require.Equal(t, -1, preview.ID)
|
||||
require.Len(t, preview.SecretRequirements, 1)
|
||||
require.False(t, preview.SecretRequirements[0].Satisfied)
|
||||
|
||||
err = setup.stream.Send(codersdk.DynamicParametersRequest{
|
||||
ID: 0,
|
||||
Inputs: map[string]string{},
|
||||
OwnerID: target.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
preview = testutil.RequireReceive(ctx, t, previews)
|
||||
require.Equal(t, 0, preview.ID)
|
||||
require.Empty(t, preview.SecretRequirements)
|
||||
require.Len(t, preview.Diagnostics, 1)
|
||||
require.Equal(t, dynamicparameters.DiagCodeSecretValidationForbidden, preview.Diagnostics[0].Extra.Code)
|
||||
|
||||
_, err = targetClient.CreateUserSecret(ctx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
||||
Name: "github-token",
|
||||
Value: "ghp_target",
|
||||
EnvName: "GITHUB_TOKEN",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Never(t, func() bool {
|
||||
select {
|
||||
case <-previews:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}, testutil.WaitShort/5, testutil.IntervalFast)
|
||||
})
|
||||
|
||||
// Regression test for PLAT-100: a workspace whose template has an
|
||||
// unsatisfied coder_secret requirement must still be stoppable and
|
||||
// deletable. Start remains blocked.
|
||||
@@ -475,6 +652,7 @@ func TestDynamicParametersWithTerraformValues(t *testing.T) {
|
||||
type setupDynamicParamsTestParams struct {
|
||||
db database.Store
|
||||
ps pubsub.Pubsub
|
||||
authorizer rbac.Authorizer
|
||||
provisionerDaemonVersion string
|
||||
mainTF []byte
|
||||
modulesArchive []byte
|
||||
@@ -486,16 +664,18 @@ type setupDynamicParamsTestParams struct {
|
||||
}
|
||||
|
||||
type dynamicParamsTest struct {
|
||||
client *codersdk.Client
|
||||
api *coderd.API
|
||||
stream *wsjson.Stream[codersdk.DynamicParametersResponse, codersdk.DynamicParametersRequest]
|
||||
template codersdk.Template
|
||||
client *codersdk.Client
|
||||
dynamicParamsClient *codersdk.Client
|
||||
api *coderd.API
|
||||
stream *wsjson.Stream[codersdk.DynamicParametersResponse, codersdk.DynamicParametersRequest]
|
||||
template codersdk.Template
|
||||
}
|
||||
|
||||
func setupDynamicParamsTest(t *testing.T, args setupDynamicParamsTestParams) dynamicParamsTest {
|
||||
ownerClient, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{
|
||||
Database: args.db,
|
||||
Pubsub: args.ps,
|
||||
Authorizer: args.authorizer,
|
||||
IncludeProvisionerDaemon: true,
|
||||
ProvisionerDaemonVersion: args.provisionerDaemonVersion,
|
||||
})
|
||||
@@ -530,10 +710,11 @@ func setupDynamicParamsTest(t *testing.T, args setupDynamicParamsTestParams) dyn
|
||||
})
|
||||
|
||||
return dynamicParamsTest{
|
||||
client: ownerClient,
|
||||
api: api,
|
||||
stream: stream,
|
||||
template: tpl,
|
||||
client: ownerClient,
|
||||
dynamicParamsClient: templateAdmin,
|
||||
api: api,
|
||||
stream: stream,
|
||||
template: tpl,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user