diff --git a/cmd/esc/cli/cli_test.go b/cmd/esc/cli/cli_test.go index 2ca47c17..9ca4e6c0 100644 --- a/cmd/esc/cli/cli_test.go +++ b/cmd/esc/cli/cli_test.go @@ -683,6 +683,7 @@ func (c *testPulumiClient) RotateEnvironment( projectName string, envName string, duration time.Duration, + rotationPaths []string, ) (string, []client.EnvironmentDiagnostic, error) { return c.OpenEnvironment(ctx, orgName, projectName, envName, "", duration) } diff --git a/cmd/esc/cli/client/client.go b/cmd/esc/cli/client/client.go index 21d662cd..131af482 100644 --- a/cmd/esc/cli/client/client.go +++ b/cmd/esc/cli/client/client.go @@ -156,6 +156,7 @@ type Client interface { ) (string, []EnvironmentDiagnostic, error) // RotateEnvironment will rotate credentials in an environment. + // If rotationPaths is non-empty, will only rotate credentials at those paths. // It also evaluates the environment projectName/envName in org orgName and returns the ID of the opened // environment. The opened environment will be available for the indicated duration, after which it // will expire. @@ -167,6 +168,7 @@ type Client interface { projectName string, envName string, duration time.Duration, + rotationPaths []string, ) (string, []EnvironmentDiagnostic, error) // CheckYAMLEnvironment checks the given environment YAML for errors within the context of org orgName. @@ -636,9 +638,16 @@ func (pc *client) RotateEnvironment( projectName string, envName string, duration time.Duration, + rotationPaths []string, ) (string, []EnvironmentDiagnostic, error) { path := fmt.Sprintf("/api/esc/environments/%v/%v/%v/rotate", orgName, projectName, envName) + reqObj := struct { + Paths []string `url:"paths"` + }{ + Paths: rotationPaths, + } + queryObj := struct { Duration string `url:"duration"` }{ @@ -648,7 +657,7 @@ func (pc *client) RotateEnvironment( ID string `json:"id"` } var errResp EnvironmentErrorResponse - err := pc.restCallWithOptions(ctx, http.MethodPost, path, queryObj, nil, &resp, httpCallOptions{ + err := pc.restCallWithOptions(ctx, http.MethodPost, path, queryObj, reqObj, &resp, httpCallOptions{ ErrorResponse: &errResp, }) if err != nil { diff --git a/cmd/esc/cli/client/client_test.go b/cmd/esc/cli/client/client_test.go index a833b585..d0fdb0f0 100644 --- a/cmd/esc/cli/client/client_test.go +++ b/cmd/esc/cli/client/client_test.go @@ -505,18 +505,24 @@ func TestOpenEnvironment(t *testing.T) { } func TestRotateEnvironment(t *testing.T) { + rotationPaths := []string{"a.b", "c"} t.Run("OK", func(t *testing.T) { const expectedID = "open-id" duration := 2 * time.Hour + expectedBody := "{\"Paths\":[\"a.b\",\"c\"]}" client := newTestClient(t, http.MethodPost, "/api/esc/environments/test-org/test-project/test-env/rotate", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, duration.String(), r.URL.Query().Get("duration")) - err := json.NewEncoder(w).Encode(map[string]any{"id": expectedID}) + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + assert.Equal(t, expectedBody, string(body)) + + err = json.NewEncoder(w).Encode(map[string]any{"id": expectedID}) require.NoError(t, err) }) - id, diags, err := client.RotateEnvironment(context.Background(), "test-org", "test-project", "test-env", duration) + id, diags, err := client.RotateEnvironment(context.Background(), "test-org", "test-project", "test-env", duration, rotationPaths) require.NoError(t, err) assert.Equal(t, expectedID, id) assert.Empty(t, diags) @@ -553,7 +559,7 @@ func TestRotateEnvironment(t *testing.T) { require.NoError(t, err) }) - _, diags, err := client.RotateEnvironment(context.Background(), "test-org", "test-project", "test-env", 2*time.Hour) + _, diags, err := client.RotateEnvironment(context.Background(), "test-org", "test-project", "test-env", 2*time.Hour, rotationPaths) require.NoError(t, err) assert.Equal(t, expected, diags) }) @@ -569,7 +575,7 @@ func TestRotateEnvironment(t *testing.T) { require.NoError(t, err) }) - _, _, err := client.RotateEnvironment(context.Background(), "test-org", "test-project", "test-env", 2*time.Hour) + _, _, err := client.RotateEnvironment(context.Background(), "test-org", "test-project", "test-env", 2*time.Hour, rotationPaths) assert.ErrorContains(t, err, "not found") }) } diff --git a/cmd/esc/cli/env_rotate.go b/cmd/esc/cli/env_rotate.go index f386c22c..14a1dfb2 100644 --- a/cmd/esc/cli/env_rotate.go +++ b/cmd/esc/cli/env_rotate.go @@ -18,9 +18,11 @@ func newEnvRotateCmd(envcmd *envCommand) *cobra.Command { var format string cmd := &cobra.Command{ - Use: "rotate [/][/]", + Use: "rotate [/][/] [path(s) to rotate]", Short: "Rotate secrets and open the environment", Long: "Rotate secrets and open the environment\n" + + "\n" + + "Optionally accepts any number of Property Paths as additional arguments. If given any paths, will only rotate secrets at those paths.\n" + "\n" + "This command opens the environment with the given name. The result is written to\n" + "stdout as JSON.\n", @@ -42,6 +44,15 @@ func newEnvRotateCmd(envcmd *envCommand) *cobra.Command { return fmt.Errorf("the rotate command does not accept environments at specific versions") } + rotationPaths := []string{} + for _, arg := range args[1:] { + _, err := resource.ParsePropertyPath(arg) + if err != nil { + return fmt.Errorf("invalid path: %w", err) + } + rotationPaths = append(rotationPaths, arg) + } + switch format { case "detailed", "json", "yaml", "string", "dotenv", "shell": // OK @@ -49,7 +60,7 @@ func newEnvRotateCmd(envcmd *envCommand) *cobra.Command { return fmt.Errorf("unknown output format %q", format) } - env, diags, err := envcmd.rotateEnvironment(ctx, ref, duration) + env, diags, err := envcmd.rotateEnvironment(ctx, ref, duration, rotationPaths) if err != nil { return err } @@ -75,8 +86,9 @@ func (env *envCommand) rotateEnvironment( ctx context.Context, ref environmentRef, duration time.Duration, + rotationPaths []string, ) (*esc.Environment, []client.EnvironmentDiagnostic, error) { - envID, diags, err := env.esc.client.RotateEnvironment(ctx, ref.orgName, ref.projectName, ref.envName, duration) + envID, diags, err := env.esc.client.RotateEnvironment(ctx, ref.orgName, ref.projectName, ref.envName, duration, rotationPaths) if err != nil { return nil, nil, err }