This change implements a `pulumi refresh` command. It operates a bit like `pulumi update`, and friends, in that it supports `--preview` and `--diff`, along with the usual flags, and will update your checkpoint. It works through substitution of the deploy.Source abstraction, which generates a sequence of resource registration events. This new deploy.RefreshSource takes in a prior checkpoint and will walk it, refreshing the state via the associated resource providers by invoking Read for each resource encountered, and merging the resulting state with the prior checkpoint, to yield a new resource.Goal state. This state is then fed through the engine in the usual ways with a few minor caveats: namely, although the engine must generate steps for the logical operations (permitting us to get nice summaries, progress, and diffs), it mustn't actually carry them out because the state being imported already reflects reality (a deleted resource has *already* been deleted, so of course the engine need not perform the deletion). The diffing logic also needs to know how to treat the case of refresh slightly differently, because we are going to be diffing outputs and not inputs. Note that support for managed stacks is not yet complete, since that requires updates to the service to support a refresh endpoint. That will be coming soon ...
166 lines
6.2 KiB
Go
166 lines
6.2 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/tokens"
|
|
"github.com/pulumi/pulumi/pkg/workspace"
|
|
)
|
|
|
|
// Stack is a stack associated with a particular backend implementation.
|
|
type Stack interface {
|
|
Name() tokens.QName // this stack's name.
|
|
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, displayOpts DisplayOptions) error
|
|
// Refresh this stack's state from the cloud provider.
|
|
Refresh(proj *workspace.Project, root string,
|
|
m UpdateMetadata, opts engine.UpdateOptions, displayOpts DisplayOptions) error
|
|
// Destroy this stack's resources.
|
|
Destroy(proj *workspace.Project, root string,
|
|
m UpdateMetadata, opts engine.UpdateOptions, displayOpts DisplayOptions) 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, displayOpts DisplayOptions) error {
|
|
return s.Backend().Update(s.Name(), proj, root, m, opts, displayOpts)
|
|
}
|
|
|
|
// 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, displayOpts DisplayOptions) error {
|
|
return s.Backend().Refresh(s.Name(), proj, root, m, opts, displayOpts)
|
|
}
|
|
|
|
// DestroyStack destroys all of this stack's resources.
|
|
func DestroyStack(s Stack, proj *workspace.Project, root string,
|
|
m UpdateMetadata, opts engine.UpdateOptions, displayOpts DisplayOptions) error {
|
|
return s.Backend().Destroy(s.Name(), proj, root, m, opts, displayOpts)
|
|
}
|
|
|
|
// 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
|
|
}
|