Automation API - add recovery APIs (cancel/export/import) (#5369)
This commit is contained in:
parent
4e6ea760db
commit
0e3666cc36
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue