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.
|
- Automation API - support streaming output for Up/Refresh/Destroy operations.
|
||||||
[#5367](https://github.com/pulumi/pulumi/pull/5367)
|
[#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)
|
## 2.10.0 (2020-09-10)
|
||||||
|
|
||||||
- feat(autoapi): add Upsert methods for stacks
|
- feat(autoapi): add Upsert methods for stacks
|
||||||
|
|
|
@ -70,7 +70,7 @@ func newCancelCmd() *cobra.Command {
|
||||||
// Ensure the user really wants to do this.
|
// Ensure the user really wants to do this.
|
||||||
stackName := string(s.Ref().Name())
|
stackName := string(s.Ref().Name())
|
||||||
prompt := fmt.Sprintf("This will irreversibly cancel the currently running update for '%s'!", stackName)
|
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")
|
fmt.Println("confirmation declined")
|
||||||
return result.Bail()
|
return result.Bail()
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ package auto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -25,6 +26,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"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/tokens"
|
||||||
"github.com/pulumi/pulumi/sdk/v2/go/common/workspace"
|
"github.com/pulumi/pulumi/sdk/v2/go/common/workspace"
|
||||||
"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
|
"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
|
||||||
|
@ -1075,3 +1077,98 @@ func ExampleStack_SetAllConfig() {
|
||||||
// set all config in map
|
// set all config in map
|
||||||
_ = stack.SetAllConfig(ctx, cfg)
|
_ = 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"
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"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/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/common/workspace"
|
||||||
"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
|
"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
|
||||||
)
|
)
|
||||||
|
@ -409,6 +411,62 @@ func (l *LocalWorkspace) SetProgram(fn pulumi.RunFunc) {
|
||||||
l.program = fn
|
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(
|
func (l *LocalWorkspace) runPulumiCmdSync(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
args ...string,
|
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")
|
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 {
|
func getTestOrg() string {
|
||||||
testOrg := "pulumi-test"
|
testOrg := "pulumi-test"
|
||||||
if _, set := os.LookupEnv("PULUMI_TEST_ORG"); set {
|
if _, set := os.LookupEnv("PULUMI_TEST_ORG"); set {
|
||||||
|
|
|
@ -576,6 +576,36 @@ func (s *Stack) Info(ctx context.Context) (StackSummary, error) {
|
||||||
return info, nil
|
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).
|
// UpdateSummary provides a summary of a Stack lifecycle operation (up/preview/refresh/destroy).
|
||||||
type UpdateSummary struct {
|
type UpdateSummary struct {
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
|
|
|
@ -17,6 +17,7 @@ package auto
|
||||||
import (
|
import (
|
||||||
"context"
|
"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/common/workspace"
|
||||||
"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
|
"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
|
||||||
)
|
)
|
||||||
|
@ -96,6 +97,12 @@ type Workspace interface {
|
||||||
Program() pulumi.RunFunc
|
Program() pulumi.RunFunc
|
||||||
// SetProgram sets the program associated with the Workspace to the specified `pulumi.RunFunc`.
|
// SetProgram sets the program associated with the Workspace to the specified `pulumi.RunFunc`.
|
||||||
SetProgram(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.
|
// ConfigValue is a configuration value used by a Pulumi program.
|
||||||
|
|
Loading…
Reference in a new issue