From ba046b063bebec0efdd793e08490e519fe3d8c9e Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 13 Feb 2020 12:25:57 -0800 Subject: [PATCH] Add --version flag to 'pulumi stack export' (#3906) * Add --version flag to 'pulumi stack export' * Update CHANGELOG * Update/rebase with latest * Fix lint warning --- CHANGELOG.md | 3 +++ cmd/stack_export.go | 31 +++++++++++++++++++++++--- pkg/backend/backend.go | 11 +++++++++ pkg/backend/httpstate/backend.go | 26 +++++++++++++++++---- pkg/backend/httpstate/client/client.go | 14 +++++++++--- pkg/backend/httpstate/state.go | 2 +- 6 files changed, 76 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff218f781..9ad9f2f70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ CHANGELOG - Move .NET SDK attributes to the root namespace. [#3902](https://github.com/pulumi/pulumi/pull/3902) +- Support exporting older stack versions. + [#3906](https://github.com/pulumi/pulumi/pull/3906) + ## 1.10.1 (2020-02-06) - Support stack references in the Go SDK. [#3829](https://github.com/pulumi/pulumi/pull/3829) diff --git a/cmd/stack_export.go b/cmd/stack_export.go index 313bc877b..f39d47a0e 100644 --- a/cmd/stack_export.go +++ b/cmd/stack_export.go @@ -21,6 +21,8 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/pulumi/pulumi/pkg/apitype" + "github.com/pulumi/pulumi/pkg/backend" "github.com/pulumi/pulumi/pkg/backend/display" "github.com/pulumi/pulumi/pkg/util/cmdutil" ) @@ -28,6 +30,7 @@ import ( func newStackExportCmd() *cobra.Command { var file string var stackName string + var version string cmd := &cobra.Command{ Use: "export", @@ -40,6 +43,7 @@ func newStackExportCmd() *cobra.Command { "in a stack's state due to failed deployments, manual changes to cloud\n" + "resources, etc.", Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error { + ctx := commandContext() opts := display.Options{ Color: cmdutil.GetGlobalColorization(), } @@ -50,9 +54,28 @@ func newStackExportCmd() *cobra.Command { return err } - deployment, err := s.ExportDeployment(commandContext()) - if err != nil { - return err + var deployment *apitype.UntypedDeployment + // Export the latest version of the checkpoint by default. Otherwise, we require that + // the backend/stack implements the ability the export previous checkpoints. + if version == "" { + deployment, err = s.ExportDeployment(ctx) + if err != nil { + return err + } + } else { + // Check that the stack and its backend supports the ability to do this. + be := s.Backend() + specificExpBE, ok := be.(backend.SpecificDeploymentExporter) + if !ok { + return errors.Errorf( + "the current backend (%s) does not provide the ability to export previous deployments", + be.Name()) + } + + deployment, err = specificExpBE.ExportDeploymentForVersion(ctx, s, version) + if err != nil { + return err + } } // Read from stdin or a specified file. @@ -77,5 +100,7 @@ func newStackExportCmd() *cobra.Command { &stackName, "stack", "s", "", "The name of the stack to operate on. Defaults to the current stack") cmd.PersistentFlags().StringVarP( &file, "file", "", "", "A filename to write stack output to") + cmd.PersistentFlags().StringVarP( + &version, "version", "", "", "Previous stack version to export. (If unset, will export the latest.)") return cmd } diff --git a/pkg/backend/backend.go b/pkg/backend/backend.go index d1423ec91..769923bb9 100644 --- a/pkg/backend/backend.go +++ b/pkg/backend/backend.go @@ -189,6 +189,17 @@ type Backend interface { CurrentUser() (string, error) } +// SpecificDeploymentExporter is an interface defining an additional capability of a Backend, specifically the +// ability to export a specific versions of a stack's deployment. This isn't a requirement for all backends and +// should be checked for dynamically. +type SpecificDeploymentExporter interface { + // ExportDeploymentForVersion exports a specific deployment from the history of a stack. The meaning of + // version is backend-specific. For the Pulumi Console, it is the numeric version. (The first update + // being version "1", the second "2", and so on.) Though this might change in the future to use some + // other type of identifier or commitish . + ExportDeploymentForVersion(ctx context.Context, stack Stack, version string) (*apitype.UntypedDeployment, error) +} + // UpdateOperation is a complete stack update operation (preview, update, refresh, or destroy). type UpdateOperation struct { Proj *workspace.Project diff --git a/pkg/backend/httpstate/backend.go b/pkg/backend/httpstate/backend.go index 037aecf8a..dce2ab1df 100644 --- a/pkg/backend/httpstate/backend.go +++ b/pkg/backend/httpstate/backend.go @@ -125,6 +125,9 @@ type cloudBackend struct { currentProject *workspace.Project } +// Assert we implement the backend.Backend and backend.SpecificDeploymentExporter interfaces. +var _ backend.SpecificDeploymentExporter = &cloudBackend{} + // New creates a new Pulumi backend for the given cloud API URL and token. func New(d diag.Sink, cloudURL string) (Backend, error) { cloudURL = ValueOrDefaultURL(cloudURL) @@ -1160,17 +1163,32 @@ func (b *cloudBackend) GetLogs(ctx context.Context, stack backend.Stack, cfg bac func (b *cloudBackend) ExportDeployment(ctx context.Context, stack backend.Stack) (*apitype.UntypedDeployment, error) { - return b.exportDeployment(ctx, stack.Ref()) + return b.exportDeployment(ctx, stack.Ref(), nil /* latest */) } -func (b *cloudBackend) exportDeployment(ctx context.Context, - stackRef backend.StackReference) (*apitype.UntypedDeployment, error) { +func (b *cloudBackend) ExportDeploymentForVersion( + ctx context.Context, stack backend.Stack, version string) (*apitype.UntypedDeployment, error) { + // The Pulumi Console defines versions as a positive integer. Parse the provided version string and + // ensure it is valid. + // + // The first stack update version is 1, and monotonically increasing from there. + versionNumber, err := strconv.Atoi(version) + if err != nil || versionNumber <= 0 { + return nil, errors.Errorf("%q is not a valid stack version. It should be a positive integer.", version) + } + + return b.exportDeployment(ctx, stack.Ref(), &versionNumber) +} + +// exportDeployment exports the checkpoint file for a stack, optionally getting a previous version. +func (b *cloudBackend) exportDeployment( + ctx context.Context, stackRef backend.StackReference, version *int) (*apitype.UntypedDeployment, error) { stack, err := b.getCloudStackIdentifier(stackRef) if err != nil { return nil, err } - deployment, err := b.client.ExportStackDeployment(ctx, stack) + deployment, err := b.client.ExportStackDeployment(ctx, stack, version) if err != nil { return nil, err } diff --git a/pkg/backend/httpstate/client/client.go b/pkg/backend/httpstate/client/client.go index 2470eda66..aca728264 100644 --- a/pkg/backend/httpstate/client/client.go +++ b/pkg/backend/httpstate/client/client.go @@ -376,11 +376,19 @@ func (pc *Client) GetStackUpdates(ctx context.Context, stack StackIdentifier) ([ } // ExportStackDeployment exports the indicated stack's deployment as a raw JSON message. -func (pc *Client) ExportStackDeployment(ctx context.Context, - stack StackIdentifier) (apitype.UntypedDeployment, error) { +// If version is nil, will export the latest version of the stack. +func (pc *Client) ExportStackDeployment( + ctx context.Context, stack StackIdentifier, version *int) (apitype.UntypedDeployment, error) { + + path := getStackPath(stack, "export") + + // Tack on a specific version as desired. + if version != nil { + path += fmt.Sprintf("/%d", *version) + } var resp apitype.ExportStackResponse - if err := pc.restCall(ctx, "GET", getStackPath(stack, "export"), nil, nil, &resp); err != nil { + if err := pc.restCall(ctx, "GET", path, nil, nil, &resp); err != nil { return apitype.UntypedDeployment{}, err } diff --git a/pkg/backend/httpstate/state.go b/pkg/backend/httpstate/state.go index b8b2d9040..6d4cb5349 100644 --- a/pkg/backend/httpstate/state.go +++ b/pkg/backend/httpstate/state.go @@ -262,7 +262,7 @@ func (b *cloudBackend) newUpdate(ctx context.Context, stackRef backend.StackRefe } func (b *cloudBackend) getSnapshot(ctx context.Context, stackRef backend.StackReference) (*deploy.Snapshot, error) { - untypedDeployment, err := b.exportDeployment(ctx, stackRef) + untypedDeployment, err := b.exportDeployment(ctx, stackRef, nil /* get latest */) if err != nil { return nil, err }