github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/backend/httpstate/client/client.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 client
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"net/http"
    24  	"path"
    25  	"regexp"
    26  	"strconv"
    27  	"time"
    28  
    29  	"github.com/blang/semver"
    30  	"github.com/opentracing/opentracing-go"
    31  
    32  	"github.com/pulumi/pulumi/pkg/v3/engine"
    33  	"github.com/pulumi/pulumi/pkg/v3/util/validation"
    34  	"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
    35  	"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
    36  	"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
    37  	"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
    38  	"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
    39  	"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
    40  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
    41  	"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
    42  )
    43  
    44  // Client provides a slim wrapper around the Pulumi HTTP/REST API.
    45  type Client struct {
    46  	apiURL   string
    47  	apiToken apiAccessToken
    48  	apiUser  string
    49  	apiOrgs  []string
    50  	diag     diag.Sink
    51  	client   restClient
    52  }
    53  
    54  // newClient creates a new Pulumi API client with the given URL and API token. It is a variable instead of a regular
    55  // function so it can be set to a different implementation at runtime, if necessary.
    56  var newClient = func(apiURL, apiToken string, d diag.Sink) *Client {
    57  	return &Client{
    58  		apiURL:   apiURL,
    59  		apiToken: apiAccessToken(apiToken),
    60  		diag:     d,
    61  		client: &defaultRESTClient{
    62  			client: &defaultHTTPClient{
    63  				client: http.DefaultClient,
    64  			},
    65  		},
    66  	}
    67  }
    68  
    69  // NewClient creates a new Pulumi API client with the given URL and API token.
    70  func NewClient(apiURL, apiToken string, d diag.Sink) *Client {
    71  	return newClient(apiURL, apiToken, d)
    72  }
    73  
    74  // URL returns the URL of the API endpoint this client interacts with
    75  func (pc *Client) URL() string {
    76  	return pc.apiURL
    77  }
    78  
    79  // restCall makes a REST-style request to the Pulumi API using the given method, path, query object, and request
    80  // object. If a response object is provided, the server's response is deserialized into that object.
    81  func (pc *Client) restCall(ctx context.Context, method, path string, queryObj, reqObj, respObj interface{}) error {
    82  	return pc.client.Call(ctx, pc.diag, pc.apiURL, method, path, queryObj, reqObj, respObj, pc.apiToken,
    83  		httpCallOptions{})
    84  }
    85  
    86  // restCall makes a REST-style request to the Pulumi API using the given method, path, query object, and request
    87  // object. If a response object is provided, the server's response is deserialized into that object.
    88  func (pc *Client) restCallWithOptions(ctx context.Context, method, path string, queryObj, reqObj,
    89  	respObj interface{}, opts httpCallOptions) error {
    90  	return pc.client.Call(ctx, pc.diag, pc.apiURL, method, path, queryObj, reqObj, respObj, pc.apiToken, opts)
    91  }
    92  
    93  // updateRESTCall makes a REST-style request to the Pulumi API using the given method, path, query object, and request
    94  // object. The call is authorized with the indicated update token. If a response object is provided, the server's
    95  // response is deserialized into that object.
    96  func (pc *Client) updateRESTCall(ctx context.Context, method, path string, queryObj, reqObj, respObj interface{},
    97  	token updateAccessToken, httpOptions httpCallOptions) error {
    98  
    99  	return pc.client.Call(ctx, pc.diag, pc.apiURL, method, path, queryObj, reqObj, respObj, token, httpOptions)
   100  }
   101  
   102  // getProjectPath returns the API path for the given owner and the given project name joined with path separators
   103  // and appended to the stack root.
   104  func getProjectPath(owner string, projectName string) string {
   105  	return fmt.Sprintf("/api/stacks/%s/%s", owner, projectName)
   106  }
   107  
   108  // getStackPath returns the API path to for the given stack with the given components joined with path separators
   109  // and appended to the stack root.
   110  func getStackPath(stack StackIdentifier, components ...string) string {
   111  	prefix := fmt.Sprintf("/api/stacks/%s/%s/%s", stack.Owner, stack.Project, stack.Stack)
   112  	return path.Join(append([]string{prefix}, components...)...)
   113  }
   114  
   115  // listPolicyGroupsPath returns the path for an API call to the Pulumi service to list the Policy Groups
   116  // in a Pulumi organization.
   117  func listPolicyGroupsPath(orgName string) string {
   118  	return fmt.Sprintf("/api/orgs/%s/policygroups", orgName)
   119  }
   120  
   121  // listPolicyPacksPath returns the path for an API call to the Pulumi service to list the Policy Packs
   122  // in a Pulumi organization.
   123  func listPolicyPacksPath(orgName string) string {
   124  	return fmt.Sprintf("/api/orgs/%s/policypacks", orgName)
   125  }
   126  
   127  // publishPolicyPackPath returns the path for an API call to the Pulumi service to publish a new Policy Pack
   128  // in a Pulumi organization.
   129  func publishPolicyPackPath(orgName string) string {
   130  	return fmt.Sprintf("/api/orgs/%s/policypacks", orgName)
   131  }
   132  
   133  // updatePolicyGroupPath returns the path for an API call to the Pulumi service to update a PolicyGroup
   134  // for a Pulumi organization.
   135  func updatePolicyGroupPath(orgName, policyGroup string) string {
   136  	return fmt.Sprintf(
   137  		"/api/orgs/%s/policygroups/%s", orgName, policyGroup)
   138  }
   139  
   140  // deletePolicyPackPath returns the path for an API call to the Pulumi service to delete
   141  // all versions of a Policy Pack from a Pulumi organization.
   142  func deletePolicyPackPath(orgName, policyPackName string) string {
   143  	return fmt.Sprintf("/api/orgs/%s/policypacks/%s", orgName, policyPackName)
   144  }
   145  
   146  // deletePolicyPackVersionPath returns the path for an API call to the Pulumi service to delete
   147  // a version of a Policy Pack from a Pulumi organization.
   148  func deletePolicyPackVersionPath(orgName, policyPackName, versionTag string) string {
   149  	return fmt.Sprintf(
   150  		"/api/orgs/%s/policypacks/%s/versions/%s", orgName, policyPackName, versionTag)
   151  }
   152  
   153  // publishPolicyPackPublishComplete returns the path for an API call to signal to the Pulumi service
   154  // that a PolicyPack to a Pulumi organization.
   155  func publishPolicyPackPublishComplete(orgName, policyPackName string, versionTag string) string {
   156  	return fmt.Sprintf(
   157  		"/api/orgs/%s/policypacks/%s/versions/%s/complete", orgName, policyPackName, versionTag)
   158  }
   159  
   160  // getPolicyPackConfigSchemaPath returns the API path to retrieve the policy pack configuration schema.
   161  func getPolicyPackConfigSchemaPath(orgName, policyPackName string, versionTag string) string {
   162  	return fmt.Sprintf(
   163  		"/api/orgs/%s/policypacks/%s/versions/%s/schema", orgName, policyPackName, versionTag)
   164  }
   165  
   166  // getUpdatePath returns the API path to for the given stack with the given components joined with path separators
   167  // and appended to the update root.
   168  func getUpdatePath(update UpdateIdentifier, components ...string) string {
   169  	components = append([]string{string(apitype.UpdateUpdate), update.UpdateID}, components...)
   170  	return getStackPath(update.StackIdentifier, components...)
   171  }
   172  
   173  // Copied from https://github.com/pulumi/pulumi-service/blob/master/pkg/apitype/users.go#L7-L16
   174  type serviceUserInfo struct {
   175  	Name        string `json:"name"`
   176  	GitHubLogin string `json:"githubLogin"`
   177  	AvatarURL   string `json:"avatarUrl"`
   178  	Email       string `json:"email,omitempty"`
   179  }
   180  
   181  // Copied from https://github.com/pulumi/pulumi-service/blob/master/pkg/apitype/users.go#L20-L34
   182  type serviceUser struct {
   183  	ID            string            `json:"id"`
   184  	GitHubLogin   string            `json:"githubLogin"`
   185  	Name          string            `json:"name"`
   186  	Email         string            `json:"email"`
   187  	AvatarURL     string            `json:"avatarUrl"`
   188  	Organizations []serviceUserInfo `json:"organizations"`
   189  	Identities    []string          `json:"identities"`
   190  	SiteAdmin     *bool             `json:"siteAdmin,omitempty"`
   191  }
   192  
   193  // GetPulumiAccountName returns the user implied by the API token associated with this client.
   194  func (pc *Client) GetPulumiAccountDetails(ctx context.Context) (string, []string, error) {
   195  	if pc.apiUser == "" {
   196  		resp := serviceUser{}
   197  		if err := pc.restCall(ctx, "GET", "/api/user", nil, nil, &resp); err != nil {
   198  			return "", nil, err
   199  		}
   200  
   201  		if resp.GitHubLogin == "" {
   202  			return "", nil, errors.New("unexpected response from server")
   203  		}
   204  
   205  		pc.apiUser = resp.GitHubLogin
   206  		pc.apiOrgs = make([]string, len(resp.Organizations))
   207  		for i, org := range resp.Organizations {
   208  			if org.GitHubLogin == "" {
   209  				return "", nil, errors.New("unexpected response from server")
   210  			}
   211  
   212  			pc.apiOrgs[i] = org.GitHubLogin
   213  		}
   214  	}
   215  
   216  	return pc.apiUser, pc.apiOrgs, nil
   217  }
   218  
   219  // GetCLIVersionInfo asks the service for information about versions of the CLI (the newest version as well as the
   220  // oldest version before the CLI should warn about an upgrade).
   221  func (pc *Client) GetCLIVersionInfo(ctx context.Context) (semver.Version, semver.Version, error) {
   222  	var versionInfo apitype.CLIVersionResponse
   223  
   224  	if err := pc.restCall(ctx, "GET", "/api/cli/version", nil, nil, &versionInfo); err != nil {
   225  		return semver.Version{}, semver.Version{}, err
   226  	}
   227  
   228  	latestSem, err := semver.ParseTolerant(versionInfo.LatestVersion)
   229  	if err != nil {
   230  		return semver.Version{}, semver.Version{}, err
   231  	}
   232  
   233  	oldestSem, err := semver.ParseTolerant(versionInfo.OldestWithoutWarning)
   234  	if err != nil {
   235  		return semver.Version{}, semver.Version{}, err
   236  	}
   237  
   238  	return latestSem, oldestSem, nil
   239  }
   240  
   241  // ListStacksFilter describes optional filters when listing stacks.
   242  type ListStacksFilter struct {
   243  	Project      *string
   244  	Organization *string
   245  	TagName      *string
   246  	TagValue     *string
   247  }
   248  
   249  // ListStacks lists all stacks the current user has access to, optionally filtered by project.
   250  func (pc *Client) ListStacks(
   251  	ctx context.Context, filter ListStacksFilter, inContToken *string) ([]apitype.StackSummary, *string, error) {
   252  	queryFilter := struct {
   253  		Project           *string `url:"project,omitempty"`
   254  		Organization      *string `url:"organization,omitempty"`
   255  		TagName           *string `url:"tagName,omitempty"`
   256  		TagValue          *string `url:"tagValue,omitempty"`
   257  		ContinuationToken *string `url:"continuationToken,omitempty"`
   258  	}{
   259  		Project:           filter.Project,
   260  		Organization:      filter.Organization,
   261  		TagName:           filter.TagName,
   262  		TagValue:          filter.TagValue,
   263  		ContinuationToken: inContToken,
   264  	}
   265  
   266  	var resp apitype.ListStacksResponse
   267  	if err := pc.restCall(ctx, "GET", "/api/user/stacks", queryFilter, nil, &resp); err != nil {
   268  		return nil, nil, err
   269  	}
   270  
   271  	return resp.Stacks, resp.ContinuationToken, nil
   272  }
   273  
   274  var (
   275  	// ErrNoPreviousDeployment is returned when there isn't a previous deployment.
   276  	ErrNoPreviousDeployment = errors.New("no previous deployment")
   277  )
   278  
   279  type getLatestConfigurationResponse struct {
   280  	Info apitype.UpdateInfo `json:"info,omitempty"`
   281  }
   282  
   283  // GetLatestConfiguration returns the configuration for the latest deployment of a given stack.
   284  func (pc *Client) GetLatestConfiguration(ctx context.Context, stackID StackIdentifier) (config.Map, error) {
   285  	latest := getLatestConfigurationResponse{}
   286  	if err := pc.restCall(ctx, "GET", getStackPath(stackID, "updates", "latest"), nil, nil, &latest); err != nil {
   287  		if restErr, ok := err.(*apitype.ErrorResponse); ok {
   288  			if restErr.Code == http.StatusNotFound {
   289  				return nil, ErrNoPreviousDeployment
   290  			}
   291  		}
   292  
   293  		return nil, err
   294  	}
   295  
   296  	cfg := make(config.Map)
   297  	for k, v := range latest.Info.Config {
   298  		newKey, err := config.ParseKey(k)
   299  		if err != nil {
   300  			return nil, err
   301  		}
   302  		if v.Object {
   303  			if v.Secret {
   304  				cfg[newKey] = config.NewSecureObjectValue(v.String)
   305  			} else {
   306  				cfg[newKey] = config.NewObjectValue(v.String)
   307  			}
   308  		} else {
   309  			if v.Secret {
   310  				cfg[newKey] = config.NewSecureValue(v.String)
   311  			} else {
   312  				cfg[newKey] = config.NewValue(v.String)
   313  			}
   314  		}
   315  	}
   316  
   317  	return cfg, nil
   318  }
   319  
   320  // DoesProjectExist returns true if a project with the given name exists, or false otherwise.
   321  func (pc *Client) DoesProjectExist(ctx context.Context, owner string, projectName string) (bool, error) {
   322  	if err := pc.restCall(ctx, "HEAD", getProjectPath(owner, projectName), nil, nil, nil); err != nil {
   323  		// If this was a 404, return false - project not found.
   324  		if errResp, ok := err.(*apitype.ErrorResponse); ok && errResp.Code == http.StatusNotFound {
   325  			return false, nil
   326  		}
   327  
   328  		return false, err
   329  	}
   330  	return true, nil
   331  }
   332  
   333  // GetStack retrieves the stack with the given name.
   334  func (pc *Client) GetStack(ctx context.Context, stackID StackIdentifier) (apitype.Stack, error) {
   335  	var stack apitype.Stack
   336  	if err := pc.restCall(ctx, "GET", getStackPath(stackID), nil, nil, &stack); err != nil {
   337  		return apitype.Stack{}, err
   338  	}
   339  	return stack, nil
   340  }
   341  
   342  // CreateStack creates a stack with the given cloud and stack name in the scope of the indicated project.
   343  func (pc *Client) CreateStack(
   344  	ctx context.Context, stackID StackIdentifier, tags map[apitype.StackTagName]string) (apitype.Stack, error) {
   345  	// Validate names and tags.
   346  	if err := validation.ValidateStackProperties(stackID.Stack, tags); err != nil {
   347  		return apitype.Stack{}, fmt.Errorf("validating stack properties: %w", err)
   348  	}
   349  
   350  	stack := apitype.Stack{
   351  		StackName:   tokens.QName(stackID.Stack),
   352  		ProjectName: stackID.Project,
   353  		OrgName:     stackID.Owner,
   354  		Tags:        tags,
   355  	}
   356  	createStackReq := apitype.CreateStackRequest{
   357  		StackName: stackID.Stack,
   358  		Tags:      tags,
   359  	}
   360  
   361  	endpoint := fmt.Sprintf("/api/stacks/%s/%s", stackID.Owner, stackID.Project)
   362  	if err := pc.restCall(
   363  		ctx, "POST", endpoint, nil, &createStackReq, nil); err != nil {
   364  		return apitype.Stack{}, err
   365  	}
   366  
   367  	return stack, nil
   368  }
   369  
   370  // DeleteStack deletes the indicated stack. If force is true, the stack is deleted even if it contains resources.
   371  func (pc *Client) DeleteStack(ctx context.Context, stack StackIdentifier, force bool) (bool, error) {
   372  	path := getStackPath(stack)
   373  	queryObj := struct {
   374  		Force bool `url:"force"`
   375  	}{
   376  		Force: force,
   377  	}
   378  
   379  	err := pc.restCall(ctx, "DELETE", path, queryObj, nil, nil)
   380  	return isStackHasResourcesError(err), err
   381  }
   382  
   383  func isStackHasResourcesError(err error) bool {
   384  	if err == nil {
   385  		return false
   386  	}
   387  
   388  	errRsp, ok := err.(*apitype.ErrorResponse)
   389  	if !ok {
   390  		return false
   391  	}
   392  
   393  	return errRsp.Code == 400 && errRsp.Message == "Bad Request: Stack still contains resources."
   394  }
   395  
   396  // EncryptValue encrypts a plaintext value in the context of the indicated stack.
   397  func (pc *Client) EncryptValue(ctx context.Context, stack StackIdentifier, plaintext []byte) ([]byte, error) {
   398  	req := apitype.EncryptValueRequest{Plaintext: plaintext}
   399  	var resp apitype.EncryptValueResponse
   400  	if err := pc.restCall(ctx, "POST", getStackPath(stack, "encrypt"), nil, &req, &resp); err != nil {
   401  		return nil, err
   402  	}
   403  	return resp.Ciphertext, nil
   404  }
   405  
   406  // DecryptValue decrypts a ciphertext value in the context of the indicated stack.
   407  func (pc *Client) DecryptValue(ctx context.Context, stack StackIdentifier, ciphertext []byte) ([]byte, error) {
   408  	req := apitype.DecryptValueRequest{Ciphertext: ciphertext}
   409  	var resp apitype.DecryptValueResponse
   410  	if err := pc.restCall(ctx, "POST", getStackPath(stack, "decrypt"), nil, &req, &resp); err != nil {
   411  		return nil, err
   412  	}
   413  	return resp.Plaintext, nil
   414  }
   415  
   416  func (pc *Client) Log3rdPartySecretsProviderDecryptionEvent(ctx context.Context, stack StackIdentifier,
   417  	secretName string) error {
   418  	req := apitype.Log3rdPartyDecryptionEvent{SecretName: secretName}
   419  	if err := pc.restCall(ctx, "POST", path.Join(getStackPath(stack, "decrypt"), "log-decryption"),
   420  		nil, &req, nil); err != nil {
   421  		return err
   422  	}
   423  	return nil
   424  }
   425  
   426  func (pc *Client) LogBulk3rdPartySecretsProviderDecryptionEvent(ctx context.Context, stack StackIdentifier,
   427  	command string) error {
   428  	req := apitype.Log3rdPartyDecryptionEvent{CommandName: command}
   429  	if err := pc.restCall(ctx, "POST", path.Join(getStackPath(stack, "decrypt"), "log-batch-decryption"), nil,
   430  		&req, nil); err != nil {
   431  		return err
   432  	}
   433  	return nil
   434  }
   435  
   436  // BulkDecryptValue decrypts a ciphertext value in the context of the indicated stack.
   437  func (pc *Client) BulkDecryptValue(ctx context.Context, stack StackIdentifier,
   438  	ciphertexts [][]byte) (map[string][]byte, error) {
   439  	req := apitype.BulkDecryptValueRequest{Ciphertexts: ciphertexts}
   440  	var resp apitype.BulkDecryptValueResponse
   441  	if err := pc.restCallWithOptions(ctx, "POST", getStackPath(stack, "batch-decrypt"), nil, &req, &resp,
   442  		httpCallOptions{GzipCompress: true}); err != nil {
   443  		return nil, err
   444  	}
   445  
   446  	return resp.Plaintexts, nil
   447  }
   448  
   449  // GetStackUpdates returns all updates to the indicated stack.
   450  func (pc *Client) GetStackUpdates(
   451  	ctx context.Context,
   452  	stack StackIdentifier,
   453  	pageSize int,
   454  	page int) ([]apitype.UpdateInfo, error) {
   455  	var response apitype.GetHistoryResponse
   456  	path := getStackPath(stack, "updates")
   457  	if pageSize > 0 {
   458  		if page < 1 {
   459  			page = 1
   460  		}
   461  		path += fmt.Sprintf("?pageSize=%d&page=%d", pageSize, page)
   462  	}
   463  	if err := pc.restCall(ctx, "GET", path, nil, nil, &response); err != nil {
   464  		return nil, err
   465  	}
   466  
   467  	return response.Updates, nil
   468  }
   469  
   470  // ExportStackDeployment exports the indicated stack's deployment as a raw JSON message.
   471  // If version is nil, will export the latest version of the stack.
   472  func (pc *Client) ExportStackDeployment(
   473  	ctx context.Context, stack StackIdentifier, version *int) (apitype.UntypedDeployment, error) {
   474  
   475  	tracingSpan, childCtx := opentracing.StartSpanFromContext(ctx, "ExportStackDeployment")
   476  	defer tracingSpan.Finish()
   477  
   478  	path := getStackPath(stack, "export")
   479  
   480  	// Tack on a specific version as desired.
   481  	if version != nil {
   482  		path += fmt.Sprintf("/%d", *version)
   483  	}
   484  
   485  	var resp apitype.ExportStackResponse
   486  	if err := pc.restCall(childCtx, "GET", path, nil, nil, &resp); err != nil {
   487  		return apitype.UntypedDeployment{}, err
   488  	}
   489  
   490  	return apitype.UntypedDeployment(resp), nil
   491  }
   492  
   493  // ImportStackDeployment imports a new deployment into the indicated stack.
   494  func (pc *Client) ImportStackDeployment(ctx context.Context, stack StackIdentifier,
   495  	deployment *apitype.UntypedDeployment) (UpdateIdentifier, error) {
   496  
   497  	var resp apitype.ImportStackResponse
   498  	if err := pc.restCallWithOptions(ctx, "POST", getStackPath(stack, "import"), nil, deployment, &resp,
   499  		httpCallOptions{GzipCompress: true}); err != nil {
   500  		return UpdateIdentifier{}, err
   501  	}
   502  
   503  	return UpdateIdentifier{
   504  		StackIdentifier: stack,
   505  		UpdateKind:      apitype.UpdateUpdate,
   506  		UpdateID:        resp.UpdateID,
   507  	}, nil
   508  }
   509  
   510  // CreateUpdate creates a new update for the indicated stack with the given kind and assorted options. If the update
   511  // requires that the Pulumi program is uploaded, the provided getContents callback will be invoked to fetch the
   512  // contents of the Pulumi program.
   513  func (pc *Client) CreateUpdate(
   514  	ctx context.Context, kind apitype.UpdateKind, stack StackIdentifier, proj *workspace.Project,
   515  	cfg config.Map, m apitype.UpdateMetadata, opts engine.UpdateOptions,
   516  	dryRun bool) (UpdateIdentifier, []apitype.RequiredPolicy, error) {
   517  
   518  	// First create the update program request.
   519  	wireConfig := make(map[string]apitype.ConfigValue)
   520  	for k, cv := range cfg {
   521  		v, err := cv.Value(config.NopDecrypter)
   522  		contract.AssertNoError(err)
   523  
   524  		wireConfig[k.String()] = apitype.ConfigValue{
   525  			String: v,
   526  			Secret: cv.Secure(),
   527  			Object: cv.Object(),
   528  		}
   529  	}
   530  
   531  	description := ""
   532  	if proj.Description != nil {
   533  		description = *proj.Description
   534  	}
   535  
   536  	updateRequest := apitype.UpdateProgramRequest{
   537  		Name:        string(proj.Name),
   538  		Runtime:     proj.Runtime.Name(),
   539  		Main:        proj.Main,
   540  		Description: description,
   541  		Config:      wireConfig,
   542  		Options: apitype.UpdateOptions{
   543  			LocalPolicyPackPaths: engine.ConvertLocalPolicyPacksToPaths(opts.LocalPolicyPacks),
   544  			Color:                colors.Raw, // force raw colorization, we handle colorization in the CLI
   545  			DryRun:               dryRun,
   546  			Parallel:             opts.Parallel,
   547  			ShowConfig:           false, // This is a legacy option now, the engine will always emit config information
   548  			ShowReplacementSteps: false, // This is a legacy option now, the engine will always emit this information
   549  			ShowSames:            false, // This is a legacy option now, the engine will always emit this information
   550  		},
   551  		Metadata: m,
   552  	}
   553  
   554  	// Create the initial update object.
   555  	var endpoint string
   556  	switch kind {
   557  	case apitype.UpdateUpdate, apitype.ResourceImportUpdate:
   558  		endpoint = "update"
   559  	case apitype.PreviewUpdate:
   560  		endpoint = "preview"
   561  	case apitype.RefreshUpdate:
   562  		endpoint = "refresh"
   563  	case apitype.DestroyUpdate:
   564  		endpoint = "destroy"
   565  	default:
   566  		contract.Failf("Unknown kind: %s", kind)
   567  	}
   568  
   569  	path := getStackPath(stack, endpoint)
   570  	var updateResponse apitype.UpdateProgramResponse
   571  	if err := pc.restCall(ctx, "POST", path, nil, &updateRequest, &updateResponse); err != nil {
   572  		return UpdateIdentifier{}, []apitype.RequiredPolicy{}, err
   573  	}
   574  
   575  	return UpdateIdentifier{
   576  		StackIdentifier: stack,
   577  		UpdateKind:      kind,
   578  		UpdateID:        updateResponse.UpdateID,
   579  	}, updateResponse.RequiredPolicies, nil
   580  }
   581  
   582  // RenameStack renames the provided stack to have the new identifier.
   583  func (pc *Client) RenameStack(ctx context.Context, currentID, newID StackIdentifier) error {
   584  	req := apitype.StackRenameRequest{
   585  		NewName:    newID.Stack,
   586  		NewProject: newID.Project,
   587  	}
   588  	return pc.restCall(ctx, "POST", getStackPath(currentID, "rename"), nil, &req, nil)
   589  }
   590  
   591  // StartUpdate starts the indicated update. It returns the new version of the update's target stack and the token used
   592  // to authenticate operations on the update if any. Replaces the stack's tags with the updated set.
   593  func (pc *Client) StartUpdate(ctx context.Context, update UpdateIdentifier,
   594  	tags map[apitype.StackTagName]string) (int, string, error) {
   595  
   596  	// Validate names and tags.
   597  	if err := validation.ValidateStackProperties(update.StackIdentifier.Stack, tags); err != nil {
   598  		return 0, "", fmt.Errorf("validating stack properties: %w", err)
   599  	}
   600  
   601  	req := apitype.StartUpdateRequest{
   602  		Tags: tags,
   603  	}
   604  
   605  	var resp apitype.StartUpdateResponse
   606  	if err := pc.restCall(ctx, "POST", getUpdatePath(update), nil, req, &resp); err != nil {
   607  		return 0, "", err
   608  	}
   609  
   610  	return resp.Version, resp.Token, nil
   611  }
   612  
   613  // ListPolicyGroups lists all `PolicyGroups` the organization has in the Pulumi service.
   614  func (pc *Client) ListPolicyGroups(ctx context.Context, orgName string, inContToken *string) (
   615  	apitype.ListPolicyGroupsResponse, *string, error) {
   616  	// NOTE: The ListPolicyGroups API on the Pulumi Service is not currently paginated.
   617  	var resp apitype.ListPolicyGroupsResponse
   618  	err := pc.restCall(ctx, "GET", listPolicyGroupsPath(orgName), nil, nil, &resp)
   619  	if err != nil {
   620  		return resp, nil, fmt.Errorf("List Policy Groups failed: %w", err)
   621  	}
   622  	return resp, nil, nil
   623  }
   624  
   625  // ListPolicyPacks lists all `PolicyPack` the organization has in the Pulumi service.
   626  func (pc *Client) ListPolicyPacks(ctx context.Context, orgName string, inContToken *string) (
   627  	apitype.ListPolicyPacksResponse, *string, error) {
   628  	// NOTE: The ListPolicyPacks API on the Pulumi Service is not currently paginated.
   629  	var resp apitype.ListPolicyPacksResponse
   630  	err := pc.restCall(ctx, "GET", listPolicyPacksPath(orgName), nil, nil, &resp)
   631  	if err != nil {
   632  		return resp, nil, fmt.Errorf("List Policy Packs failed: %w", err)
   633  	}
   634  	return resp, nil, nil
   635  }
   636  
   637  // PublishPolicyPack publishes a `PolicyPack` to the Pulumi service. If it successfully publishes
   638  // the Policy Pack, it returns the version of the pack.
   639  func (pc *Client) PublishPolicyPack(ctx context.Context, orgName string,
   640  	analyzerInfo plugin.AnalyzerInfo, dirArchive io.Reader) (string, error) {
   641  
   642  	//
   643  	// Step 1: Send POST containing policy metadata to service. This begins process of creating
   644  	// publishing the PolicyPack.
   645  	//
   646  
   647  	if err := validatePolicyPackVersion(analyzerInfo.Version); err != nil {
   648  		return "", err
   649  	}
   650  
   651  	policies := make([]apitype.Policy, len(analyzerInfo.Policies))
   652  	for i, policy := range analyzerInfo.Policies {
   653  		configSchema, err := convertPolicyConfigSchema(policy.ConfigSchema)
   654  		if err != nil {
   655  			return "", err
   656  		}
   657  
   658  		policies[i] = apitype.Policy{
   659  			Name:             policy.Name,
   660  			DisplayName:      policy.DisplayName,
   661  			Description:      policy.Description,
   662  			EnforcementLevel: policy.EnforcementLevel,
   663  			Message:          policy.Message,
   664  			ConfigSchema:     configSchema,
   665  		}
   666  	}
   667  
   668  	req := apitype.CreatePolicyPackRequest{
   669  		Name:        analyzerInfo.Name,
   670  		DisplayName: analyzerInfo.DisplayName,
   671  		VersionTag:  analyzerInfo.Version,
   672  		Policies:    policies,
   673  	}
   674  
   675  	// Print a publishing message. We have to handle the case where an older version of pulumi/policy
   676  	// is in use, which does not provide  a version tag.
   677  	var versionMsg string
   678  	if analyzerInfo.Version != "" {
   679  		versionMsg = fmt.Sprintf(" - version %s", analyzerInfo.Version)
   680  	}
   681  	fmt.Printf("Publishing %q%s to %q\n", analyzerInfo.Name, versionMsg, orgName)
   682  
   683  	var resp apitype.CreatePolicyPackResponse
   684  	err := pc.restCall(ctx, "POST", publishPolicyPackPath(orgName), nil, req, &resp)
   685  	if err != nil {
   686  		return "", fmt.Errorf("Publish policy pack failed: %w", err)
   687  	}
   688  
   689  	//
   690  	// Step 2: Upload the compressed PolicyPack directory to the pre-signed object storage service URL.
   691  	// The PolicyPack is now published.
   692  	//
   693  
   694  	putReq, err := http.NewRequest(http.MethodPut, resp.UploadURI, dirArchive)
   695  	if err != nil {
   696  		return "", fmt.Errorf("Failed to upload compressed PolicyPack: %w", err)
   697  	}
   698  
   699  	for k, v := range resp.RequiredHeaders {
   700  		putReq.Header.Add(k, v)
   701  	}
   702  
   703  	_, err = http.DefaultClient.Do(putReq)
   704  	if err != nil {
   705  		return "", fmt.Errorf("Failed to upload compressed PolicyPack: %w", err)
   706  	}
   707  
   708  	//
   709  	// Step 3: Signal to the service that the PolicyPack publish operation is complete.
   710  	//
   711  
   712  	// If the version tag is empty, an older version of pulumi/policy is being used and
   713  	// we therefore need to use the version provided by the pulumi service.
   714  	version := analyzerInfo.Version
   715  	if version == "" {
   716  		version = strconv.Itoa(resp.Version)
   717  		fmt.Printf("Published as version %s\n", version)
   718  	}
   719  	err = pc.restCall(ctx, "POST",
   720  		publishPolicyPackPublishComplete(orgName, analyzerInfo.Name, version), nil, nil, nil)
   721  	if err != nil {
   722  		return "", fmt.Errorf("Request to signal completion of the publish operation failed: %w", err)
   723  	}
   724  
   725  	return version, nil
   726  }
   727  
   728  // convertPolicyConfigSchema converts a policy's schema from the analyzer to the apitype.
   729  func convertPolicyConfigSchema(schema *plugin.AnalyzerPolicyConfigSchema) (*apitype.PolicyConfigSchema, error) {
   730  	if schema == nil {
   731  		return nil, nil
   732  	}
   733  	properties := map[string]*json.RawMessage{}
   734  	for k, v := range schema.Properties {
   735  		bytes, err := json.Marshal(v)
   736  		if err != nil {
   737  			return nil, err
   738  		}
   739  		raw := json.RawMessage(bytes)
   740  		properties[k] = &raw
   741  	}
   742  	return &apitype.PolicyConfigSchema{
   743  		Type:       apitype.Object,
   744  		Properties: properties,
   745  		Required:   schema.Required,
   746  	}, nil
   747  }
   748  
   749  // validatePolicyPackVersion validates the version of a Policy Pack. The version may be empty,
   750  // as it is likely an older version of pulumi/policy that does not gather the version.
   751  func validatePolicyPackVersion(s string) error {
   752  	if s == "" {
   753  		return nil
   754  	}
   755  
   756  	policyPackVersionTagRE := regexp.MustCompile("^[a-zA-Z0-9-_.]{1,100}$")
   757  	if !policyPackVersionTagRE.MatchString(s) {
   758  		msg := fmt.Sprintf("invalid version %q - version may only contain alphanumeric, hyphens, or underscores. "+
   759  			"It must also be between 1 and 100 characters long.", s)
   760  		return errors.New(msg)
   761  	}
   762  	return nil
   763  }
   764  
   765  // ApplyPolicyPack enables a `PolicyPack` to the Pulumi organization. If policyGroup is not empty,
   766  // it will enable the PolicyPack on the default PolicyGroup.
   767  func (pc *Client) ApplyPolicyPack(ctx context.Context, orgName, policyGroup,
   768  	policyPackName, versionTag string, policyPackConfig map[string]*json.RawMessage) error {
   769  
   770  	// If a Policy Group was not specified, we use the default Policy Group.
   771  	if policyGroup == "" {
   772  		policyGroup = apitype.DefaultPolicyGroup
   773  	}
   774  
   775  	req := apitype.UpdatePolicyGroupRequest{
   776  		AddPolicyPack: &apitype.PolicyPackMetadata{
   777  			Name:       policyPackName,
   778  			VersionTag: versionTag,
   779  			Config:     policyPackConfig,
   780  		},
   781  	}
   782  
   783  	err := pc.restCall(ctx, http.MethodPatch, updatePolicyGroupPath(orgName, policyGroup), nil, req, nil)
   784  	if err != nil {
   785  		return fmt.Errorf("Enable policy pack failed: %w", err)
   786  	}
   787  	return nil
   788  }
   789  
   790  // GetPolicyPackSchema gets Policy Pack config schema.
   791  func (pc *Client) GetPolicyPackSchema(ctx context.Context, orgName,
   792  	policyPackName, versionTag string) (*apitype.GetPolicyPackConfigSchemaResponse, error) {
   793  	var resp apitype.GetPolicyPackConfigSchemaResponse
   794  	err := pc.restCall(ctx, http.MethodGet,
   795  		getPolicyPackConfigSchemaPath(orgName, policyPackName, versionTag), nil, nil, &resp)
   796  	if err != nil {
   797  		return nil, fmt.Errorf("Retrieving policy pack config schema failed: %w", err)
   798  	}
   799  	return &resp, nil
   800  }
   801  
   802  // DisablePolicyPack disables a `PolicyPack` to the Pulumi organization. If policyGroup is not empty,
   803  // it will disable the PolicyPack on the default PolicyGroup.
   804  func (pc *Client) DisablePolicyPack(ctx context.Context, orgName string, policyGroup string,
   805  	policyPackName, versionTag string) error {
   806  
   807  	// If Policy Group was not specified, use the default Policy Group.
   808  	if policyGroup == "" {
   809  		policyGroup = apitype.DefaultPolicyGroup
   810  	}
   811  
   812  	req := apitype.UpdatePolicyGroupRequest{
   813  		RemovePolicyPack: &apitype.PolicyPackMetadata{
   814  			Name:       policyPackName,
   815  			VersionTag: versionTag,
   816  		},
   817  	}
   818  
   819  	err := pc.restCall(ctx, http.MethodPatch, updatePolicyGroupPath(orgName, policyGroup), nil, req, nil)
   820  	if err != nil {
   821  		return fmt.Errorf("Request to disable policy pack failed: %w", err)
   822  	}
   823  	return nil
   824  }
   825  
   826  // RemovePolicyPack removes all versions of a `PolicyPack` from the Pulumi organization.
   827  func (pc *Client) RemovePolicyPack(ctx context.Context, orgName string, policyPackName string) error {
   828  	path := deletePolicyPackPath(orgName, policyPackName)
   829  	err := pc.restCall(ctx, http.MethodDelete, path, nil, nil, nil)
   830  	if err != nil {
   831  		return fmt.Errorf("Request to remove policy pack failed: %w", err)
   832  	}
   833  	return nil
   834  }
   835  
   836  // RemovePolicyPackByVersion removes a specific version of a `PolicyPack` from
   837  // the Pulumi organization.
   838  func (pc *Client) RemovePolicyPackByVersion(ctx context.Context, orgName string,
   839  	policyPackName string, versionTag string) error {
   840  
   841  	path := deletePolicyPackVersionPath(orgName, policyPackName, versionTag)
   842  	err := pc.restCall(ctx, http.MethodDelete, path, nil, nil, nil)
   843  	if err != nil {
   844  		return fmt.Errorf("Request to remove policy pack failed: %w", err)
   845  	}
   846  	return nil
   847  }
   848  
   849  // DownloadPolicyPack applies a `PolicyPack` to the Pulumi organization.
   850  func (pc *Client) DownloadPolicyPack(ctx context.Context, url string) (io.ReadCloser, error) {
   851  	getS3Req, err := http.NewRequest(http.MethodGet, url, nil)
   852  	if err != nil {
   853  		return nil, fmt.Errorf("Failed to download compressed PolicyPack: %w", err)
   854  	}
   855  
   856  	resp, err := http.DefaultClient.Do(getS3Req)
   857  	if err != nil {
   858  		return nil, fmt.Errorf("Failed to download compressed PolicyPack: %w", err)
   859  	}
   860  
   861  	return resp.Body, nil
   862  }
   863  
   864  // GetUpdateEvents returns all events, taking an optional continuation token from a previous call.
   865  func (pc *Client) GetUpdateEvents(ctx context.Context, update UpdateIdentifier,
   866  	continuationToken *string) (apitype.UpdateResults, error) {
   867  
   868  	path := getUpdatePath(update)
   869  	if continuationToken != nil {
   870  		path += fmt.Sprintf("?continuationToken=%s", *continuationToken)
   871  	}
   872  
   873  	var results apitype.UpdateResults
   874  	if err := pc.restCall(ctx, "GET", path, nil, nil, &results); err != nil {
   875  		return apitype.UpdateResults{}, err
   876  	}
   877  
   878  	return results, nil
   879  }
   880  
   881  // RenewUpdateLease renews the indicated update lease for the given duration.
   882  func (pc *Client) RenewUpdateLease(ctx context.Context, update UpdateIdentifier, token string,
   883  	duration time.Duration) (string, error) {
   884  
   885  	req := apitype.RenewUpdateLeaseRequest{
   886  		Duration: int(duration / time.Second),
   887  	}
   888  	var resp apitype.RenewUpdateLeaseResponse
   889  
   890  	// While renewing a lease uses POST, it is safe to send multiple requests (consider that we do this multiple times
   891  	// during a long running update).  Since we would fail our update operation if we can't renew our lease, we'll retry
   892  	// these POST operations.
   893  	if err := pc.updateRESTCall(ctx, "POST", getUpdatePath(update, "renew_lease"), nil, req, &resp,
   894  		updateAccessToken(token), httpCallOptions{RetryAllMethods: true}); err != nil {
   895  		return "", err
   896  	}
   897  	return resp.Token, nil
   898  }
   899  
   900  // InvalidateUpdateCheckpoint invalidates the checkpoint for the indicated update.
   901  func (pc *Client) InvalidateUpdateCheckpoint(ctx context.Context, update UpdateIdentifier, token string) error {
   902  	req := apitype.PatchUpdateCheckpointRequest{
   903  		IsInvalid: true,
   904  	}
   905  
   906  	// It is safe to retry this PATCH operation, because it is logically idempotent.
   907  	return pc.updateRESTCall(ctx, "PATCH", getUpdatePath(update, "checkpoint"), nil, req, nil,
   908  		updateAccessToken(token), httpCallOptions{RetryAllMethods: true})
   909  }
   910  
   911  // PatchUpdateCheckpoint patches the checkpoint for the indicated update with the given contents.
   912  func (pc *Client) PatchUpdateCheckpoint(ctx context.Context, update UpdateIdentifier, deployment *apitype.DeploymentV3,
   913  	token string) error {
   914  
   915  	rawDeployment, err := json.Marshal(deployment)
   916  	if err != nil {
   917  		return err
   918  	}
   919  
   920  	req := apitype.PatchUpdateCheckpointRequest{
   921  		Version:    3,
   922  		Deployment: rawDeployment,
   923  	}
   924  
   925  	// It is safe to retry this PATCH operation, because it is logically idempotent, since we send the entire
   926  	// deployment instead of a set of changes to apply.
   927  	return pc.updateRESTCall(ctx, "PATCH", getUpdatePath(update, "checkpoint"), nil, req, nil,
   928  		updateAccessToken(token), httpCallOptions{RetryAllMethods: true, GzipCompress: true})
   929  }
   930  
   931  // PatchUpdateCheckpointVerbatim is a variant of PatchUpdateCheckpoint that preserves JSON indentation of the
   932  // UntypedDeployment transferred over the wire.
   933  func (pc *Client) PatchUpdateCheckpointVerbatim(ctx context.Context, update UpdateIdentifier,
   934  	sequenceNumber int, untypedDeploymentBytes json.RawMessage, token string) error {
   935  
   936  	req := apitype.PatchUpdateVerbatimCheckpointRequest{
   937  		Version:           3,
   938  		UntypedDeployment: untypedDeploymentBytes,
   939  		SequenceNumber:    sequenceNumber,
   940  	}
   941  
   942  	reqPayload, err := marshalVerbatimCheckpointRequest(req)
   943  	if err != nil {
   944  		return err
   945  	}
   946  
   947  	// It is safe to retry this PATCH operation, because it is logically idempotent, since we send the entire
   948  	// deployment instead of a set of changes to apply.
   949  	return pc.updateRESTCall(ctx, "PATCH", getUpdatePath(update, "checkpointverbatim"), nil, reqPayload, nil,
   950  		updateAccessToken(token), httpCallOptions{RetryAllMethods: true, GzipCompress: true})
   951  }
   952  
   953  // PatchUpdateCheckpointDelta patches the checkpoint for the indicated update with the given contents, just like
   954  // PatchUpdateCheckpoint. Unlike PatchUpdateCheckpoint, it uses a text diff-based protocol to conserve bandwidth on
   955  // large stack states.
   956  func (pc *Client) PatchUpdateCheckpointDelta(ctx context.Context, update UpdateIdentifier,
   957  	sequenceNumber int, checkpointHash string, deploymentDelta json.RawMessage, token string) error {
   958  
   959  	req := apitype.PatchUpdateCheckpointDeltaRequest{
   960  		Version:         3,
   961  		CheckpointHash:  checkpointHash,
   962  		SequenceNumber:  sequenceNumber,
   963  		DeploymentDelta: deploymentDelta,
   964  	}
   965  
   966  	// It is safe to retry because SequenceNumber serves as an idempotency key.
   967  	return pc.updateRESTCall(ctx, "PATCH", getUpdatePath(update, "checkpointdelta"), nil, req, nil,
   968  		updateAccessToken(token), httpCallOptions{RetryAllMethods: true, GzipCompress: true})
   969  }
   970  
   971  // CancelUpdate cancels the indicated update.
   972  func (pc *Client) CancelUpdate(ctx context.Context, update UpdateIdentifier) error {
   973  
   974  	// It is safe to retry this PATCH operation, because it is logically idempotent.
   975  	return pc.restCallWithOptions(ctx, "POST", getUpdatePath(update, "cancel"), nil, nil, nil,
   976  		httpCallOptions{RetryAllMethods: true})
   977  }
   978  
   979  // CompleteUpdate completes the indicated update with the given status.
   980  func (pc *Client) CompleteUpdate(ctx context.Context, update UpdateIdentifier, status apitype.UpdateStatus,
   981  	token string) error {
   982  
   983  	req := apitype.CompleteUpdateRequest{
   984  		Status: status,
   985  	}
   986  
   987  	// It is safe to retry this PATCH operation, because it is logically idempotent.
   988  	return pc.updateRESTCall(ctx, "POST", getUpdatePath(update, "complete"), nil, req, nil,
   989  		updateAccessToken(token), httpCallOptions{RetryAllMethods: true})
   990  }
   991  
   992  // GetUpdateEngineEvents returns the engine events for an update.
   993  func (pc *Client) GetUpdateEngineEvents(ctx context.Context, update UpdateIdentifier,
   994  	continuationToken *string) (apitype.GetUpdateEventsResponse, error) {
   995  
   996  	path := getUpdatePath(update, "events")
   997  	if continuationToken != nil {
   998  		path += fmt.Sprintf("?continuationToken=%s", *continuationToken)
   999  	}
  1000  
  1001  	var resp apitype.GetUpdateEventsResponse
  1002  	if err := pc.restCall(ctx, "GET", path, nil, nil, &resp); err != nil {
  1003  		return apitype.GetUpdateEventsResponse{}, err
  1004  	}
  1005  
  1006  	return resp, nil
  1007  }
  1008  
  1009  // RecordEngineEvents posts a batch of engine events to the Pulumi service.
  1010  func (pc *Client) RecordEngineEvents(
  1011  	ctx context.Context, update UpdateIdentifier, batch apitype.EngineEventBatch, token string) error {
  1012  	callOpts := httpCallOptions{
  1013  		GzipCompress:    true,
  1014  		RetryAllMethods: true,
  1015  	}
  1016  	return pc.updateRESTCall(
  1017  		ctx, "POST", getUpdatePath(update, "events/batch"),
  1018  		nil, batch, nil,
  1019  		updateAccessToken(token), callOpts)
  1020  }
  1021  
  1022  // UpdateStackTags updates the stacks's tags, replacing all existing tags.
  1023  func (pc *Client) UpdateStackTags(
  1024  	ctx context.Context, stack StackIdentifier, tags map[apitype.StackTagName]string) error {
  1025  
  1026  	// Validate stack tags.
  1027  	if err := validation.ValidateStackTags(tags); err != nil {
  1028  		return err
  1029  	}
  1030  
  1031  	return pc.restCall(ctx, "PATCH", getStackPath(stack, "tags"), nil, tags, nil)
  1032  }
  1033  
  1034  func getDeploymentPath(stack StackIdentifier, components ...string) string {
  1035  	prefix := fmt.Sprintf("/api/preview/%s/%s/%s/deployments", stack.Owner, stack.Project, stack.Stack)
  1036  	return path.Join(append([]string{prefix}, components...)...)
  1037  }
  1038  
  1039  func (pc *Client) CreateDeployment(ctx context.Context, stack StackIdentifier,
  1040  	req apitype.CreateDeploymentRequest) (*apitype.CreateDeploymentResponse, error) {
  1041  
  1042  	var resp apitype.CreateDeploymentResponse
  1043  	err := pc.restCall(ctx, http.MethodPost, getDeploymentPath(stack), nil, req, &resp)
  1044  	if err != nil {
  1045  		return nil, fmt.Errorf("creating deployment failed: %w", err)
  1046  	}
  1047  	return &resp, nil
  1048  }
  1049  
  1050  func (pc *Client) GetDeploymentLogs(ctx context.Context, stack StackIdentifier, id,
  1051  	token string) (*apitype.DeploymentLogs, error) {
  1052  
  1053  	path := getDeploymentPath(stack, id, fmt.Sprintf("logs?continuationToken=%s", token))
  1054  	var resp apitype.DeploymentLogs
  1055  	err := pc.restCall(ctx, http.MethodGet, path, nil, nil, &resp)
  1056  	if err != nil {
  1057  		return nil, fmt.Errorf("getting deployment %s logs failed: %w", id, err)
  1058  	}
  1059  	return &resp, nil
  1060  }
  1061  
  1062  func (pc *Client) GetDeploymentUpdates(ctx context.Context, stack StackIdentifier,
  1063  	id string) ([]apitype.GetDeploymentUpdatesUpdateInfo, error) {
  1064  
  1065  	path := getDeploymentPath(stack, id, "updates")
  1066  	var resp []apitype.GetDeploymentUpdatesUpdateInfo
  1067  	err := pc.restCall(ctx, http.MethodGet, path, nil, nil, &resp)
  1068  	if err != nil {
  1069  		return nil, fmt.Errorf("getting deployment %s updates failed: %w", id, err)
  1070  	}
  1071  	return resp, nil
  1072  }