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 }