pulumi/pkg/backend/stack.go
joeduffy 6ad785d5c4 Revise the way previews are controlled
I found the flag --force to be a strange name for skipping a preview,
since that name is usually reserved for operations that might be harmful
and yet you're coercing a tool to do it anyway, knowing there's a chance
you're going to shoot yourself in the foot.

I also found that what I almost always want in the situation where
--force was being used is to actually just run a preview and have the
confirmation auto-accepted.  Going straight to --force isn't the right
thing in a CI scenario, where you actually want to run a preview first,
just to ensure there aren't any issues, before doing the update.

In a sense, there are four options here:

1. Run a preview, ask for confirmation, then do an update (the default).
2. Run a preview, auto-accept, and then do an update (the CI scenario).
3. Just run a preview with neither a confirmation nor an update (dry run).
4. Just do an update, without performing a preview beforehand (rare).

This change enables all four workflows in our CLI.

Rather than have an explosion of flags, we have a single flag,
--preview, which can specify the mode that we're operating in.  The
following are the values which correlate to the above four modes:

1. "": default (no --preview specified)
2. "auto": auto-accept preview confirmation
3. "only": only run a preview, don't confirm or update
4. "skip": skip the preview altogether

As part of this change, I redid a bit of how the preview modes
were specified.  Rather than booleans, which had some illegal
combinations, this change introduces a new enum type.  Furthermore,
because the engine is wholly ignorant of these flags -- and only the
backend understands them -- it was confusing to me that
engine.UpdateOptions stored this flag, especially given that all
interesting engine options _also_ accepted a dryRun boolean.  As of
this change, the backend.PreviewBehavior controls the preview options.
2018-05-06 13:55:04 -07:00

171 lines
6.5 KiB
Go

// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
package backend
import (
"regexp"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/apitype"
"github.com/pulumi/pulumi/pkg/engine"
"github.com/pulumi/pulumi/pkg/operations"
"github.com/pulumi/pulumi/pkg/resource/config"
"github.com/pulumi/pulumi/pkg/resource/deploy"
"github.com/pulumi/pulumi/pkg/workspace"
)
// Stack is a stack associated with a particular backend implementation.
type Stack interface {
Name() StackReference // this stack's identity.
Config() config.Map // the current config map.
Snapshot() *deploy.Snapshot // the latest deployment snapshot.
Backend() Backend // the backend this stack belongs to.
// Update this stack.
Update(proj *workspace.Project, root string, m UpdateMetadata,
opts engine.UpdateOptions, preview PreviewBehavior,
displayOpts DisplayOptions, scopes CancellationScopeSource) error
// Refresh this stack's state from the cloud provider.
Refresh(proj *workspace.Project, root string, m UpdateMetadata,
opts engine.UpdateOptions, preview PreviewBehavior,
displayOpts DisplayOptions, scopes CancellationScopeSource) error
// Destroy this stack's resources.
Destroy(proj *workspace.Project, root string, m UpdateMetadata,
opts engine.UpdateOptions, preview PreviewBehavior,
displayOpts DisplayOptions, scopes CancellationScopeSource) error
Remove(force bool) (bool, error) // remove this stack.
GetLogs(query operations.LogQuery) ([]operations.LogEntry, error) // list log entries for this stack.
ExportDeployment() (*apitype.UntypedDeployment, error) // export this stack's deployment.
ImportDeployment(deployment *apitype.UntypedDeployment) error // import the given deployment into this stack.
}
// RemoveStack returns the stack, or returns an error if it cannot.
func RemoveStack(s Stack, force bool) (bool, error) {
return s.Backend().RemoveStack(s.Name(), force)
}
// UpdateStack updates the target stack with the current workspace's contents (config and code).
func UpdateStack(s Stack, proj *workspace.Project, root string, m UpdateMetadata,
opts engine.UpdateOptions, preview PreviewBehavior,
displayOpts DisplayOptions, scopes CancellationScopeSource) error {
return s.Backend().Update(s.Name(), proj, root, m, opts, preview, displayOpts, scopes)
}
// RefreshStack refresh's the stack's state from the cloud provider.
func RefreshStack(s Stack, proj *workspace.Project, root string, m UpdateMetadata,
opts engine.UpdateOptions, preview PreviewBehavior,
displayOpts DisplayOptions, scopes CancellationScopeSource) error {
return s.Backend().Refresh(s.Name(), proj, root, m, opts, preview, displayOpts, scopes)
}
// DestroyStack destroys all of this stack's resources.
func DestroyStack(s Stack, proj *workspace.Project, root string, m UpdateMetadata,
opts engine.UpdateOptions, preview PreviewBehavior,
displayOpts DisplayOptions, scopes CancellationScopeSource) error {
return s.Backend().Destroy(s.Name(), proj, root, m, opts, preview, displayOpts, scopes)
}
// GetStackCrypter fetches the encrypter/decrypter for a stack.
func GetStackCrypter(s Stack) (config.Crypter, error) {
return s.Backend().GetStackCrypter(s.Name())
}
// GetStackLogs fetches a list of log entries for the current stack in the current backend.
func GetStackLogs(s Stack, query operations.LogQuery) ([]operations.LogEntry, error) {
return s.Backend().GetLogs(s.Name(), query)
}
// ExportStackDeployment exports the given stack's deployment as an opaque JSON message.
func ExportStackDeployment(s Stack) (*apitype.UntypedDeployment, error) {
return s.Backend().ExportDeployment(s.Name())
}
// ImportStackDeployment imports the given deployment into the indicated stack.
func ImportStackDeployment(s Stack, deployment *apitype.UntypedDeployment) error {
return s.Backend().ImportDeployment(s.Name(), deployment)
}
// GetStackTags returns the set of tags for the "current" stack, based on the environment
// and Pulumi.yaml file.
func GetStackTags() (map[apitype.StackTagName]string, error) {
tags := make(map[apitype.StackTagName]string)
// Tags based on the workspace's repository.
w, err := workspace.New()
if err != nil {
return nil, err
}
repo := w.Repository()
if repo != nil {
tags[apitype.GitHubOwnerNameTag] = repo.Owner
tags[apitype.GitHubRepositoryNameTag] = repo.Name
}
// Tags based on Pulumi.yaml.
projPath, err := workspace.DetectProjectPath()
if err != nil {
return nil, err
}
if projPath != "" {
proj, err := workspace.LoadProject(projPath)
if err != nil {
return nil, errors.Wrapf(err, "error loading project %q", projPath)
}
tags[apitype.ProjectNameTag] = proj.Name.String()
tags[apitype.ProjectRuntimeTag] = proj.Runtime
if proj.Description != nil {
tags[apitype.ProjectDescriptionTag] = *proj.Description
}
}
return tags, nil
}
// ValidateStackProperties validates the stack name and its tags to confirm they adhear to various
// naming and length restrictions.
func ValidateStackProperties(stack string, tags map[apitype.StackTagName]string) error {
// Validate a name, used for both stack and project names since they use the same regex.
validateName := func(ty, name string) error {
// Must be alphanumeric, dash, or underscore. Must begin an alphanumeric. Must be between 3 and 100 chars.
stackNameRE := regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9-_]{1,98}[a-zA-Z0-9]$")
if !stackNameRE.MatchString(name) {
if len(name) < 3 || len(name) > 100 {
return errors.Errorf("%s name must be between 3 and 100 characters long", ty)
}
first, last := name[0], name[len(name)-1]
if first == '-' || first == '_' || last == '-' || last == '_' {
return errors.Errorf("%s name begin and end with an alphanumeric", ty)
}
return errors.Errorf("%s name can only contain alphanumeric, hyphens, or underscores", ty)
}
return nil
}
if err := validateName("stack", stack); err != nil {
return err
}
// Tags must all be shorter than a given length, and may have other tag-specific restrictions.
// These values are enforced by the Pulumi Service, but we validate them here to have a better
// error experience.
const maxTagName = 40
const maxTagValue = 256
for t, v := range tags {
switch t {
case apitype.ProjectNameTag, apitype.ProjectRuntimeTag:
if err := validateName("project", v); err != nil {
return err
}
}
if len(t) > maxTagName {
return errors.Errorf("stack tag %q is too long (max length 40 characters)", t)
}
if len(v) > maxTagValue {
return errors.Errorf("stack tag %q value is too long (max length 255 characters)", t)
}
}
return nil
}