Automation API - add recovery APIs (cancel/export/import) (#5369)

This commit is contained in:
Evan Boyle 2020-09-15 14:20:58 -07:00 committed by GitHub
parent 4e6ea760db
commit 0e3666cc36
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 268 additions and 1 deletions

View file

@ -27,6 +27,9 @@ CHANGELOG
- Automation API - support streaming output for Up/Refresh/Destroy operations.
[#5367](https://github.com/pulumi/pulumi/pull/5367)
- Automation API - add recovery APIs (cancel/export/import)
[#5369](https://github.com/pulumi/pulumi/pull/5369)
## 2.10.0 (2020-09-10)
- feat(autoapi): add Upsert methods for stacks

View file

@ -70,7 +70,7 @@ func newCancelCmd() *cobra.Command {
// Ensure the user really wants to do this.
stackName := string(s.Ref().Name())
prompt := fmt.Sprintf("This will irreversibly cancel the currently running update for '%s'!", stackName)
if !yes && !confirmPrompt(prompt, stackName, opts) {
if cmdutil.Interactive() && (!yes && !confirmPrompt(prompt, stackName, opts)) {
fmt.Println("confirmation declined")
return result.Bail()
}

View file

@ -17,6 +17,7 @@ package auto
import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
@ -25,6 +26,7 @@ import (
"path/filepath"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/sdk/v2/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v2/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v2/go/common/workspace"
"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
@ -1075,3 +1077,98 @@ func ExampleStack_SetAllConfig() {
// set all config in map
_ = stack.SetAllConfig(ctx, cfg)
}
func ExampleStack_Cancel() {
ctx := context.Background()
stackName := FullyQualifiedStackName("org", "project", "stack")
stack, _ := SelectStackLocalSource(ctx, stackName, filepath.Join(".", "program"))
// attempt to cancel the in progress operation
// note that this operation is _very dangerous_, and may leave the stack in an inconsistent state.
_ = stack.Cancel(ctx)
}
func ExampleStack_Export() {
ctx := context.Background()
stackName := FullyQualifiedStackName("org", "project", "stack")
stack, _ := SelectStackLocalSource(ctx, stackName, filepath.Join(".", "program"))
dep, _ := stack.Export(ctx)
// import/export is backwards compatible, and we must write code specific to the verison we're dealing with.
if dep.Version != 3 {
panic("expected deployment version 3")
}
var state apitype.DeploymentV3
_ = json.Unmarshal(dep.Deployment, &state)
// ... perform edits on the state ...
// marshal out updated deployment state
bytes, _ := json.Marshal(state)
dep.Deployment = bytes
// import our edited deployment state back to our stack
_ = stack.Import(ctx, dep)
}
func ExampleStack_Import() {
ctx := context.Background()
stackName := FullyQualifiedStackName("org", "project", "stack")
stack, _ := SelectStackLocalSource(ctx, stackName, filepath.Join(".", "program"))
dep, _ := stack.Export(ctx)
// import/export is backwards compatible, and we must write code specific to the verison we're dealing with.
if dep.Version != 3 {
panic("expected deployment version 3")
}
var state apitype.DeploymentV3
_ = json.Unmarshal(dep.Deployment, &state)
// ... perform edits on the state ...
// marshal out updated deployment state
bytes, _ := json.Marshal(state)
dep.Deployment = bytes
// import our edited deployment state back to our stack
_ = stack.Import(ctx, dep)
}
func ExampleLocalWorkspace_ExportStack() {
ctx := context.Background()
// create a workspace from a local project
w, _ := NewLocalWorkspace(ctx, WorkDir(filepath.Join(".", "program")))
stackName := FullyQualifiedStackName("org", "proj", "existing_stack")
dep, _ := w.ExportStack(ctx, stackName)
// import/export is backwards compatible, and we must write code specific to the verison we're dealing with.
if dep.Version != 3 {
panic("expected deployment version 3")
}
var state apitype.DeploymentV3
_ = json.Unmarshal(dep.Deployment, &state)
// ... perform edits on the state ...
// marshal out updated deployment state
bytes, _ := json.Marshal(state)
dep.Deployment = bytes
// import our edited deployment state back to our stack
_ = w.ImportStack(ctx, stackName, dep)
}
func ExampleLocalWorkspace_ImportStack() {
ctx := context.Background()
// create a workspace from a local project
w, _ := NewLocalWorkspace(ctx, WorkDir(filepath.Join(".", "program")))
stackName := FullyQualifiedStackName("org", "proj", "existing_stack")
dep, _ := w.ExportStack(ctx, stackName)
// import/export is backwards compatible, and we must write code specific to the verison we're dealing with.
if dep.Version != 3 {
panic("expected deployment version 3")
}
var state apitype.DeploymentV3
_ = json.Unmarshal(dep.Deployment, &state)
// ... perform edits on the state ...
// marshal out updated deployment state
bytes, _ := json.Marshal(state)
dep.Deployment = bytes
// import our edited deployment state back to our stack
_ = w.ImportStack(ctx, stackName, dep)
}

View file

@ -24,7 +24,9 @@ import (
"strings"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/sdk/v2/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v2/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v2/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v2/go/common/workspace"
"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
)
@ -409,6 +411,62 @@ func (l *LocalWorkspace) SetProgram(fn pulumi.RunFunc) {
l.program = fn
}
// ExportStack exports the deployment state of the stack matching the given name.
// This can be combined with ImportStack to edit a stack's state (such as recovery from failed deployments).
func (l *LocalWorkspace) ExportStack(ctx context.Context, stackName string) (apitype.UntypedDeployment, error) {
var state apitype.UntypedDeployment
err := l.SelectStack(ctx, stackName)
if err != nil {
return state, errors.Wrapf(err, "could not export stack, unable to select stack %s.", stackName)
}
stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, "stack", "export", "--show-secrets")
if err != nil {
return state, newAutoError(errors.Wrap(err, "could not export stack."), stdout, stderr, errCode)
}
err = json.Unmarshal([]byte(stdout), &state)
if err != nil {
return state, newAutoError(
errors.Wrap(err, "failed to export stack, unable to unmarshall stack state."), stdout, stderr, errCode,
)
}
return state, nil
}
// ImportStack imports the specified deployment state into a pre-existing stack.
// This can be combined with ExportStack to edit a stack's state (such as recovery from failed deployments).
func (l *LocalWorkspace) ImportStack(ctx context.Context, stackName string, state apitype.UntypedDeployment) error {
err := l.SelectStack(ctx, stackName)
if err != nil {
return errors.Wrapf(err, "could not import stack, failed to select stack %s.", stackName)
}
f, err := ioutil.TempFile(os.TempDir(), "")
if err != nil {
return errors.Wrap(err, "could not import stack. failed to get allocate temp file.")
}
defer func() { contract.IgnoreError(os.Remove(f.Name())) }()
bytes, err := json.Marshal(state)
if err != nil {
return errors.Wrap(err, "could not import stack, failed to marshal stack state.")
}
_, err = f.Write(bytes)
if err != nil {
return errors.Wrap(err, "could not import stack. failed to write out stack intermediate.")
}
stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, "stack", "import", "--file", f.Name())
if err != nil {
return newAutoError(errors.Wrap(err, "could not import stack."), stdout, stderr, errCode)
}
return nil
}
func (l *LocalWorkspace) runPulumiCmdSync(
ctx context.Context,
args ...string,

View file

@ -1012,6 +1012,78 @@ func TestProgressStreams(t *testing.T) {
assert.Equal(t, desOut.String(), dRes.StdOut, "expected stdout writers to contain same contents")
}
func TestImportExportStack(t *testing.T) {
ctx := context.Background()
sName := fmt.Sprintf("int_test%d", rangeIn(10000000, 99999999))
stackName := FullyQualifiedStackName(pulumiOrg, pName, sName)
cfg := ConfigMap{
"bar": ConfigValue{
Value: "abc",
},
"buzz": ConfigValue{
Value: "secret",
Secret: true,
},
}
// initialize
s, err := NewStackInlineSource(ctx, stackName, pName, func(ctx *pulumi.Context) error {
c := config.New(ctx, "")
ctx.Export("exp_static", pulumi.String("foo"))
ctx.Export("exp_cfg", pulumi.String(c.Get("bar")))
ctx.Export("exp_secret", c.GetSecret("buzz"))
return nil
})
if err != nil {
t.Errorf("failed to initialize stack, err: %v", err)
t.FailNow()
}
defer func() {
// -- pulumi stack rm --
err = s.Workspace().RemoveStack(ctx, s.Name())
assert.Nil(t, err, "failed to remove stack. Resources have leaked.")
}()
err = s.SetAllConfig(ctx, cfg)
if err != nil {
t.Errorf("failed to set config, err: %v", err)
t.FailNow()
}
// -- pulumi up --
_, err = s.Up(ctx)
if err != nil {
t.Errorf("up failed, err: %v", err)
t.FailNow()
}
// -- pulumi stack export --
state, err := s.Export(ctx)
if err != nil {
t.Errorf("export failed, err: %v", err)
t.FailNow()
}
// -- pulumi stack import --
err = s.Import(ctx, state)
if err != nil {
t.Errorf("import failed, err: %v", err)
t.FailNow()
}
// -- pulumi destroy --
dRes, err := s.Destroy(ctx)
if err != nil {
t.Errorf("destroy failed, err: %v", err)
t.FailNow()
}
assert.Equal(t, "destroy", dRes.Summary.Kind)
assert.Equal(t, "succeeded", dRes.Summary.Result)
}
func getTestOrg() string {
testOrg := "pulumi-test"
if _, set := os.LookupEnv("PULUMI_TEST_ORG"); set {

View file

@ -576,6 +576,36 @@ func (s *Stack) Info(ctx context.Context) (StackSummary, error) {
return info, nil
}
// Cancel stops a stack's currently running update. It returns an error if no update is currently running.
// Note that this operation is _very dangerous_, and may leave the stack in an inconsistent state
// if a resource operation was pending when the update was canceled.
// This command is not supported for local backends.
func (s *Stack) Cancel(ctx context.Context) error {
err := s.Workspace().SelectStack(ctx, s.Name())
if err != nil {
return errors.Wrap(err, "failed to cancel update")
}
stdout, stderr, errCode, err := s.runPulumiCmdSync(ctx, nil /* additionalOutput */, "cancel", "--yes")
if err != nil {
return newAutoError(errors.Wrap(err, "failed to cancel update"), stdout, stderr, errCode)
}
return nil
}
// Export exports the deployment state of the stack.
// This can be combined with Stack.Import to edit a stack's state (such as recovery from failed deployments).
func (s *Stack) Export(ctx context.Context) (apitype.UntypedDeployment, error) {
return s.Workspace().ExportStack(ctx, s.Name())
}
// Import imports the specified deployment state into the stack.
// This can be combined with Stack.Export to edit a stack's state (such as recovery from failed deployments).
func (s *Stack) Import(ctx context.Context, state apitype.UntypedDeployment) error {
return s.Workspace().ImportStack(ctx, s.Name(), state)
}
// UpdateSummary provides a summary of a Stack lifecycle operation (up/preview/refresh/destroy).
type UpdateSummary struct {
Kind string `json:"kind"`

View file

@ -17,6 +17,7 @@ package auto
import (
"context"
"github.com/pulumi/pulumi/sdk/v2/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v2/go/common/workspace"
"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
)
@ -96,6 +97,12 @@ type Workspace interface {
Program() pulumi.RunFunc
// SetProgram sets the program associated with the Workspace to the specified `pulumi.RunFunc`.
SetProgram(pulumi.RunFunc)
// ExportStack exports the deployment state of the stack matching the given name.
// This can be combined with ImportStack to edit a stack's state (such as recovery from failed deployments).
ExportStack(context.Context, string) (apitype.UntypedDeployment, error)
// ImportStack imports the specified deployment state into a pre-existing stack.
// This can be combined with ExportStack to edit a stack's state (such as recovery from failed deployments).
ImportStack(context.Context, string, apitype.UntypedDeployment) error
}
// ConfigValue is a configuration value used by a Pulumi program.