pulumi/pkg/backend/stack.go
joeduffy 7c7f6d3ed7 Bring back preview, swizzle some flags
This changes the CLI interface in a few ways:

* `pulumi preview` is back!  The alternative of saying
  `pulumi update --preview` just felt awkward, and it's a common
  operation to want to perform.  Let's just make it work.

* There are two flags consistent across all update commands,
  `update`, `refresh`, and `destroy`:

    - `--skip-preview` will skip the preview step.  Note that this
      does *not* skip the prompt to confirm that you'd like to proceed.
      Indeed, it will still prompt, with a little warning text about
      the fact that the preview has been skipped.

    * `--yes` will auto-approve the updates.

This lands us in a simpler and more intuitive spot for common scenarios.
2018-05-06 13:55:39 -07:00

173 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/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.
// Preview changes to this stack.
Preview(proj *workspace.Project, root string, m UpdateMetadata,
opts UpdateOptions, scopes CancellationScopeSource) error
// Update this stack.
Update(proj *workspace.Project, root string, m UpdateMetadata,
opts UpdateOptions, scopes CancellationScopeSource) error
// Refresh this stack's state from the cloud provider.
Refresh(proj *workspace.Project, root string, m UpdateMetadata,
opts UpdateOptions, scopes CancellationScopeSource) error
// Destroy this stack's resources.
Destroy(proj *workspace.Project, root string, m UpdateMetadata,
opts UpdateOptions, 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)
}
// PreviewStack previews changes to this stack.
func PreviewStack(s Stack, proj *workspace.Project, root string, m UpdateMetadata,
opts UpdateOptions, scopes CancellationScopeSource) error {
return s.Backend().Preview(s.Name(), proj, root, m, opts, scopes)
}
// 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 UpdateOptions, scopes CancellationScopeSource) error {
return s.Backend().Update(s.Name(), proj, root, m, opts, scopes)
}
// RefreshStack refresh's the stack's state from the cloud provider.
func RefreshStack(s Stack, proj *workspace.Project, root string, m UpdateMetadata,
opts UpdateOptions, scopes CancellationScopeSource) error {
return s.Backend().Refresh(s.Name(), proj, root, m, opts, scopes)
}
// DestroyStack destroys all of this stack's resources.
func DestroyStack(s Stack, proj *workspace.Project, root string, m UpdateMetadata,
opts UpdateOptions, scopes CancellationScopeSource) error {
return s.Backend().Destroy(s.Name(), proj, root, m, opts, 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
}