pulumi/pkg/backend/stack.go
Matt Ellis 9a8f8881c0 Show manifest information for stacks
This change supports displaying manifest information for a stack and
changes the way we handle Snapshots in our backend.

Previously, every call to GetStack would synthesize a Snapshot by
taking the set of resources returned from the
`/api/stacks/<owner>/<name>` endpoint, combined with an empty
manfiest (since the service was not returning the manifest).

This wasn't great for two reasons:

1. We didn't have manifest information, so we couldn't display any of
   its information (most important the last updated time).

2. This strategy required that the service return all the resources
   for a stack anytime GetStack was called. While the CLI did not
   often need this detailed information the fact that we forced the
   Service to produce it (which in the case of stack managed PPC would
   require the service to talk to yet another service) creates a bunch
   of work that we end up ignoring.

I've refactored the code such that `backend.Stack`'s `Snapshot()` method
now lazily requests the information from the service such that we can
construct a `Snapshot()` on demand and only pay the cost when we
actually need it.

I think making more of this stuff lazy is the long term direction we
want to follow.

Unfortunately, right now, it means in cases where we do need this data
we end up fetching it twice. The service does it once when we call
GetStack and then we do it again when we actually need to get at the
Snapshot.  However, once we land this change, we can update the
service to no longer return resources on the apistack.Stack type. The
CLI no longer needs this property.  We'll likely want to continue in a
direction where `apistack.Stack` can be created quickly by the
service (without expensive database queries or fetching remote
resources) and just add additional endpoints that let us get at the
specific information we want in the specific cases when we want it
instead of forcing us to return a bunch of data that we often ignore.

Fixes pulumi/pulumi-service#371
2018-05-23 16:43:34 -07:00

194 lines
7.9 KiB
Go

// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package backend
import (
"context"
"path/filepath"
"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/util/gitutil"
"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(ctx context.Context) (*deploy.Snapshot, error) // the latest deployment snapshot.
Backend() Backend // the backend this stack belongs to.
// Preview changes to this stack.
Preview(ctx context.Context, proj *workspace.Project, root string, m UpdateMetadata, opts UpdateOptions,
scopes CancellationScopeSource) (engine.ResourceChanges, error)
// Update this stack.
Update(ctx context.Context, proj *workspace.Project, root string, m UpdateMetadata, opts UpdateOptions,
scopes CancellationScopeSource) (engine.ResourceChanges, error)
// Refresh this stack's state from the cloud provider.
Refresh(ctx context.Context, proj *workspace.Project, root string, m UpdateMetadata, opts UpdateOptions,
scopes CancellationScopeSource) (engine.ResourceChanges, error)
// Destroy this stack's resources.
Destroy(ctx context.Context, proj *workspace.Project, root string, m UpdateMetadata, opts UpdateOptions,
scopes CancellationScopeSource) (engine.ResourceChanges, error)
// remove this stack.
Remove(ctx context.Context, force bool) (bool, error)
// list log entries for this stack.
GetLogs(ctx context.Context, query operations.LogQuery) ([]operations.LogEntry, error)
// export this stack's deployment.
ExportDeployment(ctx context.Context) (*apitype.UntypedDeployment, error)
// import the given deployment into this stack.
ImportDeployment(ctx context.Context, deployment *apitype.UntypedDeployment) error
}
// RemoveStack returns the stack, or returns an error if it cannot.
func RemoveStack(ctx context.Context, s Stack, force bool) (bool, error) {
return s.Backend().RemoveStack(ctx, s.Name(), force)
}
// PreviewStack previews changes to this stack.
func PreviewStack(ctx context.Context, s Stack, proj *workspace.Project, root string, m UpdateMetadata,
opts UpdateOptions, scopes CancellationScopeSource) (engine.ResourceChanges, error) {
return s.Backend().Preview(ctx, s.Name(), proj, root, m, opts, scopes)
}
// UpdateStack updates the target stack with the current workspace's contents (config and code).
func UpdateStack(ctx context.Context, s Stack, proj *workspace.Project, root string, m UpdateMetadata,
opts UpdateOptions, scopes CancellationScopeSource) (engine.ResourceChanges, error) {
return s.Backend().Update(ctx, s.Name(), proj, root, m, opts, scopes)
}
// RefreshStack refresh's the stack's state from the cloud provider.
func RefreshStack(ctx context.Context, s Stack, proj *workspace.Project, root string, m UpdateMetadata,
opts UpdateOptions, scopes CancellationScopeSource) (engine.ResourceChanges, error) {
return s.Backend().Refresh(ctx, s.Name(), proj, root, m, opts, scopes)
}
// DestroyStack destroys all of this stack's resources.
func DestroyStack(ctx context.Context, s Stack, proj *workspace.Project, root string, m UpdateMetadata,
opts UpdateOptions, scopes CancellationScopeSource) (engine.ResourceChanges, error) {
return s.Backend().Destroy(ctx, 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())
}
// GetLatestConfiguration returns the configuration for the most recent deployment of the stack.
func GetLatestConfiguration(ctx context.Context, s Stack) (config.Map, error) {
return s.Backend().GetLatestConfiguration(ctx, s.Name())
}
// GetStackLogs fetches a list of log entries for the current stack in the current backend.
func GetStackLogs(ctx context.Context, s Stack, query operations.LogQuery) ([]operations.LogEntry, error) {
return s.Backend().GetLogs(ctx, s.Name(), query)
}
// ExportStackDeployment exports the given stack's deployment as an opaque JSON message.
func ExportStackDeployment(ctx context.Context, s Stack) (*apitype.UntypedDeployment, error) {
return s.Backend().ExportDeployment(ctx, s.Name())
}
// ImportStackDeployment imports the given deployment into the indicated stack.
func ImportStackDeployment(ctx context.Context, s Stack, deployment *apitype.UntypedDeployment) error {
return s.Backend().ImportDeployment(ctx, 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 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
}
if owner, repo, err := gitutil.GetGitHubProjectForOrigin(filepath.Dir(projPath)); err == nil {
tags[apitype.GitHubOwnerNameTag] = owner
tags[apitype.GitHubRepositoryNameTag] = repo
}
}
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
}