mirror of https://github.com/pulumi/pulumi.git
828 lines
29 KiB
Go
828 lines
29 KiB
Go
// Copyright 2016-2020, 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 cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
|
|
opentracing "github.com/opentracing/opentracing-go"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/pulumi/pulumi/pkg/v2/backend"
|
|
"github.com/pulumi/pulumi/pkg/v2/backend/display"
|
|
"github.com/pulumi/pulumi/pkg/v2/engine"
|
|
"github.com/pulumi/pulumi/pkg/v2/operations"
|
|
"github.com/pulumi/pulumi/pkg/v2/resource/deploy"
|
|
"github.com/pulumi/pulumi/pkg/v2/secrets"
|
|
"github.com/pulumi/pulumi/pkg/v2/util/cancel"
|
|
"github.com/pulumi/pulumi/sdk/v2/go/common/apitype"
|
|
"github.com/pulumi/pulumi/sdk/v2/go/common/diag"
|
|
"github.com/pulumi/pulumi/sdk/v2/go/common/diag/colors"
|
|
"github.com/pulumi/pulumi/sdk/v2/go/common/resource/config"
|
|
"github.com/pulumi/pulumi/sdk/v2/go/common/util/contract"
|
|
"github.com/pulumi/pulumi/sdk/v2/go/common/util/result"
|
|
"github.com/pulumi/pulumi/sdk/v2/go/common/workspace"
|
|
)
|
|
|
|
// CancellationScope provides a scoped source of cancellation and termination requests.
|
|
type CancellationScope interface {
|
|
// Context returns the cancellation context used to observe cancellation and termination requests for this scope.
|
|
Context() *cancel.Context
|
|
// Close closes the cancellation scope.
|
|
Close()
|
|
}
|
|
|
|
// CancellationScopeSource provides a source for cancellation scopes.
|
|
type CancellationScopeSource interface {
|
|
// NewScope creates a new cancellation scope.
|
|
NewScope(events chan<- engine.Event, isPreview bool) CancellationScope
|
|
}
|
|
|
|
// UpdateMetadata describes optional metadata about an update.
|
|
type UpdateMetadata struct {
|
|
// Message is an optional message associated with the update.
|
|
Message string `json:"message"`
|
|
// Environment contains optional data from the deploying environment. e.g. the current
|
|
// source code control commit information.
|
|
Environment map[string]string `json:"environment"`
|
|
}
|
|
|
|
// UpdateOperation is a complete stack update operation (preview, update, import, refresh, or destroy).
|
|
type UpdateOperation struct {
|
|
Proj *workspace.Project
|
|
Root string
|
|
Imports []deploy.Import
|
|
M *UpdateMetadata
|
|
Opts UpdateOptions
|
|
SecretsManager secrets.Manager
|
|
StackConfiguration StackConfiguration
|
|
Scopes CancellationScopeSource
|
|
}
|
|
|
|
// QueryOperation configures a query operation.
|
|
type QueryOperation struct {
|
|
Proj *workspace.Project
|
|
Root string
|
|
Opts UpdateOptions
|
|
SecretsManager secrets.Manager
|
|
StackConfiguration StackConfiguration
|
|
Scopes CancellationScopeSource
|
|
}
|
|
|
|
// StackConfiguration holds the configuration for a stack and it's associated decrypter.
|
|
type StackConfiguration struct {
|
|
Config config.Map
|
|
Decrypter config.Decrypter
|
|
}
|
|
|
|
// UpdateOptions is the full set of update options, including backend and engine options.
|
|
type UpdateOptions struct {
|
|
// Engine contains all of the engine-specific options.
|
|
Engine engine.UpdateOptions
|
|
// Display contains all of the backend display options.
|
|
Display display.Options
|
|
|
|
// AutoApprove, when true, will automatically approve previews.
|
|
AutoApprove bool
|
|
// SkipPreview, when true, causes the preview step to be skipped.
|
|
SkipPreview bool
|
|
}
|
|
|
|
// QueryOptions configures a query to operate against a backend and the engine.
|
|
type QueryOptions struct {
|
|
// Engine contains all of the engine-specific options.
|
|
Engine engine.UpdateOptions
|
|
// Display contains all of the backend display options.
|
|
Display display.Options
|
|
}
|
|
|
|
type Backend struct {
|
|
d diag.Sink
|
|
client backend.Client
|
|
currentProject *workspace.Project
|
|
}
|
|
|
|
// NewBackend creates a new CLI backend using the given client.
|
|
func NewBackend(d diag.Sink, client backend.Client) (*Backend, error) {
|
|
// When stringifying backend references, we take the current project (if present) into account.
|
|
currentProject, err := workspace.DetectProject()
|
|
if err != nil {
|
|
currentProject = nil
|
|
}
|
|
|
|
return &Backend{
|
|
d: d,
|
|
client: client,
|
|
currentProject: currentProject,
|
|
}, nil
|
|
}
|
|
|
|
// Name returns a friendly name for this backend.
|
|
func (b *Backend) Name() string {
|
|
return b.client.Name()
|
|
}
|
|
|
|
// URL returns a URL at which information about this backend may be seen.
|
|
func (b *Backend) URL() string {
|
|
return b.client.URL()
|
|
}
|
|
|
|
// Client returns the client instance that implements the lower-level operations required by the backend.
|
|
func (b *Backend) Client() backend.Client {
|
|
return b.client
|
|
}
|
|
|
|
// Returns the identity of the current user for the backend.
|
|
func (b *Backend) CurrentUser() (string, error) {
|
|
return b.client.User(context.Background())
|
|
}
|
|
|
|
// CurrentStack reads the current stack and returns an instance connected to its backend provider.
|
|
func (b *Backend) CurrentStack(ctx context.Context) (*Stack, error) {
|
|
w, err := workspace.New()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stackName := w.Settings().Stack
|
|
if stackName == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
ref, err := b.ParseStackIdentifier(stackName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return b.GetStack(ctx, ref)
|
|
}
|
|
|
|
// SetCurrentStack changes the current stack to the given stack identifier.
|
|
func (b *Backend) SetCurrentStack(id backend.StackIdentifier) error {
|
|
// Switch the current workspace to that stack.
|
|
w, err := workspace.New()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
w.Settings().Stack = id.String()
|
|
return w.Save()
|
|
}
|
|
|
|
func (b *Backend) policyClient() (backend.PolicyClient, error) {
|
|
policyClient, ok := b.client.(backend.PolicyClient)
|
|
if !ok {
|
|
return nil, fmt.Errorf("the selected backend does not support policy packs")
|
|
}
|
|
return policyClient, nil
|
|
}
|
|
|
|
// ParsePolicyPackIdentifier parses a policy pack identifier.
|
|
func (b *Backend) ParsePolicyPackIdentifier(s string) (backend.PolicyPackIdentifier, error) {
|
|
currentUser, err := b.CurrentUser()
|
|
if err != nil {
|
|
currentUser = ""
|
|
}
|
|
return backend.ParsePolicyPackIdentifier(s, currentUser, b.client.URL())
|
|
}
|
|
|
|
// GetPolicyPack returns a PolicyPack object tied to this backend, or nil if it cannot be found.
|
|
func (b *Backend) GetPolicyPack(ctx context.Context, policyPack string) (*PolicyPack, error) {
|
|
policyClient, err := b.policyClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
id, err := b.ParsePolicyPackIdentifier(policyPack)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &PolicyPack{
|
|
id: id,
|
|
b: b,
|
|
cl: policyClient,
|
|
}, nil
|
|
}
|
|
|
|
// ListPolicyGroups returns all Policy Groups for an organization in this backend or an error if it cannot be found.
|
|
func (b *Backend) ListPolicyGroups(ctx context.Context, orgName string) (apitype.ListPolicyGroupsResponse, error) {
|
|
policyClient, err := b.policyClient()
|
|
if err != nil {
|
|
return apitype.ListPolicyGroupsResponse{}, err
|
|
}
|
|
return policyClient.ListPolicyGroups(ctx, orgName)
|
|
}
|
|
|
|
// ListPolicyPacks returns all Policy Packs for an organization in this backend, or an error if it cannot be found.
|
|
func (b *Backend) ListPolicyPacks(ctx context.Context, orgName string) (apitype.ListPolicyPacksResponse, error) {
|
|
policyClient, err := b.policyClient()
|
|
if err != nil {
|
|
return apitype.ListPolicyPacksResponse{}, err
|
|
}
|
|
return policyClient.ListPolicyPacks(ctx, orgName)
|
|
}
|
|
|
|
// SupportsOrganizations tells whether a user can belong to multiple organizations in this backend.
|
|
func (b *Backend) SupportsOrganizations() bool {
|
|
return true
|
|
}
|
|
|
|
// ParseStackIdentifier parses a stack identifier in the context of the current user and project.
|
|
func (b *Backend) ParseStackIdentifier(s string) (backend.StackIdentifier, error) {
|
|
return backend.ParseStackIdentifierWithClient(context.Background(), s, b.client)
|
|
}
|
|
|
|
// ValidateStackName verifies that the string is a legal identifier for a (potentially qualified) stack.
|
|
func (b *Backend) ValidateStackName(s string) error {
|
|
id, err := backend.ParseStackIdentifier(s, "", "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// The Pulumi Service enforces specific naming restrictions for organizations,
|
|
// projects, and stacks. Though ignore any values that need to be inferred later.
|
|
if id.Owner != "" {
|
|
if err := validateOwnerName(id.Owner); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if id.Project != "" {
|
|
if err := validateProjectName(id.Project); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return validateStackName(id.Stack)
|
|
}
|
|
|
|
// StackFriendlyName returns the short form for a stack identifier using the current project and user context.
|
|
func (b *Backend) StackFriendlyName(id backend.StackIdentifier) string {
|
|
currentUser, err := b.CurrentUser()
|
|
if err != nil {
|
|
currentUser = ""
|
|
}
|
|
return id.FriendlyName(currentUser, string(b.currentProject.Name))
|
|
}
|
|
|
|
// StackConsoleURL returns the Pulumi Console URL for the given stack identifier, if any. Callers should consider an
|
|
// empty URL and a nil error to indicate that the client does not support the Pulum Console.
|
|
func (b *Backend) StackConsoleURL(id backend.StackIdentifier) (string, error) {
|
|
return b.client.StackConsoleURL(id)
|
|
}
|
|
|
|
// Name validation rules.
|
|
var (
|
|
stackOwnerRegexp = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9-_]{1,38}[a-zA-Z0-9]$")
|
|
stackNameAndProjectRegexp = regexp.MustCompile("^[A-Za-z0-9_.-]{1,100}$")
|
|
)
|
|
|
|
// validateOwnerName checks if a stack owner name is valid. An "owner" is simply the namespace
|
|
// a stack may exist within, which for the Pulumi Service is the user account or organization.
|
|
func validateOwnerName(s string) error {
|
|
if !stackOwnerRegexp.MatchString(s) {
|
|
return errors.New("invalid stack owner")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateStackName checks if a stack name is valid, returning a user-suitable error if needed.
|
|
func validateStackName(s string) error {
|
|
if len(s) > 100 {
|
|
return errors.New("stack names must be less than 100 characters")
|
|
}
|
|
if !stackNameAndProjectRegexp.MatchString(s) {
|
|
return errors.New("stack names may only contain alphanumeric, hyphens, underscores, and periods")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateProjectName checks if a project name is valid, returning a user-suitable error if needed.
|
|
//
|
|
// NOTE: Be careful when requiring a project name be valid. The Pulumi.yaml file may contain
|
|
// an invalid project name like "r@bid^W0MBAT!!", but we try to err on the side of flexibility by
|
|
// implicitly "cleaning" the project name before we send it to the Pulumi Service. So when we go
|
|
// to make HTTP requests, we use a more palitable name like "r_bid_W0MBAT__".
|
|
//
|
|
// The projects canonical name will be the sanitized "r_bid_W0MBAT__" form, but we do not require the
|
|
// Pulumi.yaml file be updated.
|
|
//
|
|
// So we should only call validateProject name when creating _new_ stacks or creating _new_ projects.
|
|
// We should not require that project names be valid when reading what is in the current workspace.
|
|
func validateProjectName(s string) error {
|
|
if len(s) > 100 {
|
|
return errors.New("project names must be less than 100 characters")
|
|
}
|
|
if !stackNameAndProjectRegexp.MatchString(s) {
|
|
return errors.New("project names may only contain alphanumeric, hyphens, underscores, and periods")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Logout logs out of the backend.
|
|
func (b *Backend) Logout() error {
|
|
return workspace.DeleteAccount(b.URL())
|
|
}
|
|
|
|
// DoesProjectExist returns true if a project with the given name exists in this backend, or false otherwise.
|
|
func (b *Backend) DoesProjectExist(ctx context.Context, projectName string) (bool, error) {
|
|
owner, err := b.client.User(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return b.client.DoesProjectExist(ctx, owner, projectName)
|
|
}
|
|
|
|
// GetStack returns a stack object tied to this backend with the given identifier, or nil if it cannot be found.
|
|
func (b *Backend) GetStack(ctx context.Context, stackID backend.StackIdentifier) (*Stack, error) {
|
|
stack, err := b.client.GetStack(ctx, stackID)
|
|
if err != nil {
|
|
if err == backend.ErrNotFound {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
return newStack(stack, b), nil
|
|
}
|
|
|
|
// CreateStack creates a new stack with the given name and options that are specific to the backend provider.
|
|
func (b *Backend) CreateStack(ctx context.Context, stackID backend.StackIdentifier) (*Stack, error) {
|
|
tags, err := GetEnvironmentTagsForCurrentStack()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error determining initial tags")
|
|
}
|
|
|
|
// Confirm the stack identity matches the environment. e.g. stack init foo/bar/baz shouldn't work
|
|
// if the project name in Pulumi.yaml is anything other than "bar".
|
|
projNameTag, ok := tags[apitype.ProjectNameTag]
|
|
if ok && stackID.Project != projNameTag {
|
|
return nil, errors.Errorf("provided project name %q doesn't match Pulumi.yaml", stackID.Project)
|
|
}
|
|
|
|
apistack, err := b.client.CreateStack(ctx, stackID, tags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stack := newStack(apistack, b)
|
|
fmt.Printf("Created stack '%s'\n", stack.id)
|
|
|
|
return stack, nil
|
|
}
|
|
|
|
// ListStacks returns a list of stack summaries for all known stacks in the target backend.
|
|
func (b *Backend) ListStacks(ctx context.Context, filter backend.ListStacksFilter) ([]apitype.StackSummary, error) {
|
|
// Sanitize the project name as needed, so when communicating with the Pulumi Service we
|
|
// always use the name the service expects. (So that a similar, but not technically valid
|
|
// name may be put in Pulumi.yaml without causing problems.)
|
|
if filter.Project != nil {
|
|
cleanedProj := cleanProjectName(*filter.Project)
|
|
filter.Project = &cleanedProj
|
|
}
|
|
|
|
return b.client.ListStacks(ctx, filter)
|
|
}
|
|
|
|
// RemoveStack removes a stack with the given name. If force is true, the stack will be removed even if it still
|
|
// contains resources. Otherwise, if the stack contains resources, a non-nil error is returned, and the first boolean
|
|
// return value will be set to true.
|
|
func (b *Backend) RemoveStack(ctx context.Context, stack *Stack, force bool) (bool, error) {
|
|
return b.client.DeleteStack(ctx, stack.id, force)
|
|
}
|
|
|
|
// RenameStack renames the given stack to a new name, and then returns an updated stack reference that can be used to
|
|
// refer to the newly renamed stack.
|
|
func (b *Backend) RenameStack(ctx context.Context, stack *Stack, newID string) (backend.StackIdentifier, error) {
|
|
parsedID, err := b.ParseStackIdentifier(newID)
|
|
if err != nil {
|
|
return backend.StackIdentifier{}, err
|
|
}
|
|
|
|
if stack.id.Owner != parsedID.Owner {
|
|
errMsg := fmt.Sprintf(
|
|
"New stack owner, %s, does not match existing owner, %s.\n\n",
|
|
stack.id.Owner, parsedID.Owner)
|
|
|
|
parsedID, err = backend.ParseStackIdentifier(newID, "", "")
|
|
if err == nil && parsedID.Owner == "" {
|
|
errMsg += fmt.Sprintf(
|
|
" Did you forget to include the owner name? If yes, rerun the command as follows:\n\n"+
|
|
" $ pulumi stack rename %s/%s\n\n",
|
|
stack.id.Owner, parsedID.Stack)
|
|
}
|
|
|
|
if consoleURL, err := stack.ConsoleURL(); err != nil && consoleURL != "" {
|
|
errMsg += " You cannot transfer stack ownership via a rename. If you wish to transfer ownership\n" +
|
|
" of a stack to another organization, you can do so in the Pulumi Console by going to the\n" +
|
|
" \"Settings\" page of the stack and then clicking the \"Transfer Stack\" button:\n" +
|
|
"\n" +
|
|
" " + consoleURL + "/settings/options"
|
|
}
|
|
|
|
return backend.StackIdentifier{}, errors.New(errMsg)
|
|
}
|
|
|
|
if err = b.client.RenameStack(ctx, stack.id, parsedID); err != nil {
|
|
return backend.StackIdentifier{}, err
|
|
}
|
|
return parsedID, nil
|
|
}
|
|
|
|
// Preview shows what would be updated given the current workspace's contents.
|
|
func (b *Backend) Preview(ctx context.Context, stack *Stack,
|
|
op UpdateOperation) (engine.ResourceChanges, result.Result) {
|
|
|
|
// We can skip PreviewtThenPromptThenExecute, and just go straight to Execute.
|
|
opts := applierOptions{
|
|
DryRun: true,
|
|
ShowLink: true,
|
|
}
|
|
return b.apply(
|
|
ctx, apitype.PreviewUpdate, stack, op, opts, nil /*events*/)
|
|
}
|
|
|
|
// Update updates the target stack with the current workspace's contents (config and code).
|
|
func (b *Backend) Update(ctx context.Context, stack *Stack,
|
|
op UpdateOperation) (engine.ResourceChanges, result.Result) {
|
|
return previewThenPromptThenExecute(ctx, apitype.UpdateUpdate, stack, op, b.apply)
|
|
}
|
|
|
|
// Import imports resources into a stack.
|
|
func (b *Backend) Import(ctx context.Context, stack *Stack,
|
|
op UpdateOperation, imports []deploy.Import) (engine.ResourceChanges, result.Result) {
|
|
op.Imports = imports
|
|
return previewThenPromptThenExecute(ctx, apitype.ResourceImportUpdate, stack, op, b.apply)
|
|
}
|
|
|
|
// Refresh refreshes the stack's state from the cloud provider.
|
|
func (b *Backend) Refresh(ctx context.Context, stack *Stack,
|
|
op UpdateOperation) (engine.ResourceChanges, result.Result) {
|
|
return previewThenPromptThenExecute(ctx, apitype.RefreshUpdate, stack, op, b.apply)
|
|
}
|
|
|
|
// Destroy destroys all of this stack's resources.
|
|
func (b *Backend) Destroy(ctx context.Context, stack *Stack,
|
|
op UpdateOperation) (engine.ResourceChanges, result.Result) {
|
|
return previewThenPromptThenExecute(ctx, apitype.DestroyUpdate, stack, op, b.apply)
|
|
}
|
|
|
|
// Watch watches the project's working directory for changes and automatically updates the active stack.
|
|
func (b *Backend) Watch(ctx context.Context, stack *Stack,
|
|
op UpdateOperation) result.Result {
|
|
return Watch(ctx, b, stack, op, b.apply)
|
|
}
|
|
|
|
// Query against the resource outputs in a stack's state checkpoint.
|
|
func (b *Backend) Query(ctx context.Context, op QueryOperation) result.Result {
|
|
return b.query(ctx, op, nil /*events*/)
|
|
}
|
|
|
|
func (b *Backend) startUpdate(
|
|
ctx context.Context, action apitype.UpdateKind, stack *Stack,
|
|
op *UpdateOperation, dryRun bool) (backend.Update, error) {
|
|
|
|
metadata := apitype.UpdateMetadata{
|
|
Message: op.M.Message,
|
|
Environment: op.M.Environment,
|
|
}
|
|
|
|
// Start the update. We use this opportunity to pass new tags to the service, to pick up any
|
|
// metadata changes.
|
|
tags, err := GetMergedStackTags(ctx, stack)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting stack tags")
|
|
}
|
|
|
|
return b.client.StartUpdate(ctx, action, stack.id, op.Proj, op.StackConfiguration.Config, metadata, op.Opts.Engine,
|
|
tags, dryRun)
|
|
}
|
|
|
|
// apply actually performs the provided type of update on a stack hosted in the Pulumi Cloud.
|
|
func (b *Backend) apply(
|
|
ctx context.Context, kind apitype.UpdateKind, stack *Stack,
|
|
op UpdateOperation, opts applierOptions,
|
|
events chan<- engine.Event) (engine.ResourceChanges, result.Result) {
|
|
|
|
actionLabel := actionLabel(kind, opts.DryRun)
|
|
|
|
if !(op.Opts.Display.JSONDisplay || op.Opts.Display.Type == display.DisplayWatch) {
|
|
// Print a banner so it's clear this is going to the client.
|
|
fmt.Printf(op.Opts.Display.Color.Colorize(
|
|
colors.SpecHeadline+"%s (%s):"+colors.Reset+"\n"), actionLabel, stack.FriendlyName())
|
|
}
|
|
|
|
// Create an update object to persist results.
|
|
update, err := b.startUpdate(ctx, kind, stack, &op, opts.DryRun)
|
|
if err != nil {
|
|
return nil, result.FromError(err)
|
|
}
|
|
|
|
// Set up required policies.
|
|
for _, policy := range update.RequiredPolicies() {
|
|
op.Opts.Engine.RequiredPolicies = append(op.Opts.Engine.RequiredPolicies, &requiredPolicy{
|
|
meta: policy,
|
|
orgName: stack.orgName,
|
|
client: stack.b.client,
|
|
})
|
|
}
|
|
|
|
if !op.Opts.Display.SuppressPermaLink && opts.ShowLink && !op.Opts.Display.JSONDisplay {
|
|
// Print a URL at the beginning of the update pointing to the Pulumi Service.
|
|
if url := update.ProgressURL(); url != "" {
|
|
fmt.Printf(op.Opts.Display.Color.Colorize(
|
|
colors.SpecHeadline+"View Live: "+
|
|
colors.Underline+colors.BrightBlue+"%s"+colors.Reset+"\n\n"), url)
|
|
}
|
|
|
|
}
|
|
|
|
// Run the update.
|
|
changes, res := b.runEngineAction(ctx, kind, stack.id, op, update, events, opts.DryRun)
|
|
|
|
// Make sure to print a link to the stack's checkpoint before exiting.
|
|
if !op.Opts.Display.SuppressPermaLink && opts.ShowLink && !op.Opts.Display.JSONDisplay {
|
|
if url := update.PermalinkURL(); url != "" && url != update.ProgressURL() {
|
|
fmt.Printf(op.Opts.Display.Color.Colorize(
|
|
colors.SpecHeadline+"Permalink: "+
|
|
colors.Underline+colors.BrightBlue+"%s"+colors.Reset+"\n"), url)
|
|
}
|
|
}
|
|
|
|
return changes, res
|
|
}
|
|
|
|
// query executes a query program against the resource outputs of a stack hosted in the Pulumi
|
|
// Cloud.
|
|
func (b *Backend) query(ctx context.Context, op QueryOperation,
|
|
callerEventsOpt chan<- engine.Event) result.Result {
|
|
|
|
return RunQuery(ctx, b, op, callerEventsOpt, b.newQuery)
|
|
}
|
|
|
|
func (b *Backend) runEngineAction(
|
|
ctx context.Context, kind apitype.UpdateKind, stackID backend.StackIdentifier,
|
|
op UpdateOperation, update backend.Update, callerEventsOpt chan<- engine.Event,
|
|
dryRun bool) (engine.ResourceChanges, result.Result) {
|
|
|
|
u, err := b.newUpdate(ctx, stackID, op, update)
|
|
if err != nil {
|
|
return nil, result.FromError(err)
|
|
}
|
|
|
|
// displayEvents renders the event to the console and Pulumi service. The processor for the
|
|
// will signal all events have been proceed when a value is written to the displayDone channel.
|
|
displayEvents := make(chan engine.Event)
|
|
displayDone := make(chan bool)
|
|
go u.RecordAndDisplayEvents(
|
|
actionLabel(kind, dryRun), kind, stackID, op,
|
|
displayEvents, displayDone, op.Opts.Display, dryRun)
|
|
|
|
// The engineEvents channel receives all events from the engine, which we then forward onto other
|
|
// channels for actual processing. (displayEvents and callerEventsOpt.)
|
|
engineEvents := make(chan engine.Event)
|
|
eventsDone := make(chan bool)
|
|
go func() {
|
|
for e := range engineEvents {
|
|
displayEvents <- e
|
|
if callerEventsOpt != nil {
|
|
callerEventsOpt <- e
|
|
}
|
|
}
|
|
|
|
close(eventsDone)
|
|
}()
|
|
|
|
// The backend.SnapshotManager and backend.SnapshotPersister will keep track of any changes to
|
|
// the Snapshot (checkpoint file) in the HTTP backend. We will reuse the snapshot's secrets manager when possible
|
|
// to ensure that secrets are not re-encrypted on each update.
|
|
sm := op.SecretsManager
|
|
if secrets.AreCompatible(sm, u.GetTarget().Snapshot.SecretsManager) {
|
|
sm = u.GetTarget().Snapshot.SecretsManager
|
|
}
|
|
snapshotManager := backend.NewSnapshotManager(ctx, u.update, sm, u.GetTarget().Snapshot)
|
|
|
|
// Depending on the action, kick off the relevant engine activity. Note that we don't immediately check and
|
|
// return error conditions, because we will do so below after waiting for the display channels to close.
|
|
cancellationScope := op.Scopes.NewScope(engineEvents, dryRun)
|
|
engineCtx := &engine.Context{
|
|
Cancel: cancellationScope.Context(),
|
|
Events: engineEvents,
|
|
SnapshotManager: snapshotManager,
|
|
BackendClient: backend.NewBackendClient(b.client),
|
|
}
|
|
if parentSpan := opentracing.SpanFromContext(ctx); parentSpan != nil {
|
|
engineCtx.ParentSpan = parentSpan.Context()
|
|
}
|
|
|
|
var changes engine.ResourceChanges
|
|
var res result.Result
|
|
switch kind {
|
|
case apitype.PreviewUpdate:
|
|
changes, res = engine.Update(u, engineCtx, op.Opts.Engine, true)
|
|
case apitype.UpdateUpdate:
|
|
changes, res = engine.Update(u, engineCtx, op.Opts.Engine, dryRun)
|
|
case apitype.RefreshUpdate:
|
|
changes, res = engine.Refresh(u, engineCtx, op.Opts.Engine, dryRun)
|
|
case apitype.DestroyUpdate:
|
|
changes, res = engine.Destroy(u, engineCtx, op.Opts.Engine, dryRun)
|
|
default:
|
|
contract.Failf("Unrecognized update kind: %s", kind)
|
|
}
|
|
|
|
// Wait for dependent channels to finish processing engineEvents before closing.
|
|
<-displayDone
|
|
cancellationScope.Close() // Don't take any cancellations anymore, we're shutting down.
|
|
close(engineEvents)
|
|
contract.IgnoreClose(snapshotManager)
|
|
|
|
// Make sure that the goroutine writing to displayEvents and callerEventsOpt
|
|
// has exited before proceeding
|
|
<-eventsDone
|
|
close(displayEvents)
|
|
|
|
// Mark the update as complete.
|
|
status := apitype.UpdateStatusSucceeded
|
|
if res != nil {
|
|
status = apitype.UpdateStatusFailed
|
|
}
|
|
completeErr := u.Complete(status)
|
|
if completeErr != nil {
|
|
res = result.Merge(res, result.FromError(errors.Wrap(completeErr, "failed to complete update")))
|
|
}
|
|
|
|
return changes, res
|
|
}
|
|
|
|
// CancelCurrentUpdate cancels the currently running update for the given stack, if any.
|
|
func (b *Backend) CancelCurrentUpdate(ctx context.Context, stackID backend.StackIdentifier) error {
|
|
stack, err := b.client.GetStack(ctx, stackID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if stack.ActiveUpdate == "" {
|
|
return errors.Errorf("stack %v has never been updated", stackID)
|
|
}
|
|
|
|
return b.client.CancelCurrentUpdate(ctx, stackID)
|
|
}
|
|
|
|
// GetHistory returns all updates for the stack. The returned UpdateInfo slice will be in descending order (newest
|
|
// first).
|
|
func (b *Backend) GetHistory(ctx context.Context, stackID backend.StackIdentifier) ([]backend.UpdateInfo, error) {
|
|
updates, err := b.client.GetStackHistory(ctx, stackID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert apitype.UpdateInfo objects to the backend type.
|
|
var beUpdates []backend.UpdateInfo
|
|
for _, update := range updates {
|
|
// Convert types from the apitype package into their internal counterparts.
|
|
cfg, err := convertConfig(update.Config)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "converting configuration")
|
|
}
|
|
|
|
beUpdates = append(beUpdates, backend.UpdateInfo{
|
|
Kind: update.Kind,
|
|
Message: update.Message,
|
|
Environment: update.Environment,
|
|
Config: cfg,
|
|
Result: backend.UpdateResult(update.Result),
|
|
StartTime: update.StartTime,
|
|
EndTime: update.EndTime,
|
|
ResourceChanges: convertResourceChanges(update.ResourceChanges),
|
|
})
|
|
}
|
|
|
|
return beUpdates, nil
|
|
}
|
|
|
|
// Get the configuration from the most recent deployment of the stack.
|
|
func (b *Backend) GetLatestConfiguration(ctx context.Context, stack *Stack) (config.Map, error) {
|
|
return b.client.GetLatestStackConfig(ctx, stack.id)
|
|
}
|
|
|
|
// convertResourceChanges converts the apitype version of engine.ResourceChanges into the internal version.
|
|
func convertResourceChanges(changes map[apitype.OpType]int) engine.ResourceChanges {
|
|
b := make(engine.ResourceChanges)
|
|
for k, v := range changes {
|
|
b[deploy.StepOp(k)] = v
|
|
}
|
|
return b
|
|
}
|
|
|
|
// convertResourceChanges converts the apitype version of config.Map into the internal version.
|
|
func convertConfig(apiConfig map[string]apitype.ConfigValue) (config.Map, error) {
|
|
c := make(config.Map)
|
|
for rawK, rawV := range apiConfig {
|
|
k, err := config.ParseKey(rawK)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if rawV.Object {
|
|
if rawV.Secret {
|
|
c[k] = config.NewSecureObjectValue(rawV.String)
|
|
} else {
|
|
c[k] = config.NewObjectValue(rawV.String)
|
|
}
|
|
} else {
|
|
if rawV.Secret {
|
|
c[k] = config.NewSecureValue(rawV.String)
|
|
} else {
|
|
c[k] = config.NewValue(rawV.String)
|
|
}
|
|
}
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// GetLogs fetches a list of log entries for the given stack, with optional filtering/querying.
|
|
func (b *Backend) GetLogs(ctx context.Context, stack *Stack, cfg StackConfiguration,
|
|
logQuery operations.LogQuery) ([]operations.LogEntry, error) {
|
|
|
|
target, targetErr := b.getTarget(ctx, stack.id, cfg.Config, cfg.Decrypter)
|
|
if targetErr != nil {
|
|
return nil, targetErr
|
|
}
|
|
return backend.GetLogsForTarget(target, logQuery)
|
|
}
|
|
|
|
// ExportDeployment exports the deployment for the given stack as an opaque JSON message.
|
|
func (b *Backend) ExportDeployment(ctx context.Context,
|
|
stack *Stack) (*apitype.UntypedDeployment, error) {
|
|
return b.exportDeployment(ctx, stack.id, nil /* latest */)
|
|
}
|
|
|
|
// ExportDeploymentForVersion exports a specific deployment from the history of a stack. The meaning of version is
|
|
// client-specific. For the Pulumi client, it is a simple numeric version (the first update being version "1", the
|
|
// second "2", and so on), though this might change in the future to use some other type of identifier or commitish.
|
|
func (b *Backend) ExportDeploymentForVersion(
|
|
ctx context.Context, stack *Stack, version string) (*apitype.UntypedDeployment, error) {
|
|
// The Pulumi Console defines versions as a positive integer. Parse the provided version string and
|
|
// ensure it is valid.
|
|
//
|
|
// The first stack update version is 1, and monotonically increasing from there.
|
|
versionNumber, err := strconv.Atoi(version)
|
|
if err != nil || versionNumber <= 0 {
|
|
return nil, errors.Errorf("%q is not a valid stack version. It should be a positive integer.", version)
|
|
}
|
|
|
|
return b.exportDeployment(ctx, stack.id, &versionNumber)
|
|
}
|
|
|
|
// exportDeployment exports the checkpoint file for a stack, optionally getting a previous version.
|
|
func (b *Backend) exportDeployment(ctx context.Context, stackID backend.StackIdentifier,
|
|
version *int) (*apitype.UntypedDeployment, error) {
|
|
|
|
deployment, err := b.client.ExportStackDeployment(ctx, stackID, version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &deployment, nil
|
|
}
|
|
|
|
// ImportDeployment imports the given deployment into the indicated stack.
|
|
func (b *Backend) ImportDeployment(ctx context.Context, stack *Stack, deployment *apitype.UntypedDeployment) error {
|
|
return b.client.ImportStackDeployment(ctx, stack.id, deployment)
|
|
}
|
|
|
|
// GetStackTags fetches the stack's existing tags.
|
|
func (b *Backend) GetStackTags(ctx context.Context, stack *Stack) (map[apitype.StackTagName]string, error) {
|
|
return stack.tags, nil
|
|
}
|
|
|
|
// UpdateStackTags updates the stacks's tags, replacing all existing tags.
|
|
func (b *Backend) UpdateStackTags(ctx context.Context, stack *Stack, tags map[apitype.StackTagName]string) error {
|
|
return b.client.UpdateStackTags(ctx, stack.id, tags)
|
|
}
|
|
|
|
var projectNameCleanRegexp = regexp.MustCompile("[^a-zA-Z0-9-_.]")
|
|
|
|
// cleanProjectName replaces undesirable characters in project names with hyphens. At some point, these restrictions
|
|
// will be further enforced by the service, but for now we need to ensure that if we are making a rest call, we
|
|
// do this cleaning on our end.
|
|
func cleanProjectName(projectName string) string {
|
|
return projectNameCleanRegexp.ReplaceAllString(projectName, "-")
|
|
}
|