Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add paths to rotate #440

Merged
merged 3 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
- Introduce support for rotating static credentials via `fn::rotate` providers [432](https://github.com/pulumi/esc/pull/432)
- Add the `rotate` CLI command
[#433](https://github.com/pulumi/esc/pull/433)
- Add ability to pass specific paths to rotate with the `rotate` CLI command
[#440](https://github.com/pulumi/esc/pull/440)

### Bug Fixes

Expand Down
1 change: 1 addition & 0 deletions cmd/esc/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
11 changes: 10 additions & 1 deletion cmd/esc/cli/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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"`
}{
Expand All @@ -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 {
Expand Down
14 changes: 10 additions & 4 deletions cmd/esc/cli/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
})
Expand All @@ -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")
})
}
Expand Down
18 changes: 15 additions & 3 deletions cmd/esc/cli/env_rotate.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ func newEnvRotateCmd(envcmd *envCommand) *cobra.Command {
var format string

cmd := &cobra.Command{
Use: "rotate [<org-name>/][<project-name>/]<environment-name>",
Use: "rotate [<org-name>/][<project-name>/]<environment-name> [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",
Expand All @@ -42,14 +44,23 @@ 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("'%s' is an invalid property path: %w", arg, err)
}
rotationPaths = append(rotationPaths, arg)
}

switch format {
case "detailed", "json", "yaml", "string", "dotenv", "shell":
// OK
default:
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
}
Expand All @@ -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
}
Expand Down
Loading