github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/backend/httpstate/backend.go (about)

     1  // Copyright 2016-2022, Pulumi Corporation.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package httpstate
    16  
    17  import (
    18  	"context"
    19  	cryptorand "crypto/rand"
    20  	"encoding/hex"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"net"
    25  	"net/http"
    26  	"net/url"
    27  	"os"
    28  	"path"
    29  	"regexp"
    30  	"strconv"
    31  	"strings"
    32  	"time"
    33  
    34  	opentracing "github.com/opentracing/opentracing-go"
    35  
    36  	"github.com/pulumi/pulumi/pkg/v3/backend"
    37  	"github.com/pulumi/pulumi/pkg/v3/backend/display"
    38  	"github.com/pulumi/pulumi/pkg/v3/backend/filestate"
    39  	"github.com/pulumi/pulumi/pkg/v3/backend/httpstate/client"
    40  	"github.com/pulumi/pulumi/pkg/v3/engine"
    41  	"github.com/pulumi/pulumi/pkg/v3/operations"
    42  	"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
    43  	"github.com/pulumi/pulumi/pkg/v3/secrets"
    44  	"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
    45  	"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
    46  	"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
    47  	sdkDisplay "github.com/pulumi/pulumi/sdk/v3/go/common/display"
    48  	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
    49  	"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
    50  	"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
    51  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
    52  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
    53  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
    54  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/result"
    55  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/retry"
    56  	"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
    57  	"github.com/skratchdot/open-golang/open"
    58  )
    59  
    60  const (
    61  	// defaultAPIEnvVar can be set to override the default cloud chosen, if `--cloud` is not present.
    62  	defaultURLEnvVar = "PULUMI_API"
    63  	// AccessTokenEnvVar is the environment variable used to bypass a prompt on login.
    64  	AccessTokenEnvVar = "PULUMI_ACCESS_TOKEN"
    65  )
    66  
    67  // Name validation rules enforced by the Pulumi Service.
    68  var (
    69  	stackOwnerRegexp          = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9-_]{1,38}[a-zA-Z0-9]$")
    70  	stackNameAndProjectRegexp = regexp.MustCompile("^[A-Za-z0-9_.-]{1,100}$")
    71  )
    72  
    73  // DefaultURL returns the default cloud URL.  This may be overridden using the PULUMI_API environment
    74  // variable.  If no override is found, and we are authenticated with a cloud, choose that.  Otherwise,
    75  // we will default to the https://api.pulumi.com/ endpoint.
    76  func DefaultURL() string {
    77  	return ValueOrDefaultURL("")
    78  }
    79  
    80  // ValueOrDefaultURL returns the value if specified, or the default cloud URL otherwise.
    81  func ValueOrDefaultURL(cloudURL string) string {
    82  	// If we have a cloud URL, just return it.
    83  	if cloudURL != "" {
    84  		return strings.TrimSuffix(cloudURL, "/")
    85  	}
    86  
    87  	// Otherwise, respect the PULUMI_API override.
    88  	if cloudURL := os.Getenv(defaultURLEnvVar); cloudURL != "" {
    89  		return cloudURL
    90  	}
    91  
    92  	// If that didn't work, see if we have a current cloud, and use that. Note we need to be careful
    93  	// to ignore the local cloud.
    94  	if creds, err := workspace.GetStoredCredentials(); err == nil {
    95  		if creds.Current != "" && !filestate.IsFileStateBackendURL(creds.Current) {
    96  			return creds.Current
    97  		}
    98  	}
    99  
   100  	// If none of those led to a cloud URL, simply return the default.
   101  	return PulumiCloudURL
   102  }
   103  
   104  // Backend extends the base backend interface with specific information about cloud backends.
   105  type Backend interface {
   106  	backend.Backend
   107  
   108  	CloudURL() string
   109  
   110  	StackConsoleURL(stackRef backend.StackReference) (string, error)
   111  	Client() *client.Client
   112  
   113  	RunDeployment(ctx context.Context, stackRef backend.StackReference, req apitype.CreateDeploymentRequest,
   114  		opts display.Options) error
   115  }
   116  
   117  type cloudBackend struct {
   118  	d              diag.Sink
   119  	url            string
   120  	client         *client.Client
   121  	currentProject *workspace.Project
   122  }
   123  
   124  // Assert we implement the backend.Backend and backend.SpecificDeploymentExporter interfaces.
   125  var _ backend.SpecificDeploymentExporter = &cloudBackend{}
   126  
   127  // New creates a new Pulumi backend for the given cloud API URL and token.
   128  func New(d diag.Sink, cloudURL string) (Backend, error) {
   129  	cloudURL = ValueOrDefaultURL(cloudURL)
   130  	account, err := workspace.GetAccount(cloudURL)
   131  	if err != nil {
   132  		return nil, fmt.Errorf("getting stored credentials: %w", err)
   133  	}
   134  	apiToken := account.AccessToken
   135  
   136  	// When stringifying backend references, we take the current project (if present) into account.
   137  	currentProject, err := workspace.DetectProject()
   138  	if err != nil {
   139  		currentProject = nil
   140  	}
   141  
   142  	return &cloudBackend{
   143  		d:              d,
   144  		url:            cloudURL,
   145  		client:         client.NewClient(cloudURL, apiToken, d),
   146  		currentProject: currentProject,
   147  	}, nil
   148  }
   149  
   150  // loginWithBrowser uses a web-browser to log into the cloud and returns the cloud backend for it.
   151  func loginWithBrowser(ctx context.Context, d diag.Sink, cloudURL string, opts display.Options) (Backend, error) {
   152  	// Locally, we generate a nonce and spin up a web server listening on a random port on localhost. We then open a
   153  	// browser to a special endpoint on the Pulumi.com console, passing the generated nonce as well as the port of the
   154  	// webserver we launched. This endpoint does the OAuth flow and when it completes, redirects to localhost passing
   155  	// the nonce and the pulumi access token we created as part of the OAuth flow. If the nonces match, we set the
   156  	// access token that was passed to us and the redirect to a special welcome page on Pulumi.com
   157  
   158  	loginURL := cloudConsoleURL(cloudURL, "cli-login")
   159  	finalWelcomeURL := cloudConsoleURL(cloudURL, "welcome", "cli")
   160  
   161  	if loginURL == "" || finalWelcomeURL == "" {
   162  		return nil, errors.New("could not determine login url")
   163  	}
   164  
   165  	// Listen on localhost, have the kernel pick a random port for us
   166  	c := make(chan string)
   167  	l, err := net.Listen("tcp", "127.0.0.1:")
   168  	if err != nil {
   169  		return nil, fmt.Errorf("could not start listener: %w", err)
   170  	}
   171  
   172  	// Extract the port
   173  	_, port, err := net.SplitHostPort(l.Addr().String())
   174  	if err != nil {
   175  		return nil, fmt.Errorf("could not determine port: %w", err)
   176  	}
   177  
   178  	// Generate a nonce we'll send with the request.
   179  	nonceBytes := make([]byte, 32)
   180  	_, err = cryptorand.Read(nonceBytes)
   181  	contract.AssertNoErrorf(err, "could not get random bytes")
   182  	nonce := hex.EncodeToString(nonceBytes)
   183  
   184  	u, err := url.Parse(loginURL)
   185  	contract.AssertNoError(err)
   186  
   187  	// Generate a description to associate with the access token we'll generate, for display on the Account Settings
   188  	// page.
   189  	var tokenDescription string
   190  	if host, hostErr := os.Hostname(); hostErr == nil {
   191  		tokenDescription = fmt.Sprintf("Generated by pulumi login on %s at %s", host, time.Now().Format(time.RFC822))
   192  	} else {
   193  		tokenDescription = fmt.Sprintf("Generated by pulumi login at %s", time.Now().Format(time.RFC822))
   194  	}
   195  
   196  	// Pass our state around as query parameters on the URL we'll open the user's preferred browser to
   197  	q := u.Query()
   198  	q.Add("cliSessionPort", port)
   199  	q.Add("cliSessionNonce", nonce)
   200  	q.Add("cliSessionDescription", tokenDescription)
   201  	u.RawQuery = q.Encode()
   202  
   203  	// Start the webserver to listen to handle the response
   204  	go serveBrowserLoginServer(l, nonce, finalWelcomeURL, c)
   205  
   206  	// Launch the web browser and navigate to the login URL.
   207  	if openErr := open.Run(u.String()); openErr != nil {
   208  		fmt.Printf("We couldn't launch your web browser for some reason. Please visit:\n\n%s\n\n"+
   209  			"to finish the login process.", u)
   210  	} else {
   211  		fmt.Println("We've launched your web browser to complete the login process.")
   212  	}
   213  
   214  	fmt.Println("\nWaiting for login to complete...")
   215  
   216  	accessToken := <-c
   217  
   218  	username, organizations, err := client.NewClient(cloudURL, accessToken, d).GetPulumiAccountDetails(ctx)
   219  	if err != nil {
   220  		return nil, err
   221  	}
   222  
   223  	// Save the token and return the backend
   224  	account := workspace.Account{
   225  		AccessToken:     accessToken,
   226  		Username:        username,
   227  		Organizations:   organizations,
   228  		LastValidatedAt: time.Now(),
   229  	}
   230  	if err = workspace.StoreAccount(cloudURL, account, true); err != nil {
   231  		return nil, err
   232  	}
   233  
   234  	// Welcome the user since this was an interactive login.
   235  	WelcomeUser(opts)
   236  
   237  	return New(d, cloudURL)
   238  }
   239  
   240  // LoginManager provides a slim wrapper around functions related to backend logins.
   241  type LoginManager interface {
   242  	// Current returns the current cloud backend if one is already logged in.
   243  	Current(ctx context.Context, d diag.Sink, cloudURL string) (Backend, error)
   244  
   245  	// Login logs into the target cloud URL and returns the cloud backend for it.
   246  	Login(ctx context.Context, d diag.Sink, cloudURL string, opts display.Options) (Backend, error)
   247  }
   248  
   249  // NewLoginManager returns a LoginManager for handling backend logins.
   250  func NewLoginManager() LoginManager {
   251  	return newLoginManager()
   252  }
   253  
   254  // newLoginManager creates a new LoginManager for handling logins. It is a variable instead of a regular
   255  // function so it can be set to a different implementation at runtime, if necessary.
   256  var newLoginManager = func() LoginManager {
   257  	return defaultLoginManager{}
   258  }
   259  
   260  type defaultLoginManager struct{}
   261  
   262  // Current returns the current cloud backend if one is already logged in.
   263  func (m defaultLoginManager) Current(ctx context.Context, d diag.Sink, cloudURL string) (Backend, error) {
   264  	cloudURL = ValueOrDefaultURL(cloudURL)
   265  
   266  	// If we have a saved access token, and it is valid, use it.
   267  	existingAccount, err := workspace.GetAccount(cloudURL)
   268  	if err == nil && existingAccount.AccessToken != "" {
   269  		// If the account was last verified less than an hour ago, assume the token is valid.
   270  		valid, username, organizations := true, existingAccount.Username, existingAccount.Organizations
   271  		if username == "" || existingAccount.LastValidatedAt.Add(1*time.Hour).Before(time.Now()) {
   272  			valid, username, organizations, err = IsValidAccessToken(ctx, cloudURL, existingAccount.AccessToken)
   273  			if err != nil {
   274  				return nil, err
   275  			}
   276  			existingAccount.LastValidatedAt = time.Now()
   277  		}
   278  
   279  		if valid {
   280  			// Save the token. While it hasn't changed this will update the current cloud we are logged into, as well.
   281  			existingAccount.Username = username
   282  			existingAccount.Organizations = organizations
   283  			if err = workspace.StoreAccount(cloudURL, existingAccount, true); err != nil {
   284  				return nil, err
   285  			}
   286  
   287  			return New(d, cloudURL)
   288  		}
   289  	}
   290  
   291  	// We intentionally don't accept command-line args for the user's access token. Having it in
   292  	// .bash_history is not great, and specifying it via flag isn't of much use.
   293  	accessToken := os.Getenv(AccessTokenEnvVar)
   294  
   295  	if accessToken == "" {
   296  		// No access token available, this isn't an error per-se but we don't have a backend
   297  		return nil, nil
   298  	}
   299  
   300  	// If there's already a token from the environment, use it.
   301  	_, err = fmt.Fprintf(os.Stderr, "Logging in using access token from %s\n", AccessTokenEnvVar)
   302  	contract.IgnoreError(err)
   303  
   304  	// Try and use the credentials to see if they are valid.
   305  	valid, username, organizations, err := IsValidAccessToken(ctx, cloudURL, accessToken)
   306  	if err != nil {
   307  		return nil, err
   308  	} else if !valid {
   309  		return nil, fmt.Errorf("invalid access token")
   310  	}
   311  
   312  	// Save them.
   313  	account := workspace.Account{
   314  		AccessToken:     accessToken,
   315  		Username:        username,
   316  		Organizations:   organizations,
   317  		LastValidatedAt: time.Now(),
   318  	}
   319  	if err = workspace.StoreAccount(cloudURL, account, true); err != nil {
   320  		return nil, err
   321  	}
   322  
   323  	return New(d, cloudURL)
   324  }
   325  
   326  // Login logs into the target cloud URL and returns the cloud backend for it.
   327  func (m defaultLoginManager) Login(
   328  	ctx context.Context, d diag.Sink, cloudURL string, opts display.Options) (Backend, error) {
   329  
   330  	current, err := m.Current(ctx, d, cloudURL)
   331  	if err != nil {
   332  		return nil, err
   333  	}
   334  	if current != nil {
   335  		return current, nil
   336  	}
   337  
   338  	cloudURL = ValueOrDefaultURL(cloudURL)
   339  	var accessToken string
   340  	accountLink := cloudConsoleURL(cloudURL, "account", "tokens")
   341  
   342  	if !cmdutil.Interactive() {
   343  		// If interactive mode isn't enabled, the only way to specify a token is through the environment variable.
   344  		// Fail the attempt to login.
   345  		return nil, fmt.Errorf("%s must be set for login during non-interactive CLI sessions", AccessTokenEnvVar)
   346  	}
   347  
   348  	// If no access token is available from the environment, and we are interactive, prompt and offer to
   349  	// open a browser to make it easy to generate and use a fresh token.
   350  	line1 := "Manage your Pulumi stacks by logging in."
   351  	line1len := len(line1)
   352  	line1 = colors.Highlight(line1, "Pulumi stacks", colors.Underline+colors.Bold)
   353  	fmt.Printf(opts.Color.Colorize(line1) + "\n")
   354  	maxlen := line1len
   355  
   356  	line2 := "Run `pulumi login --help` for alternative login options."
   357  	line2len := len(line2)
   358  	fmt.Printf(opts.Color.Colorize(line2) + "\n")
   359  	if line2len > maxlen {
   360  		maxlen = line2len
   361  	}
   362  
   363  	// In the case where we could not construct a link to the pulumi console based on the API server's hostname,
   364  	// don't offer magic log-in or text about where to find your access token.
   365  	if accountLink == "" {
   366  		for {
   367  			if accessToken, err = cmdutil.ReadConsoleNoEcho("Enter your access token"); err != nil {
   368  				return nil, err
   369  			}
   370  			if accessToken != "" {
   371  				break
   372  			}
   373  		}
   374  	} else {
   375  		line3 := fmt.Sprintf("Enter your access token from %s", accountLink)
   376  		line3len := len(line3)
   377  		line3 = colors.Highlight(line3, "access token", colors.BrightCyan+colors.Bold)
   378  		line3 = colors.Highlight(line3, accountLink, colors.BrightBlue+colors.Underline+colors.Bold)
   379  		fmt.Printf(opts.Color.Colorize(line3) + "\n")
   380  		if line3len > maxlen {
   381  			maxlen = line3len
   382  		}
   383  
   384  		line4 := "    or hit <ENTER> to log in using your browser"
   385  		var padding string
   386  		if pad := maxlen - len(line4); pad > 0 {
   387  			padding = strings.Repeat(" ", pad)
   388  		}
   389  		line4 = colors.Highlight(line4, "<ENTER>", colors.BrightCyan+colors.Bold)
   390  
   391  		if accessToken, err = cmdutil.ReadConsoleNoEcho(opts.Color.Colorize(line4) + padding); err != nil {
   392  			return nil, err
   393  		}
   394  
   395  		if accessToken == "" {
   396  			return loginWithBrowser(ctx, d, cloudURL, opts)
   397  		}
   398  
   399  		// Welcome the user since this was an interactive login.
   400  		WelcomeUser(opts)
   401  	}
   402  
   403  	// Try and use the credentials to see if they are valid.
   404  	valid, username, organizations, err := IsValidAccessToken(ctx, cloudURL, accessToken)
   405  	if err != nil {
   406  		return nil, err
   407  	} else if !valid {
   408  		return nil, fmt.Errorf("invalid access token")
   409  	}
   410  
   411  	// Save them.
   412  	account := workspace.Account{
   413  		AccessToken:     accessToken,
   414  		Username:        username,
   415  		Organizations:   organizations,
   416  		LastValidatedAt: time.Now(),
   417  	}
   418  	if err = workspace.StoreAccount(cloudURL, account, true); err != nil {
   419  		return nil, err
   420  	}
   421  
   422  	return New(d, cloudURL)
   423  }
   424  
   425  // WelcomeUser prints a Welcome to Pulumi message.
   426  func WelcomeUser(opts display.Options) {
   427  	fmt.Printf(`
   428  
   429    %s
   430  
   431    Pulumi helps you create, deploy, and manage infrastructure on any cloud using
   432    your favorite language. You can get started today with Pulumi at:
   433  
   434        https://www.pulumi.com/docs/get-started/
   435  
   436    %s Resources you create with Pulumi are given unique names (a randomly
   437    generated suffix) by default. To learn more about auto-naming or customizing resource
   438    names see https://www.pulumi.com/docs/intro/concepts/resources/#autonaming.
   439  
   440  
   441  `,
   442  		opts.Color.Colorize(colors.SpecHeadline+"Welcome to Pulumi!"+colors.Reset),
   443  		opts.Color.Colorize(colors.SpecSubHeadline+"Tip of the day:"+colors.Reset))
   444  }
   445  
   446  func (b *cloudBackend) StackConsoleURL(stackRef backend.StackReference) (string, error) {
   447  	stackID, err := b.getCloudStackIdentifier(stackRef)
   448  	if err != nil {
   449  		return "", err
   450  	}
   451  
   452  	path := b.cloudConsoleStackPath(stackID)
   453  
   454  	url := b.CloudConsoleURL(path)
   455  	if url == "" {
   456  		return "", errors.New("could not determine cloud console URL")
   457  	}
   458  	return url, nil
   459  }
   460  
   461  func (b *cloudBackend) Name() string {
   462  	if b.url == PulumiCloudURL {
   463  		return "pulumi.com"
   464  	}
   465  
   466  	return b.url
   467  }
   468  
   469  func (b *cloudBackend) URL() string {
   470  	user, _, err := b.CurrentUser()
   471  	if err != nil {
   472  		return cloudConsoleURL(b.url)
   473  	}
   474  	return cloudConsoleURL(b.url, user)
   475  }
   476  
   477  func (b *cloudBackend) CurrentUser() (string, []string, error) {
   478  	return b.currentUser(context.Background())
   479  }
   480  
   481  func (b *cloudBackend) currentUser(ctx context.Context) (string, []string, error) {
   482  	account, err := workspace.GetAccount(b.CloudURL())
   483  	if err != nil {
   484  		return "", nil, err
   485  	}
   486  	if account.Username != "" {
   487  		logging.V(1).Infof("found username for access token")
   488  		return account.Username, account.Organizations, nil
   489  	}
   490  	logging.V(1).Infof("no username for access token")
   491  	name, orgs, err := b.client.GetPulumiAccountDetails(ctx)
   492  	return name, orgs, err
   493  }
   494  
   495  func (b *cloudBackend) CloudURL() string { return b.url }
   496  
   497  func (b *cloudBackend) parsePolicyPackReference(s string) (backend.PolicyPackReference, error) {
   498  	split := strings.Split(s, "/")
   499  	var orgName string
   500  	var policyPackName string
   501  
   502  	switch len(split) {
   503  	case 2:
   504  		orgName = split[0]
   505  		policyPackName = split[1]
   506  	default:
   507  		return nil, fmt.Errorf("could not parse policy pack name '%s'; must be of the form "+
   508  			"<org-name>/<policy-pack-name>", s)
   509  	}
   510  
   511  	if orgName == "" {
   512  		currentUser, _, userErr := b.CurrentUser()
   513  		if userErr != nil {
   514  			return nil, userErr
   515  		}
   516  		orgName = currentUser
   517  	}
   518  
   519  	return newCloudBackendPolicyPackReference(b.CloudConsoleURL(), orgName, tokens.QName(policyPackName)), nil
   520  }
   521  
   522  func (b *cloudBackend) GetPolicyPack(ctx context.Context, policyPack string,
   523  	d diag.Sink) (backend.PolicyPack, error) {
   524  
   525  	policyPackRef, err := b.parsePolicyPackReference(policyPack)
   526  	if err != nil {
   527  		return nil, err
   528  	}
   529  
   530  	account, err := workspace.GetAccount(b.CloudURL())
   531  	if err != nil {
   532  		return nil, err
   533  	}
   534  	apiToken := account.AccessToken
   535  
   536  	return &cloudPolicyPack{
   537  		ref: newCloudBackendPolicyPackReference(b.CloudConsoleURL(),
   538  			policyPackRef.OrgName(), policyPackRef.Name()),
   539  		b:  b,
   540  		cl: client.NewClient(b.CloudURL(), apiToken, d)}, nil
   541  }
   542  
   543  func (b *cloudBackend) ListPolicyGroups(ctx context.Context, orgName string, inContToken backend.ContinuationToken) (
   544  	apitype.ListPolicyGroupsResponse, backend.ContinuationToken, error) {
   545  	return b.client.ListPolicyGroups(ctx, orgName, inContToken)
   546  }
   547  
   548  func (b *cloudBackend) ListPolicyPacks(ctx context.Context, orgName string, inContToken backend.ContinuationToken) (
   549  	apitype.ListPolicyPacksResponse, backend.ContinuationToken, error) {
   550  	return b.client.ListPolicyPacks(ctx, orgName, inContToken)
   551  }
   552  
   553  func (b *cloudBackend) SupportsTags() bool {
   554  	return true
   555  }
   556  
   557  func (b *cloudBackend) SupportsOrganizations() bool {
   558  	return true
   559  }
   560  
   561  // qualifiedStackReference describes a qualified stack on the Pulumi Service. The Owner or Project
   562  // may be "" if unspecified, e.g. "pulumi/production" specifies the Owner and Name, but not the
   563  // Project. We infer the missing data and try to make things work as best we can in ParseStackReference.
   564  type qualifiedStackReference struct {
   565  	Owner   string
   566  	Project string
   567  	Name    string
   568  }
   569  
   570  // parseStackName parses the stack name into a potentially qualifiedStackReference. Any omitted
   571  // portions will be left as "". For example:
   572  //
   573  // "alpha"            - will just set the Name, but ignore Owner and Project.
   574  // "alpha/beta"       - will set the Owner and Name, but not Project.
   575  // "alpha/beta/gamma" - will set Owner, Name, and Project.
   576  func (b *cloudBackend) parseStackName(s string) (qualifiedStackReference, error) {
   577  	var q qualifiedStackReference
   578  
   579  	split := strings.Split(s, "/")
   580  	switch len(split) {
   581  	case 1:
   582  		q.Name = split[0]
   583  	case 2:
   584  		q.Owner = split[0]
   585  		q.Name = split[1]
   586  	case 3:
   587  		q.Owner = split[0]
   588  		q.Project = split[1]
   589  		q.Name = split[2]
   590  	default:
   591  		return qualifiedStackReference{}, fmt.Errorf("could not parse stack name '%s'", s)
   592  	}
   593  
   594  	return q, nil
   595  }
   596  
   597  func (b *cloudBackend) ParseStackReference(s string) (backend.StackReference, error) {
   598  	// Parse the input as a qualified stack name.
   599  	qualifiedName, err := b.parseStackName(s)
   600  	if err != nil {
   601  		return nil, err
   602  	}
   603  
   604  	// If the provided stack name didn't include the Owner or Project, infer them from the
   605  	// local environment.
   606  	if qualifiedName.Owner == "" {
   607  		// if the qualifiedName doesn't include an owner then let's check to see if there is a default org which *will*
   608  		// be the stack owner. If there is no defaultOrg, then we revert to checking the CurrentUser
   609  		defaultOrg, err := workspace.GetBackendConfigDefaultOrg()
   610  		if err != nil {
   611  			return nil, err
   612  		}
   613  
   614  		if defaultOrg != "" {
   615  			qualifiedName.Owner = defaultOrg
   616  		} else {
   617  			currentUser, _, userErr := b.CurrentUser()
   618  			if userErr != nil {
   619  				return nil, userErr
   620  			}
   621  			qualifiedName.Owner = currentUser
   622  		}
   623  	}
   624  
   625  	if qualifiedName.Project == "" {
   626  		currentProject, projectErr := workspace.DetectProject()
   627  		if projectErr != nil {
   628  			return nil, fmt.Errorf("If you're using the --stack flag, "+
   629  				"pass the fully qualified name (org/project/stack): %w", projectErr)
   630  		}
   631  
   632  		qualifiedName.Project = currentProject.Name.String()
   633  	}
   634  
   635  	if !tokens.IsName(qualifiedName.Name) {
   636  		return nil, errors.New("stack names may only contain alphanumeric, hyphens, underscores, and periods")
   637  	}
   638  
   639  	return cloudBackendReference{
   640  		owner:   qualifiedName.Owner,
   641  		project: qualifiedName.Project,
   642  		name:    tokens.Name(qualifiedName.Name),
   643  		b:       b,
   644  	}, nil
   645  }
   646  
   647  func (b *cloudBackend) ValidateStackName(s string) error {
   648  	qualifiedName, err := b.parseStackName(s)
   649  	if err != nil {
   650  		return err
   651  	}
   652  
   653  	// The Pulumi Service enforces specific naming restrictions for organizations,
   654  	// projects, and stacks. Though ignore any values that need to be inferred later.
   655  	if qualifiedName.Owner != "" {
   656  		if err := validateOwnerName(qualifiedName.Owner); err != nil {
   657  			return err
   658  		}
   659  	}
   660  
   661  	if qualifiedName.Project != "" {
   662  		if err := validateProjectName(qualifiedName.Project); err != nil {
   663  			return err
   664  		}
   665  	}
   666  
   667  	return validateStackName(qualifiedName.Name)
   668  }
   669  
   670  // validateOwnerName checks if a stack owner name is valid. An "owner" is simply the namespace
   671  // a stack may exist within, which for the Pulumi Service is the user account or organization.
   672  func validateOwnerName(s string) error {
   673  	if !stackOwnerRegexp.MatchString(s) {
   674  		return errors.New("invalid stack owner")
   675  	}
   676  	return nil
   677  }
   678  
   679  // validateStackName checks if a stack name is valid, returning a user-suitable error if needed.
   680  func validateStackName(s string) error {
   681  	if len(s) > 100 {
   682  		return errors.New("stack names must be less than 100 characters")
   683  	}
   684  	if !stackNameAndProjectRegexp.MatchString(s) {
   685  		return errors.New("stack names may only contain alphanumeric, hyphens, underscores, and periods")
   686  	}
   687  	return nil
   688  }
   689  
   690  // validateProjectName checks if a project name is valid, returning a user-suitable error if needed.
   691  //
   692  // NOTE: Be careful when requiring a project name be valid. The Pulumi.yaml file may contain
   693  // an invalid project name like "r@bid^W0MBAT!!", but we try to err on the side of flexibility by
   694  // implicitly "cleaning" the project name before we send it to the Pulumi Service. So when we go
   695  // to make HTTP requests, we use a more palitable name like "r_bid_W0MBAT__".
   696  //
   697  // The projects canonical name will be the sanitized "r_bid_W0MBAT__" form, but we do not require the
   698  // Pulumi.yaml file be updated.
   699  //
   700  // So we should only call validateProject name when creating _new_ stacks or creating _new_ projects.
   701  // We should not require that project names be valid when reading what is in the current workspace.
   702  func validateProjectName(s string) error {
   703  	if len(s) > 100 {
   704  		return errors.New("project names must be less than 100 characters")
   705  	}
   706  	if !stackNameAndProjectRegexp.MatchString(s) {
   707  		return errors.New("project names may only contain alphanumeric, hyphens, underscores, and periods")
   708  	}
   709  	return nil
   710  }
   711  
   712  // CloudConsoleURL returns a link to the cloud console with the given path elements.  If a console link cannot be
   713  // created, we return the empty string instead (this can happen if the endpoint isn't a recognized pattern).
   714  func (b *cloudBackend) CloudConsoleURL(paths ...string) string {
   715  	return cloudConsoleURL(b.CloudURL(), paths...)
   716  }
   717  
   718  // serveBrowserLoginServer hosts the server that completes the browser based login flow.
   719  func serveBrowserLoginServer(l net.Listener, expectedNonce string, destinationURL string, c chan<- string) {
   720  	handler := func(res http.ResponseWriter, req *http.Request) {
   721  		tok := req.URL.Query().Get("accessToken")
   722  		nonce := req.URL.Query().Get("nonce")
   723  
   724  		if tok == "" || nonce != expectedNonce {
   725  			res.WriteHeader(400)
   726  			return
   727  		}
   728  
   729  		http.Redirect(res, req, destinationURL, http.StatusTemporaryRedirect)
   730  		c <- tok
   731  	}
   732  
   733  	mux := &http.ServeMux{}
   734  	mux.HandleFunc("/", handler)
   735  	contract.IgnoreError(http.Serve(l, mux)) // nolint gosec
   736  }
   737  
   738  // CloudConsoleStackPath returns the stack path components for getting to a stack in the cloud console.  This path
   739  // must, of course, be combined with the actual console base URL by way of the CloudConsoleURL function above.
   740  func (b *cloudBackend) cloudConsoleStackPath(stackID client.StackIdentifier) string {
   741  	return path.Join(stackID.Owner, stackID.Project, stackID.Stack)
   742  }
   743  
   744  // Logout logs out of the target cloud URL.
   745  func (b *cloudBackend) Logout() error {
   746  	return workspace.DeleteAccount(b.CloudURL())
   747  }
   748  
   749  // LogoutAll logs out of all accounts
   750  func (b *cloudBackend) LogoutAll() error {
   751  	return workspace.DeleteAllAccounts()
   752  }
   753  
   754  // DoesProjectExist returns true if a project with the given name exists in this backend, or false otherwise.
   755  func (b *cloudBackend) DoesProjectExist(ctx context.Context, projectName string) (bool, error) {
   756  	owner, _, err := b.currentUser(ctx)
   757  	if err != nil {
   758  		return false, err
   759  	}
   760  
   761  	return b.client.DoesProjectExist(ctx, owner, projectName)
   762  }
   763  
   764  func (b *cloudBackend) GetStack(ctx context.Context, stackRef backend.StackReference) (backend.Stack, error) {
   765  	stackID, err := b.getCloudStackIdentifier(stackRef)
   766  	if err != nil {
   767  		return nil, err
   768  	}
   769  
   770  	stack, err := b.client.GetStack(ctx, stackID)
   771  	if err != nil {
   772  		// If this was a 404, return nil, nil as per this method's contract.
   773  		if errResp, ok := err.(*apitype.ErrorResponse); ok && errResp.Code == http.StatusNotFound {
   774  			return nil, nil
   775  		}
   776  		return nil, err
   777  	}
   778  
   779  	return newStack(stack, b), nil
   780  }
   781  
   782  // Confirm the specified stack's project doesn't contradict the Pulumi.yaml of the current project.
   783  // if the CWD is not in a Pulumi project,
   784  //
   785  //	does not contradict
   786  //
   787  // if the project name in Pulumi.yaml is "foo".
   788  //
   789  //	a stack with a name of foo/bar/foo should not work.
   790  func currentProjectContradictsWorkspace(stack client.StackIdentifier) bool {
   791  	projPath, err := workspace.DetectProjectPath()
   792  	if err != nil {
   793  		return false
   794  	}
   795  
   796  	if projPath == "" {
   797  		return false
   798  	}
   799  
   800  	proj, err := workspace.LoadProject(projPath)
   801  	if err != nil {
   802  		return false
   803  	}
   804  
   805  	return proj.Name.String() != stack.Project
   806  }
   807  
   808  func (b *cloudBackend) CreateStack(
   809  	ctx context.Context, stackRef backend.StackReference, _ interface{} /* No custom options for httpstate backend. */) (
   810  	backend.Stack, error) {
   811  	stackID, err := b.getCloudStackIdentifier(stackRef)
   812  	if err != nil {
   813  		return nil, err
   814  	}
   815  
   816  	if currentProjectContradictsWorkspace(stackID) {
   817  		return nil, fmt.Errorf("provided project name %q doesn't match Pulumi.yaml", stackID.Project)
   818  	}
   819  
   820  	tags, err := backend.GetEnvironmentTagsForCurrentStack()
   821  	if err != nil {
   822  		return nil, fmt.Errorf("error determining initial tags: %w", err)
   823  	}
   824  
   825  	apistack, err := b.client.CreateStack(ctx, stackID, tags)
   826  	if err != nil {
   827  		// Wire through well-known error types.
   828  		if errResp, ok := err.(*apitype.ErrorResponse); ok && errResp.Code == http.StatusConflict {
   829  			// A 409 error response is returned when per-stack organizations are over their limit,
   830  			// so we need to look at the message to differentiate.
   831  			if strings.Contains(errResp.Message, "already exists") {
   832  				return nil, &backend.StackAlreadyExistsError{StackName: stackID.String()}
   833  			}
   834  			if strings.Contains(errResp.Message, "you are using") {
   835  				return nil, &backend.OverStackLimitError{Message: errResp.Message}
   836  			}
   837  		}
   838  		return nil, err
   839  	}
   840  
   841  	stack := newStack(apistack, b)
   842  	fmt.Printf("Created stack '%s'\n", stack.Ref())
   843  
   844  	return stack, nil
   845  }
   846  
   847  func (b *cloudBackend) ListStacks(
   848  	ctx context.Context, filter backend.ListStacksFilter, inContToken backend.ContinuationToken) (
   849  	[]backend.StackSummary, backend.ContinuationToken, error) {
   850  	// Sanitize the project name as needed, so when communicating with the Pulumi Service we
   851  	// always use the name the service expects. (So that a similar, but not technically valid
   852  	// name may be put in Pulumi.yaml without causing problems.)
   853  	if filter.Project != nil {
   854  		cleanedProj := cleanProjectName(*filter.Project)
   855  		filter.Project = &cleanedProj
   856  	}
   857  
   858  	// Duplicate type to avoid circular dependency.
   859  	clientFilter := client.ListStacksFilter{
   860  		Organization: filter.Organization,
   861  		Project:      filter.Project,
   862  		TagName:      filter.TagName,
   863  		TagValue:     filter.TagValue,
   864  	}
   865  
   866  	apiSummaries, outContToken, err := b.client.ListStacks(ctx, clientFilter, inContToken)
   867  	if err != nil {
   868  		return nil, nil, err
   869  	}
   870  
   871  	// Convert []apitype.StackSummary into []backend.StackSummary.
   872  	var backendSummaries []backend.StackSummary
   873  	for _, apiSummary := range apiSummaries {
   874  		backendSummary := cloudStackSummary{
   875  			summary: apiSummary,
   876  			b:       b,
   877  		}
   878  		backendSummaries = append(backendSummaries, backendSummary)
   879  	}
   880  
   881  	return backendSummaries, outContToken, nil
   882  }
   883  
   884  func (b *cloudBackend) RemoveStack(ctx context.Context, stack backend.Stack, force bool) (bool, error) {
   885  	stackID, err := b.getCloudStackIdentifier(stack.Ref())
   886  	if err != nil {
   887  		return false, err
   888  	}
   889  
   890  	return b.client.DeleteStack(ctx, stackID, force)
   891  }
   892  
   893  func (b *cloudBackend) RenameStack(ctx context.Context, stack backend.Stack,
   894  	newName tokens.QName) (backend.StackReference, error) {
   895  	stackID, err := b.getCloudStackIdentifier(stack.Ref())
   896  	if err != nil {
   897  		return nil, err
   898  	}
   899  
   900  	// Support a qualified stack name, which would also rename the stack's project too.
   901  	// e.g. if you want to change the project name on the Pulumi Console to reflect a
   902  	// new value in Pulumi.yaml.
   903  	newRef, err := b.ParseStackReference(string(newName))
   904  	if err != nil {
   905  		return nil, err
   906  	}
   907  	newIdentity, err := b.getCloudStackIdentifier(newRef)
   908  	if err != nil {
   909  		return nil, err
   910  	}
   911  
   912  	if stackID.Owner != newIdentity.Owner {
   913  		errMsg := fmt.Sprintf(
   914  			"New stack owner, %s, does not match existing owner, %s.\n\n",
   915  			stackID.Owner, newIdentity.Owner)
   916  
   917  		// Re-parse the name using the parseStackName function to avoid the logic in ParseStackReference
   918  		// that auto-populates the owner property with the currently logged in account. We actually want to
   919  		// give a different error message if the raw stack name itself didn't include an owner part.
   920  		parsedName, err := b.parseStackName(string(newName))
   921  		contract.IgnoreError(err)
   922  		if parsedName.Owner == "" {
   923  			errMsg += fmt.Sprintf(
   924  				"       Did you forget to include the owner name? If yes, rerun the command as follows:\n\n"+
   925  					"           $ pulumi stack rename %s/%s\n\n",
   926  				stackID.Owner, newName)
   927  		}
   928  
   929  		errMsgSuffix := "."
   930  		if consoleURL, err := b.StackConsoleURL(stack.Ref()); err == nil {
   931  			errMsgSuffix = ":\n\n           " + consoleURL + "/settings/options"
   932  		}
   933  		errMsg += "       You cannot transfer stack ownership via a rename. If you wish to transfer ownership\n" +
   934  			"       of a stack to another organization, you can do so in the Pulumi Console by going to the\n" +
   935  			"       \"Settings\" page of the stack and then clicking the \"Transfer Stack\" button"
   936  
   937  		return nil, errors.New(errMsg + errMsgSuffix)
   938  	}
   939  
   940  	if err = b.client.RenameStack(ctx, stackID, newIdentity); err != nil {
   941  		return nil, err
   942  	}
   943  	return newRef, nil
   944  }
   945  
   946  func (b *cloudBackend) Preview(ctx context.Context, stack backend.Stack,
   947  	op backend.UpdateOperation) (*deploy.Plan, sdkDisplay.ResourceChanges, result.Result) {
   948  	// We can skip PreviewtThenPromptThenExecute, and just go straight to Execute.
   949  	opts := backend.ApplierOptions{
   950  		DryRun:   true,
   951  		ShowLink: true,
   952  	}
   953  	return b.apply(
   954  		ctx, apitype.PreviewUpdate, stack, op, opts, nil /*events*/)
   955  }
   956  
   957  func (b *cloudBackend) Update(ctx context.Context, stack backend.Stack,
   958  	op backend.UpdateOperation) (sdkDisplay.ResourceChanges, result.Result) {
   959  	return backend.PreviewThenPromptThenExecute(ctx, apitype.UpdateUpdate, stack, op, b.apply)
   960  }
   961  
   962  func (b *cloudBackend) Import(ctx context.Context, stack backend.Stack,
   963  	op backend.UpdateOperation, imports []deploy.Import) (sdkDisplay.ResourceChanges, result.Result) {
   964  	op.Imports = imports
   965  	return backend.PreviewThenPromptThenExecute(ctx, apitype.ResourceImportUpdate, stack, op, b.apply)
   966  }
   967  
   968  func (b *cloudBackend) Refresh(ctx context.Context, stack backend.Stack,
   969  	op backend.UpdateOperation) (sdkDisplay.ResourceChanges, result.Result) {
   970  	return backend.PreviewThenPromptThenExecute(ctx, apitype.RefreshUpdate, stack, op, b.apply)
   971  }
   972  
   973  func (b *cloudBackend) Destroy(ctx context.Context, stack backend.Stack,
   974  	op backend.UpdateOperation) (sdkDisplay.ResourceChanges, result.Result) {
   975  	return backend.PreviewThenPromptThenExecute(ctx, apitype.DestroyUpdate, stack, op, b.apply)
   976  }
   977  
   978  func (b *cloudBackend) Watch(ctx context.Context, stack backend.Stack,
   979  	op backend.UpdateOperation, paths []string) result.Result {
   980  	return backend.Watch(ctx, b, stack, op, b.apply, paths)
   981  }
   982  
   983  func (b *cloudBackend) Query(ctx context.Context, op backend.QueryOperation) result.Result {
   984  	return b.query(ctx, op, nil /*events*/)
   985  }
   986  
   987  func (b *cloudBackend) createAndStartUpdate(
   988  	ctx context.Context, action apitype.UpdateKind, stack backend.Stack,
   989  	op *backend.UpdateOperation, dryRun bool) (client.UpdateIdentifier, int, string, error) {
   990  
   991  	stackRef := stack.Ref()
   992  
   993  	stackID, err := b.getCloudStackIdentifier(stackRef)
   994  	if err != nil {
   995  		return client.UpdateIdentifier{}, 0, "", err
   996  	}
   997  	if currentProjectContradictsWorkspace(stackID) {
   998  		return client.UpdateIdentifier{}, 0, "", fmt.Errorf(
   999  			"provided project name %q doesn't match Pulumi.yaml", stackID.Project)
  1000  	}
  1001  	metadata := apitype.UpdateMetadata{
  1002  		Message:     op.M.Message,
  1003  		Environment: op.M.Environment,
  1004  	}
  1005  	update, reqdPolicies, err := b.client.CreateUpdate(
  1006  		ctx, action, stackID, op.Proj, op.StackConfiguration.Config, metadata, op.Opts.Engine, dryRun)
  1007  	if err != nil {
  1008  		return client.UpdateIdentifier{}, 0, "", err
  1009  	}
  1010  
  1011  	//
  1012  	// TODO[pulumi-service#3745]: Move this to the plugin-gathering routine when we have a dedicated
  1013  	// service API when for getting a list of the required policies to run.
  1014  	//
  1015  	// For now, this list is given to us when we start an update; yet, the list of analyzers to boot
  1016  	// is given to us by CLI flag, and passed to the step generator (which lazily instantiates the
  1017  	// plugins) via `op.Opts.Engine.Analyzers`. Since the "start update" API request is sent well
  1018  	// after this field is populated, we instead populate the `RequiredPlugins` field here.
  1019  	//
  1020  	// Once this API is implemented, we can safely move these lines to the plugin-gathering code,
  1021  	// which is much closer to being the "correct" place for this stuff.
  1022  	//
  1023  	for _, policy := range reqdPolicies {
  1024  		op.Opts.Engine.RequiredPolicies = append(
  1025  			op.Opts.Engine.RequiredPolicies, newCloudRequiredPolicy(b.client, policy, update.Owner))
  1026  	}
  1027  
  1028  	// Start the update. We use this opportunity to pass new tags to the service, to pick up any
  1029  	// metadata changes.
  1030  	tags, err := backend.GetMergedStackTags(ctx, stack)
  1031  	if err != nil {
  1032  		return client.UpdateIdentifier{}, 0, "", fmt.Errorf("getting stack tags: %w", err)
  1033  	}
  1034  	version, token, err := b.client.StartUpdate(ctx, update, tags)
  1035  	if err != nil {
  1036  		if err, ok := err.(*apitype.ErrorResponse); ok && err.Code == 409 {
  1037  			conflict := backend.ConflictingUpdateError{Err: err}
  1038  			return client.UpdateIdentifier{}, 0, "", conflict
  1039  		}
  1040  		return client.UpdateIdentifier{}, 0, "", err
  1041  	}
  1042  	// Any non-preview update will be considered part of the stack's update history.
  1043  	if action != apitype.PreviewUpdate {
  1044  		logging.V(7).Infof("Stack %s being updated to version %d", stackRef, version)
  1045  	}
  1046  
  1047  	return update, version, token, nil
  1048  }
  1049  
  1050  // apply actually performs the provided type of update on a stack hosted in the Pulumi Cloud.
  1051  func (b *cloudBackend) apply(
  1052  	ctx context.Context, kind apitype.UpdateKind, stack backend.Stack,
  1053  	op backend.UpdateOperation, opts backend.ApplierOptions,
  1054  	events chan<- engine.Event) (*deploy.Plan, sdkDisplay.ResourceChanges, result.Result) {
  1055  
  1056  	actionLabel := backend.ActionLabel(kind, opts.DryRun)
  1057  
  1058  	if !(op.Opts.Display.JSONDisplay || op.Opts.Display.Type == display.DisplayWatch) {
  1059  		// Print a banner so it's clear this is going to the cloud.
  1060  		fmt.Printf(op.Opts.Display.Color.Colorize(
  1061  			colors.SpecHeadline+"%s (%s)"+colors.Reset+"\n\n"), actionLabel, stack.Ref())
  1062  	}
  1063  
  1064  	// Create an update object to persist results.
  1065  	update, version, token, err :=
  1066  		b.createAndStartUpdate(ctx, kind, stack, &op, opts.DryRun)
  1067  	if err != nil {
  1068  		return nil, nil, result.FromError(err)
  1069  	}
  1070  
  1071  	if !op.Opts.Display.SuppressPermalink && opts.ShowLink && !op.Opts.Display.JSONDisplay {
  1072  		// Print a URL at the beginning of the update pointing to the Pulumi Service.
  1073  		b.printLink(op, opts, update, version)
  1074  	}
  1075  
  1076  	return b.runEngineAction(ctx, kind, stack.Ref(), op, update, token, events, opts.DryRun)
  1077  }
  1078  
  1079  // printLink prints a link to the update in the Pulumi Service.
  1080  func (b *cloudBackend) printLink(
  1081  	op backend.UpdateOperation, opts backend.ApplierOptions,
  1082  	update client.UpdateIdentifier, version int) {
  1083  	var link string
  1084  	base := b.cloudConsoleStackPath(update.StackIdentifier)
  1085  	if !opts.DryRun {
  1086  		link = b.CloudConsoleURL(base, "updates", strconv.Itoa(version))
  1087  	} else {
  1088  		link = b.CloudConsoleURL(base, "previews", update.UpdateID)
  1089  	}
  1090  	if link != "" {
  1091  		fmt.Printf(op.Opts.Display.Color.Colorize(
  1092  			colors.SpecHeadline+"View Live: "+
  1093  				colors.Underline+colors.BrightBlue+"%s"+colors.Reset+"\n\n"), link)
  1094  	}
  1095  }
  1096  
  1097  // query executes a query program against the resource outputs of a stack hosted in the Pulumi
  1098  // Cloud.
  1099  func (b *cloudBackend) query(ctx context.Context, op backend.QueryOperation,
  1100  	callerEventsOpt chan<- engine.Event) result.Result {
  1101  
  1102  	return backend.RunQuery(ctx, b, op, callerEventsOpt, b.newQuery)
  1103  }
  1104  
  1105  func (b *cloudBackend) runEngineAction(
  1106  	ctx context.Context, kind apitype.UpdateKind, stackRef backend.StackReference,
  1107  	op backend.UpdateOperation, update client.UpdateIdentifier, token string,
  1108  	callerEventsOpt chan<- engine.Event, dryRun bool) (*deploy.Plan, sdkDisplay.ResourceChanges, result.Result) {
  1109  
  1110  	contract.Assertf(token != "", "persisted actions require a token")
  1111  	u, err := b.newUpdate(ctx, stackRef, op, update, token)
  1112  	if err != nil {
  1113  		return nil, nil, result.FromError(err)
  1114  	}
  1115  
  1116  	// displayEvents renders the event to the console and Pulumi service. The processor for the
  1117  	// will signal all events have been proceed when a value is written to the displayDone channel.
  1118  	displayEvents := make(chan engine.Event)
  1119  	displayDone := make(chan bool)
  1120  	go u.RecordAndDisplayEvents(
  1121  		backend.ActionLabel(kind, dryRun), kind, stackRef, op,
  1122  		displayEvents, displayDone, op.Opts.Display, dryRun)
  1123  
  1124  	// The engineEvents channel receives all events from the engine, which we then forward onto other
  1125  	// channels for actual processing. (displayEvents and callerEventsOpt.)
  1126  	engineEvents := make(chan engine.Event)
  1127  	eventsDone := make(chan bool)
  1128  	go func() {
  1129  		for e := range engineEvents {
  1130  			displayEvents <- e
  1131  			if callerEventsOpt != nil {
  1132  				callerEventsOpt <- e
  1133  			}
  1134  		}
  1135  
  1136  		close(eventsDone)
  1137  	}()
  1138  
  1139  	// The backend.SnapshotManager and backend.SnapshotPersister will keep track of any changes to
  1140  	// the Snapshot (checkpoint file) in the HTTP backend. We will reuse the snapshot's secrets manager when possible
  1141  	// to ensure that secrets are not re-encrypted on each update.
  1142  	sm := op.SecretsManager
  1143  	if secrets.AreCompatible(sm, u.GetTarget().Snapshot.SecretsManager) {
  1144  		sm = u.GetTarget().Snapshot.SecretsManager
  1145  	}
  1146  	persister := b.newSnapshotPersister(ctx, u.update, u.tokenSource, sm)
  1147  	snapshotManager := backend.NewSnapshotManager(persister, u.GetTarget().Snapshot)
  1148  
  1149  	// Depending on the action, kick off the relevant engine activity.  Note that we don't immediately check and
  1150  	// return error conditions, because we will do so below after waiting for the display channels to close.
  1151  	cancellationScope := op.Scopes.NewScope(engineEvents, dryRun)
  1152  	engineCtx := &engine.Context{
  1153  		Cancel:          cancellationScope.Context(),
  1154  		Events:          engineEvents,
  1155  		SnapshotManager: snapshotManager,
  1156  		BackendClient:   httpstateBackendClient{backend: b},
  1157  	}
  1158  	if parentSpan := opentracing.SpanFromContext(ctx); parentSpan != nil {
  1159  		engineCtx.ParentSpan = parentSpan.Context()
  1160  	}
  1161  
  1162  	var plan *deploy.Plan
  1163  	var changes sdkDisplay.ResourceChanges
  1164  	var res result.Result
  1165  	switch kind {
  1166  	case apitype.PreviewUpdate:
  1167  		plan, changes, res = engine.Update(u, engineCtx, op.Opts.Engine, true)
  1168  	case apitype.UpdateUpdate:
  1169  		_, changes, res = engine.Update(u, engineCtx, op.Opts.Engine, dryRun)
  1170  	case apitype.ResourceImportUpdate:
  1171  		_, changes, res = engine.Import(u, engineCtx, op.Opts.Engine, op.Imports, dryRun)
  1172  	case apitype.RefreshUpdate:
  1173  		_, changes, res = engine.Refresh(u, engineCtx, op.Opts.Engine, dryRun)
  1174  	case apitype.DestroyUpdate:
  1175  		_, changes, res = engine.Destroy(u, engineCtx, op.Opts.Engine, dryRun)
  1176  	default:
  1177  		contract.Failf("Unrecognized update kind: %s", kind)
  1178  	}
  1179  
  1180  	// Wait for dependent channels to finish processing engineEvents before closing.
  1181  	<-displayDone
  1182  	cancellationScope.Close() // Don't take any cancellations anymore, we're shutting down.
  1183  	close(engineEvents)
  1184  	contract.IgnoreClose(snapshotManager)
  1185  
  1186  	// Make sure that the goroutine writing to displayEvents and callerEventsOpt
  1187  	// has exited before proceeding
  1188  	<-eventsDone
  1189  	close(displayEvents)
  1190  
  1191  	// Mark the update as complete.
  1192  	status := apitype.UpdateStatusSucceeded
  1193  	if res != nil {
  1194  		status = apitype.UpdateStatusFailed
  1195  	}
  1196  	completeErr := u.Complete(status)
  1197  	if completeErr != nil {
  1198  		res = result.Merge(res, result.FromError(fmt.Errorf("failed to complete update: %w", completeErr)))
  1199  	}
  1200  
  1201  	return plan, changes, res
  1202  }
  1203  
  1204  func (b *cloudBackend) CancelCurrentUpdate(ctx context.Context, stackRef backend.StackReference) error {
  1205  	stackID, err := b.getCloudStackIdentifier(stackRef)
  1206  	if err != nil {
  1207  		return err
  1208  	}
  1209  	stack, err := b.client.GetStack(ctx, stackID)
  1210  	if err != nil {
  1211  		return err
  1212  	}
  1213  
  1214  	if stack.ActiveUpdate == "" {
  1215  		return fmt.Errorf("stack %v has never been updated", stackRef)
  1216  	}
  1217  
  1218  	// Compute the update identifier and attempt to cancel the update.
  1219  	//
  1220  	// NOTE: the update kind is not relevant; the same endpoint will work for updates of all kinds.
  1221  	updateID := client.UpdateIdentifier{
  1222  		StackIdentifier: stackID,
  1223  		UpdateKind:      apitype.UpdateUpdate,
  1224  		UpdateID:        stack.ActiveUpdate,
  1225  	}
  1226  	return b.client.CancelUpdate(ctx, updateID)
  1227  }
  1228  
  1229  func (b *cloudBackend) GetHistory(
  1230  	ctx context.Context,
  1231  	stackRef backend.StackReference,
  1232  	pageSize int,
  1233  	page int) ([]backend.UpdateInfo, error) {
  1234  	stack, err := b.getCloudStackIdentifier(stackRef)
  1235  	if err != nil {
  1236  		return nil, err
  1237  	}
  1238  
  1239  	updates, err := b.client.GetStackUpdates(ctx, stack, pageSize, page)
  1240  	if err != nil {
  1241  		return nil, fmt.Errorf("failed to get stack updates: %w", err)
  1242  	}
  1243  
  1244  	// Convert apitype.UpdateInfo objects to the backend type.
  1245  	var beUpdates []backend.UpdateInfo
  1246  	for _, update := range updates {
  1247  		// Convert types from the apitype package into their internal counterparts.
  1248  		cfg, err := convertConfig(update.Config)
  1249  		if err != nil {
  1250  			return nil, fmt.Errorf("converting configuration: %w", err)
  1251  		}
  1252  
  1253  		beUpdates = append(beUpdates, backend.UpdateInfo{
  1254  			Version:         update.Version,
  1255  			Kind:            update.Kind,
  1256  			Message:         update.Message,
  1257  			Environment:     update.Environment,
  1258  			Config:          cfg,
  1259  			Result:          backend.UpdateResult(update.Result),
  1260  			StartTime:       update.StartTime,
  1261  			EndTime:         update.EndTime,
  1262  			ResourceChanges: convertResourceChanges(update.ResourceChanges),
  1263  		})
  1264  	}
  1265  
  1266  	return beUpdates, nil
  1267  }
  1268  
  1269  func (b *cloudBackend) GetLatestConfiguration(ctx context.Context,
  1270  	stack backend.Stack) (config.Map, error) {
  1271  
  1272  	stackID, err := b.getCloudStackIdentifier(stack.Ref())
  1273  	if err != nil {
  1274  		return nil, err
  1275  	}
  1276  
  1277  	cfg, err := b.client.GetLatestConfiguration(ctx, stackID)
  1278  	switch {
  1279  	case err == client.ErrNoPreviousDeployment:
  1280  		return nil, backend.ErrNoPreviousDeployment
  1281  	case err != nil:
  1282  		return nil, err
  1283  	default:
  1284  		return cfg, nil
  1285  	}
  1286  }
  1287  
  1288  // convertResourceChanges converts the apitype version of sdkDisplay.ResourceChanges into the internal version.
  1289  func convertResourceChanges(changes map[apitype.OpType]int) sdkDisplay.ResourceChanges {
  1290  	b := make(sdkDisplay.ResourceChanges)
  1291  	for k, v := range changes {
  1292  		b[sdkDisplay.StepOp(k)] = v
  1293  	}
  1294  	return b
  1295  }
  1296  
  1297  // convertResourceChanges converts the apitype version of config.Map into the internal version.
  1298  func convertConfig(apiConfig map[string]apitype.ConfigValue) (config.Map, error) {
  1299  	c := make(config.Map)
  1300  	for rawK, rawV := range apiConfig {
  1301  		k, err := config.ParseKey(rawK)
  1302  		if err != nil {
  1303  			return nil, err
  1304  		}
  1305  		if rawV.Object {
  1306  			if rawV.Secret {
  1307  				c[k] = config.NewSecureObjectValue(rawV.String)
  1308  			} else {
  1309  				c[k] = config.NewObjectValue(rawV.String)
  1310  			}
  1311  		} else {
  1312  			if rawV.Secret {
  1313  				c[k] = config.NewSecureValue(rawV.String)
  1314  			} else {
  1315  				c[k] = config.NewValue(rawV.String)
  1316  			}
  1317  		}
  1318  	}
  1319  	return c, nil
  1320  }
  1321  
  1322  func (b *cloudBackend) GetLogs(ctx context.Context, stack backend.Stack, cfg backend.StackConfiguration,
  1323  	logQuery operations.LogQuery) ([]operations.LogEntry, error) {
  1324  
  1325  	target, targetErr := b.getTarget(ctx, stack.Ref(), cfg.Config, cfg.Decrypter)
  1326  	if targetErr != nil {
  1327  		return nil, targetErr
  1328  	}
  1329  	return filestate.GetLogsForTarget(target, logQuery)
  1330  }
  1331  
  1332  // ExportDeployment exports a deployment _from_ the backend service.
  1333  // This will return the stack state that was being stored on the backend service.
  1334  func (b *cloudBackend) ExportDeployment(ctx context.Context,
  1335  	stack backend.Stack) (*apitype.UntypedDeployment, error) {
  1336  	return b.exportDeployment(ctx, stack.Ref(), nil /* latest */)
  1337  }
  1338  
  1339  func (b *cloudBackend) ExportDeploymentForVersion(
  1340  	ctx context.Context, stack backend.Stack, version string) (*apitype.UntypedDeployment, error) {
  1341  	// The Pulumi Console defines versions as a positive integer. Parse the provided version string and
  1342  	// ensure it is valid.
  1343  	//
  1344  	// The first stack update version is 1, and monotonically increasing from there.
  1345  	versionNumber, err := strconv.Atoi(version)
  1346  	if err != nil || versionNumber <= 0 {
  1347  		return nil, fmt.Errorf(
  1348  			"%q is not a valid stack version. It should be a positive integer",
  1349  			version)
  1350  	}
  1351  
  1352  	return b.exportDeployment(ctx, stack.Ref(), &versionNumber)
  1353  }
  1354  
  1355  // exportDeployment exports the checkpoint file for a stack, optionally getting a previous version.
  1356  func (b *cloudBackend) exportDeployment(
  1357  	ctx context.Context, stackRef backend.StackReference, version *int) (*apitype.UntypedDeployment, error) {
  1358  	stack, err := b.getCloudStackIdentifier(stackRef)
  1359  	if err != nil {
  1360  		return nil, err
  1361  	}
  1362  
  1363  	deployment, err := b.client.ExportStackDeployment(ctx, stack, version)
  1364  	if err != nil {
  1365  		return nil, err
  1366  	}
  1367  
  1368  	return &deployment, nil
  1369  }
  1370  
  1371  // ImportDeployment imports a deployment _into_ the backend. At the end of this operation,
  1372  // the deployment provided will be the current state stored on the backend service.
  1373  func (b *cloudBackend) ImportDeployment(ctx context.Context, stack backend.Stack,
  1374  	deployment *apitype.UntypedDeployment) error {
  1375  
  1376  	stackID, err := b.getCloudStackIdentifier(stack.Ref())
  1377  	if err != nil {
  1378  		return err
  1379  	}
  1380  
  1381  	update, err := b.client.ImportStackDeployment(ctx, stackID, deployment)
  1382  	if err != nil {
  1383  		return err
  1384  	}
  1385  
  1386  	// Wait for the import to complete, which also polls and renders event output to STDOUT.
  1387  	status, err := b.waitForUpdate(
  1388  		ctx, backend.ActionLabel(apitype.StackImportUpdate, false /*dryRun*/), update,
  1389  		display.Options{Color: colors.Always})
  1390  	if err != nil {
  1391  		return fmt.Errorf("waiting for import: %w", err)
  1392  	} else if status != apitype.StatusSucceeded {
  1393  		return fmt.Errorf("import unsuccessful: status %v", status)
  1394  	}
  1395  	return nil
  1396  }
  1397  
  1398  var (
  1399  	projectNameCleanRegexp = regexp.MustCompile("[^a-zA-Z0-9-_.]")
  1400  )
  1401  
  1402  // cleanProjectName replaces undesirable characters in project names with hyphens. At some point, these restrictions
  1403  // will be further enforced by the service, but for now we need to ensure that if we are making a rest call, we
  1404  // do this cleaning on our end.
  1405  func cleanProjectName(projectName string) string {
  1406  	return projectNameCleanRegexp.ReplaceAllString(projectName, "-")
  1407  }
  1408  
  1409  // getCloudStackIdentifier converts a backend.StackReference to a client.StackIdentifier for the same logical stack
  1410  func (b *cloudBackend) getCloudStackIdentifier(stackRef backend.StackReference) (client.StackIdentifier, error) {
  1411  	cloudBackendStackRef, ok := stackRef.(cloudBackendReference)
  1412  	if !ok {
  1413  		return client.StackIdentifier{}, errors.New("bad stack reference type")
  1414  	}
  1415  
  1416  	return client.StackIdentifier{
  1417  		Owner:   cloudBackendStackRef.owner,
  1418  		Project: cleanProjectName(cloudBackendStackRef.project),
  1419  		Stack:   string(cloudBackendStackRef.name),
  1420  	}, nil
  1421  }
  1422  
  1423  // Client returns a client object that may be used to interact with this backend.
  1424  func (b *cloudBackend) Client() *client.Client {
  1425  	return b.client
  1426  }
  1427  
  1428  type DisplayEventType string
  1429  
  1430  const (
  1431  	UpdateEvent   DisplayEventType = "UpdateEvent"
  1432  	ShutdownEvent DisplayEventType = "Shutdown"
  1433  )
  1434  
  1435  type displayEvent struct {
  1436  	Kind    DisplayEventType
  1437  	Payload interface{}
  1438  }
  1439  
  1440  // waitForUpdate waits for the current update of a Pulumi program to reach a terminal state. Returns the
  1441  // final state. "path" is the URL endpoint to poll for updates.
  1442  func (b *cloudBackend) waitForUpdate(ctx context.Context, actionLabel string, update client.UpdateIdentifier,
  1443  	displayOpts display.Options) (apitype.UpdateStatus, error) {
  1444  
  1445  	events, done := make(chan displayEvent), make(chan bool)
  1446  	defer func() {
  1447  		events <- displayEvent{Kind: ShutdownEvent, Payload: nil}
  1448  		<-done
  1449  		close(events)
  1450  		close(done)
  1451  	}()
  1452  	go displayEvents(strings.ToLower(actionLabel), events, done, displayOpts)
  1453  
  1454  	// The UpdateEvents API returns a continuation token to only get events after the previous call.
  1455  	var continuationToken *string
  1456  	for {
  1457  		// Query for the latest update results, including log entries so we can provide active status updates.
  1458  		_, results, err := retry.Until(context.Background(), retry.Acceptor{
  1459  			Accept: func(try int, nextRetryTime time.Duration) (bool, interface{}, error) {
  1460  				return b.tryNextUpdate(ctx, update, continuationToken, try, nextRetryTime)
  1461  			},
  1462  		})
  1463  		if err != nil {
  1464  			return apitype.StatusFailed, err
  1465  		}
  1466  
  1467  		// We got a result, print it out.
  1468  		updateResults := results.(apitype.UpdateResults)
  1469  		for _, event := range updateResults.Events {
  1470  			events <- displayEvent{Kind: UpdateEvent, Payload: event}
  1471  		}
  1472  
  1473  		continuationToken = updateResults.ContinuationToken
  1474  		// A nil continuation token means there are no more events to read and the update has finished.
  1475  		if continuationToken == nil {
  1476  			return updateResults.Status, nil
  1477  		}
  1478  	}
  1479  }
  1480  
  1481  func displayEvents(action string, events <-chan displayEvent, done chan<- bool, opts display.Options) {
  1482  	prefix := fmt.Sprintf("%s%s...", cmdutil.EmojiOr("✨ ", "@ "), action)
  1483  	spinner, ticker := cmdutil.NewSpinnerAndTicker(prefix, nil, opts.Color, 8 /*timesPerSecond*/)
  1484  
  1485  	defer func() {
  1486  		spinner.Reset()
  1487  		ticker.Stop()
  1488  		done <- true
  1489  	}()
  1490  
  1491  	for {
  1492  		select {
  1493  		case <-ticker.C:
  1494  			spinner.Tick()
  1495  		case event := <-events:
  1496  			if event.Kind == ShutdownEvent {
  1497  				return
  1498  			}
  1499  
  1500  			// Pluck out the string.
  1501  			payload := event.Payload.(apitype.UpdateEvent)
  1502  			if raw, ok := payload.Fields["text"]; ok && raw != nil {
  1503  				if text, ok := raw.(string); ok {
  1504  					text = opts.Color.Colorize(text)
  1505  
  1506  					// Choose the stream to write to (by default stdout).
  1507  					var stream io.Writer
  1508  					if payload.Kind == apitype.StderrEvent {
  1509  						stream = os.Stderr
  1510  					} else {
  1511  						stream = os.Stdout
  1512  					}
  1513  
  1514  					if text != "" {
  1515  						spinner.Reset()
  1516  						fmt.Fprint(stream, text)
  1517  					}
  1518  				}
  1519  			}
  1520  		}
  1521  	}
  1522  }
  1523  
  1524  // tryNextUpdate tries to get the next update for a Pulumi program.  This may time or error out, which results in a
  1525  // false returned in the first return value.  If a non-nil error is returned, this operation should fail.
  1526  func (b *cloudBackend) tryNextUpdate(ctx context.Context, update client.UpdateIdentifier, continuationToken *string,
  1527  	try int, nextRetryTime time.Duration) (bool, interface{}, error) {
  1528  
  1529  	// If there is no error, we're done.
  1530  	results, err := b.client.GetUpdateEvents(ctx, update, continuationToken)
  1531  	if err == nil {
  1532  		return true, results, nil
  1533  	}
  1534  
  1535  	// There are three kinds of errors we might see:
  1536  	//     1) Expected HTTP errors (like timeouts); silently retry.
  1537  	//     2) Unexpected HTTP errors (like Unauthorized, etc); exit with an error.
  1538  	//     3) Anything else; this could be any number of things, including transient errors (flaky network).
  1539  	//        In this case, we warn the user and keep retrying; they can ^C if it's not transient.
  1540  	warn := true
  1541  	if errResp, ok := err.(*apitype.ErrorResponse); ok {
  1542  		if errResp.Code == 504 {
  1543  			// If our request to the Pulumi Service returned a 504 (Gateway Timeout), ignore it and keep
  1544  			// continuing.  The sole exception is if we've done this 10 times.  At that point, we will have
  1545  			// been waiting for many seconds, and want to let the user know something might be wrong.
  1546  			if try < 10 {
  1547  				warn = false
  1548  			}
  1549  			logging.V(3).Infof("Expected %s HTTP %d error after %d retries (retrying): %v",
  1550  				b.CloudURL(), errResp.Code, try, err)
  1551  		} else {
  1552  			// Otherwise, we will issue an error.
  1553  			logging.V(3).Infof("Unexpected %s HTTP %d error after %d retries (erroring): %v",
  1554  				b.CloudURL(), errResp.Code, try, err)
  1555  			return false, nil, err
  1556  		}
  1557  	} else {
  1558  		logging.V(3).Infof("Unexpected %s error after %d retries (retrying): %v", b.CloudURL(), try, err)
  1559  	}
  1560  
  1561  	// Issue a warning if appropriate.
  1562  	if warn {
  1563  		b.d.Warningf(diag.Message("" /*urn*/, "error querying update status: %v"), err)
  1564  		b.d.Warningf(diag.Message("" /*urn*/, "retrying in %vs... ^C to stop (this will not cancel the update)"),
  1565  			nextRetryTime.Seconds())
  1566  	}
  1567  
  1568  	return false, nil, nil
  1569  }
  1570  
  1571  // IsValidAccessToken tries to use the provided Pulumi access token and returns if it is accepted
  1572  // or not. Returns error on any unexpected error.
  1573  func IsValidAccessToken(ctx context.Context, cloudURL, accessToken string) (bool, string, []string, error) {
  1574  	// Make a request to get the authenticated user. If it returns a successful response,
  1575  	// we know the access token is legit. We also parse the response as JSON and confirm
  1576  	// it has a githubLogin field that is non-empty (like the Pulumi Service would return).
  1577  	username, organizations, err := client.NewClient(cloudURL, accessToken, cmdutil.Diag()).GetPulumiAccountDetails(ctx)
  1578  	if err != nil {
  1579  		if errResp, ok := err.(*apitype.ErrorResponse); ok && errResp.Code == 401 {
  1580  			return false, "", nil, nil
  1581  		}
  1582  		return false, "", nil, fmt.Errorf("getting user info from %v: %w", cloudURL, err)
  1583  	}
  1584  
  1585  	return true, username, organizations, nil
  1586  }
  1587  
  1588  // UpdateStackTags updates the stacks's tags, replacing all existing tags.
  1589  func (b *cloudBackend) UpdateStackTags(ctx context.Context,
  1590  	stack backend.Stack, tags map[apitype.StackTagName]string) error {
  1591  
  1592  	stackID, err := b.getCloudStackIdentifier(stack.Ref())
  1593  	if err != nil {
  1594  		return err
  1595  	}
  1596  
  1597  	return b.client.UpdateStackTags(ctx, stackID, tags)
  1598  }
  1599  
  1600  const pulumiOperationHeader = "Pulumi operation"
  1601  
  1602  func (b *cloudBackend) RunDeployment(ctx context.Context, stackRef backend.StackReference,
  1603  	req apitype.CreateDeploymentRequest, opts display.Options) error {
  1604  
  1605  	stackID, err := b.getCloudStackIdentifier(stackRef)
  1606  	if err != nil {
  1607  		return err
  1608  	}
  1609  
  1610  	resp, err := b.client.CreateDeployment(ctx, stackID, req)
  1611  	if err != nil {
  1612  		return err
  1613  	}
  1614  	id := resp.ID
  1615  
  1616  	fmt.Printf(opts.Color.Colorize(colors.SpecHeadline + "Preparing deployment..." + colors.Reset + "\n\n"))
  1617  
  1618  	if !opts.SuppressPermalink && !opts.JSONDisplay && resp.ConsoleURL != "" {
  1619  		fmt.Printf(opts.Color.Colorize(
  1620  			colors.SpecHeadline+"View Live: "+
  1621  				colors.Underline+colors.BrightBlue+"%s"+colors.Reset+"\n"), resp.ConsoleURL)
  1622  	}
  1623  
  1624  	token := ""
  1625  	for {
  1626  		logs, err := b.client.GetDeploymentLogs(ctx, stackID, id, token)
  1627  		if err != nil {
  1628  			return err
  1629  		}
  1630  
  1631  		for _, l := range logs.Lines {
  1632  			if l.Header != "" {
  1633  				fmt.Printf(opts.Color.Colorize(
  1634  					"\n" + colors.SpecHeadline + l.Header + ":" + colors.Reset + "\n"))
  1635  
  1636  				// If we see it's a Pulumi operation, rather than outputting the deployment logs,
  1637  				// find the associated update and show the normal rendering of the operation's events.
  1638  				if l.Header == pulumiOperationHeader {
  1639  					fmt.Println()
  1640  					return b.showDeploymentEvents(ctx, stackID, apitype.UpdateKind(req.Operation.Operation), id, opts)
  1641  				}
  1642  			} else {
  1643  				fmt.Print(l.Line)
  1644  			}
  1645  		}
  1646  
  1647  		// If there are no more logs for the deployment and the deployment has finished or we're not following,
  1648  		// then we're done.
  1649  		if logs.NextToken == "" {
  1650  			break
  1651  		}
  1652  
  1653  		// Otherwise, update the token, sleep, and loop around.
  1654  		if logs.NextToken == token {
  1655  			time.Sleep(500 * time.Millisecond)
  1656  		}
  1657  		token = logs.NextToken
  1658  	}
  1659  
  1660  	return nil
  1661  }
  1662  
  1663  func (b *cloudBackend) showDeploymentEvents(ctx context.Context, stackID client.StackIdentifier,
  1664  	kind apitype.UpdateKind, deploymentID string, opts display.Options) error {
  1665  
  1666  	getUpdateID := func() (string, error) {
  1667  		for tries := 0; tries < 10; tries++ {
  1668  			updates, err := b.client.GetDeploymentUpdates(ctx, stackID, deploymentID)
  1669  			if err != nil {
  1670  				return "", err
  1671  			}
  1672  			if len(updates) > 0 {
  1673  				return updates[0].UpdateID, nil
  1674  			}
  1675  
  1676  			time.Sleep(500 * time.Millisecond)
  1677  		}
  1678  		return "", fmt.Errorf("could not find update associated with deployment %s", deploymentID)
  1679  	}
  1680  
  1681  	updateID, err := getUpdateID()
  1682  	if err != nil {
  1683  		return err
  1684  	}
  1685  
  1686  	dryRun := kind == apitype.PreviewUpdate
  1687  	update := client.UpdateIdentifier{
  1688  		StackIdentifier: stackID,
  1689  		UpdateKind:      kind,
  1690  		UpdateID:        updateID,
  1691  	}
  1692  
  1693  	events := make(chan engine.Event) // Note: unbuffered, but we assume it won't matter in practice.
  1694  	done := make(chan bool)
  1695  
  1696  	// Timings do not display correctly when rendering remote events, so suppress showing them.
  1697  	opts.SuppressTimings = true
  1698  
  1699  	go display.ShowEvents(
  1700  		backend.ActionLabel(kind, dryRun), kind, tokens.Name(stackID.Stack), tokens.PackageName(stackID.Project),
  1701  		events, done, opts, dryRun)
  1702  
  1703  	// The UpdateEvents API returns a continuation token to only get events after the previous call.
  1704  	var continuationToken *string
  1705  	var lastEvent engine.Event
  1706  	for {
  1707  		resp, err := b.client.GetUpdateEngineEvents(ctx, update, continuationToken)
  1708  		if err != nil {
  1709  			return err
  1710  		}
  1711  		for _, jsonEvent := range resp.Events {
  1712  			event, err := display.ConvertJSONEvent(jsonEvent)
  1713  			if err != nil {
  1714  				return err
  1715  			}
  1716  			lastEvent = event
  1717  			events <- event
  1718  		}
  1719  
  1720  		continuationToken = resp.ContinuationToken
  1721  		// A nil continuation token means there are no more events to read and the update has finished.
  1722  		if continuationToken == nil {
  1723  			// If the event stream does not terminate with a cancel event, synthesize one here.
  1724  			if lastEvent.Type != engine.CancelEvent {
  1725  				events <- engine.NewEvent(engine.CancelEvent, nil)
  1726  			}
  1727  
  1728  			close(events)
  1729  			<-done
  1730  			return nil
  1731  		}
  1732  
  1733  		time.Sleep(500 * time.Millisecond)
  1734  	}
  1735  }
  1736  
  1737  type httpstateBackendClient struct {
  1738  	backend Backend
  1739  }
  1740  
  1741  func (c httpstateBackendClient) GetStackOutputs(ctx context.Context, name string) (resource.PropertyMap, error) {
  1742  	// When using the cloud backend, require that stack references are fully qualified so they
  1743  	// look like "<org>/<project>/<stack>"
  1744  	if strings.Count(name, "/") != 2 {
  1745  		return nil, fmt.Errorf("a stack reference's name should be of the form " +
  1746  			"'<organization>/<project>/<stack>'. See https://pulumi.io/help/stack-reference for more information.")
  1747  	}
  1748  
  1749  	return backend.NewBackendClient(c.backend).GetStackOutputs(ctx, name)
  1750  }
  1751  
  1752  func (c httpstateBackendClient) GetStackResourceOutputs(
  1753  	ctx context.Context, name string) (resource.PropertyMap, error) {
  1754  	return backend.NewBackendClient(c.backend).GetStackResourceOutputs(ctx, name)
  1755  }