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

Release Rotate CLI #459

Merged
merged 5 commits into from
Feb 19, 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 @@ -11,6 +11,8 @@
[#443](https://github.com/pulumi/esc/pull/443)
- Introduce rotateOnly inputs
[#444](https://github.com/pulumi/esc/pull/444)
- Release rotate environment CLI command
[#459](https://github.com/pulumi/esc/pull/459)

### Bug Fixes

Expand Down
4 changes: 2 additions & 2 deletions cmd/esc/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -683,8 +683,8 @@ func (c *testPulumiClient) RotateEnvironment(
projectName string,
envName string,
rotationPaths []string,
) ([]client.EnvironmentDiagnostic, error) {
return []client.EnvironmentDiagnostic{}, nil
) (*client.RotateEnvironmentResponse, []client.EnvironmentDiagnostic, error) {
return &client.RotateEnvironmentResponse{}, []client.EnvironmentDiagnostic{}, nil
}

func (c *testPulumiClient) CheckYAMLEnvironment(
Expand Down
43 changes: 43 additions & 0 deletions cmd/esc/cli/client/apitype.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,49 @@ type EnvironmentErrorResponse struct {
Diagnostics []EnvironmentDiagnostic `json:"diagnostics,omitempty"`
}

type RotateEnvironmentResponse struct {
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
Diagnostics []EnvironmentDiagnostic `json:"diagnostics,omitempty"`
SecretRotationEvent SecretRotationEvent `json:"secretRotationEvent,omitempty"`
}

type SecretRotationEvent struct {
ID string `json:"id"`
EnvironmentID string `json:"environmentId"`
CreatedAt time.Time `json:"created"`
PreRotationRevision int `json:"preRotationRevision"`
PostRotationRevision *int `json:"postRotationRevision,omitempty"`
UserID string `json:"userID"`
CompletedAt *time.Time `json:"completed,omitempty"`
Status RotationEventStatus `json:"status"`
ScheduledActionID *string `json:"scheduledActionID,omitempty"`
ErrorMessage *string `json:"errorMessage,omitempty"`
Rotations []SecretRotation `json:"rotations"`
}

type SecretRotation struct {
ID string `json:"id"`
EnvironmentPath string `json:"environmentPath"`
Status RotationStatus `json:"status"`
ErrorMessage *string `json:"errorMessage,omitempty"`
}

type RotationEventStatus string

const (
RotationEventSucceeded RotationEventStatus = "succeeded"
RotationEventFailed RotationEventStatus = "failed"
RotationEventInProgress RotationEventStatus = "in_progress"
)

type RotationStatus string

const (
RotationSucceeded RotationStatus = "succeeded"
RotationFailed RotationStatus = "failed"
)

func (err EnvironmentErrorResponse) Error() string {
errString := fmt.Sprintf("[%d] %s", err.Code, err.Message)
if len(err.Diagnostics) > 0 {
Expand Down
14 changes: 6 additions & 8 deletions cmd/esc/cli/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ type Client interface {
projectName string,
envName string,
rotationPaths []string,
) ([]EnvironmentDiagnostic, error)
) (*RotateEnvironmentResponse, []EnvironmentDiagnostic, error)

// CheckYAMLEnvironment checks the given environment YAML for errors within the context of org orgName.
//
Expand Down Expand Up @@ -632,7 +632,7 @@ func (pc *client) RotateEnvironment(
projectName string,
envName string,
rotationPaths []string,
) ([]EnvironmentDiagnostic, error) {
) (*RotateEnvironmentResponse, []EnvironmentDiagnostic, error) {
path := fmt.Sprintf("/api/esc/environments/%v/%v/%v/rotate", orgName, projectName, envName)

reqObj := struct {
Expand All @@ -641,21 +641,19 @@ func (pc *client) RotateEnvironment(
Paths: rotationPaths,
}

var resp struct {
ID string `json:"id"`
}
var resp RotateEnvironmentResponse
var errResp EnvironmentErrorResponse
err := pc.restCallWithOptions(ctx, http.MethodPost, path, nil, reqObj, &resp, httpCallOptions{
ErrorResponse: &errResp,
})
if err != nil {
var diags *EnvironmentErrorResponse
if errors.As(err, &diags) && diags.Code == http.StatusBadRequest && len(diags.Diagnostics) != 0 {
return diags.Diagnostics, nil
return nil, diags.Diagnostics, nil
}
return nil, err
return nil, nil, err
}
return nil, nil
return &resp, nil, nil
}

func (pc *client) CheckYAMLEnvironment(
Expand Down
6 changes: 3 additions & 3 deletions cmd/esc/cli/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ func TestRotateEnvironment(t *testing.T) {
require.NoError(t, err)
})

diags, err := client.RotateEnvironment(context.Background(), "test-org", "test-project", "test-env", rotationPaths)
_, diags, err := client.RotateEnvironment(context.Background(), "test-org", "test-project", "test-env", rotationPaths)
require.NoError(t, err)
assert.Empty(t, diags)
})
Expand Down Expand Up @@ -554,7 +554,7 @@ func TestRotateEnvironment(t *testing.T) {
require.NoError(t, err)
})

diags, err := client.RotateEnvironment(context.Background(), "test-org", "test-project", "test-env", rotationPaths)
_, diags, err := client.RotateEnvironment(context.Background(), "test-org", "test-project", "test-env", rotationPaths)
require.NoError(t, err)
assert.Equal(t, expected, diags)
})
Expand All @@ -570,7 +570,7 @@ func TestRotateEnvironment(t *testing.T) {
require.NoError(t, err)
})

_, err := client.RotateEnvironment(context.Background(), "test-org", "test-project", "test-env", rotationPaths)
_, _, err := client.RotateEnvironment(context.Background(), "test-org", "test-project", "test-env", rotationPaths)
assert.ErrorContains(t, err, "not found")
})
}
Expand Down
42 changes: 39 additions & 3 deletions cmd/esc/cli/env_rotate.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ package cli
import (
"context"
"fmt"
"strings"

"github.com/pulumi/esc/cmd/esc/cli/client"
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/spf13/cobra"
)
Expand All @@ -18,7 +21,6 @@ func newEnvRotateCmd(envcmd *envCommand) *cobra.Command {
"\n" +
"Optionally accepts any number of Property Paths as additional arguments. If given any paths, will only rotate secrets at those paths.\n",
SilenceUsage: true,
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()

Expand All @@ -44,15 +46,49 @@ func newEnvRotateCmd(envcmd *envCommand) *cobra.Command {
rotationPaths = append(rotationPaths, arg)
}

diags, err := envcmd.esc.client.RotateEnvironment(ctx, ref.orgName, ref.projectName, ref.envName, rotationPaths)
resp, diags, err := envcmd.esc.client.RotateEnvironment(ctx, ref.orgName, ref.projectName, ref.envName, rotationPaths)
if err != nil {
return err
}
if len(diags) != 0 {
return envcmd.writePropertyEnvironmentDiagnostics(envcmd.esc.stderr, diags)
}
if resp == nil {
return nil
}

event := resp.SecretRotationEvent

// Print result of rotation
var b strings.Builder
if event.Status == client.RotationEventSucceeded {
fmt.Fprintf(&b, "Environment '%s' rotated.\n", args[0])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe print the new revision too?

} else if event.Status == client.RotationEventFailed {
if event.ErrorMessage != nil {
fmt.Fprintf(&b, "%vError rotating: %s.%v\n", colors.SpecError, *event.ErrorMessage, colors.Reset)
} else {
fmt.Fprintf(&b, "%vEnvironment '%s' rotated with errors.%v\n", colors.SpecWarning, args[0], colors.Reset)
}
}
if event.PostRotationRevision != nil {
fmt.Fprintf(&b, "New revision '%d' was created.\n", *event.PostRotationRevision)
}

var failedRotations []client.SecretRotation
for _, rotation := range event.Rotations {
if rotation.Status == client.RotationFailed {
failedRotations = append(failedRotations, rotation)
}
}
if len(failedRotations) > 0 {
fmt.Fprintf(&b, "\n%vFailed secrets:%v\n", colors.SpecError, colors.Reset)
for _, rotation := range failedRotations {
fmt.Fprintf(&b, "Path: %s, error: %s\n", rotation.EnvironmentPath, *rotation.ErrorMessage)
}
}

fmt.Fprint(envcmd.esc.stdout, envcmd.esc.colors.Colorize(b.String()))

fmt.Fprintf(envcmd.esc.stdout, "Environment '%s' rotated.\n", args[0])
return nil
},
}
Expand Down
Loading