github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/cloud/tfe_client_mock.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package cloud 5 6 import ( 7 "bytes" 8 "context" 9 "encoding/base64" 10 "errors" 11 "fmt" 12 "io" 13 "io/ioutil" 14 "math/rand" 15 "os" 16 "path/filepath" 17 "strings" 18 "sync" 19 "time" 20 21 tfe "github.com/hashicorp/go-tfe" 22 "github.com/mitchellh/copystructure" 23 24 tfversion "github.com/terramate-io/tf/version" 25 ) 26 27 type MockClient struct { 28 Applies *MockApplies 29 ConfigurationVersions *MockConfigurationVersions 30 CostEstimates *MockCostEstimates 31 Organizations *MockOrganizations 32 Plans *MockPlans 33 PolicySetOutcomes *MockPolicySetOutcomes 34 TaskStages *MockTaskStages 35 RedactedPlans *MockRedactedPlans 36 PolicyChecks *MockPolicyChecks 37 Projects *MockProjects 38 Runs *MockRuns 39 RunEvents *MockRunEvents 40 StateVersions *MockStateVersions 41 StateVersionOutputs *MockStateVersionOutputs 42 Variables *MockVariables 43 Workspaces *MockWorkspaces 44 } 45 46 func NewMockClient() *MockClient { 47 c := &MockClient{} 48 c.Applies = newMockApplies(c) 49 c.ConfigurationVersions = newMockConfigurationVersions(c) 50 c.CostEstimates = newMockCostEstimates(c) 51 c.Organizations = newMockOrganizations(c) 52 c.Plans = newMockPlans(c) 53 c.TaskStages = newMockTaskStages(c) 54 c.PolicySetOutcomes = newMockPolicySetOutcomes(c) 55 c.PolicyChecks = newMockPolicyChecks(c) 56 c.Projects = newMockProjects(c) 57 c.Runs = newMockRuns(c) 58 c.RunEvents = newMockRunEvents(c) 59 c.StateVersions = newMockStateVersions(c) 60 c.StateVersionOutputs = newMockStateVersionOutputs(c) 61 c.Variables = newMockVariables(c) 62 c.Workspaces = newMockWorkspaces(c) 63 c.RedactedPlans = newMockRedactedPlans(c) 64 return c 65 } 66 67 type MockApplies struct { 68 client *MockClient 69 applies map[string]*tfe.Apply 70 logs map[string]string 71 } 72 73 func newMockApplies(client *MockClient) *MockApplies { 74 return &MockApplies{ 75 client: client, 76 applies: make(map[string]*tfe.Apply), 77 logs: make(map[string]string), 78 } 79 } 80 81 // create is a helper function to create a mock apply that uses the configured 82 // working directory to find the logfile. 83 func (m *MockApplies) create(cvID, workspaceID string) (*tfe.Apply, error) { 84 c, ok := m.client.ConfigurationVersions.configVersions[cvID] 85 if !ok { 86 return nil, tfe.ErrResourceNotFound 87 } 88 if c.Speculative { 89 // Speculative means its plan-only so we don't create a Apply. 90 return nil, nil 91 } 92 93 id := GenerateID("apply-") 94 url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) 95 96 a := &tfe.Apply{ 97 ID: id, 98 LogReadURL: url, 99 Status: tfe.ApplyPending, 100 } 101 102 w, ok := m.client.Workspaces.workspaceIDs[workspaceID] 103 if !ok { 104 return nil, tfe.ErrResourceNotFound 105 } 106 107 if w.AutoApply { 108 a.Status = tfe.ApplyRunning 109 } 110 111 m.logs[url] = filepath.Join( 112 m.client.ConfigurationVersions.uploadPaths[cvID], 113 w.WorkingDirectory, 114 "apply.log", 115 ) 116 m.applies[a.ID] = a 117 118 return a, nil 119 } 120 121 func (m *MockApplies) Read(ctx context.Context, applyID string) (*tfe.Apply, error) { 122 a, ok := m.applies[applyID] 123 if !ok { 124 return nil, tfe.ErrResourceNotFound 125 } 126 // Together with the mockLogReader this allows testing queued runs. 127 if a.Status == tfe.ApplyRunning { 128 a.Status = tfe.ApplyFinished 129 } 130 return a, nil 131 } 132 133 func (m *MockApplies) Logs(ctx context.Context, applyID string) (io.Reader, error) { 134 a, err := m.Read(ctx, applyID) 135 if err != nil { 136 return nil, err 137 } 138 139 logfile, ok := m.logs[a.LogReadURL] 140 if !ok { 141 return nil, tfe.ErrResourceNotFound 142 } 143 144 if _, err := os.Stat(logfile); os.IsNotExist(err) { 145 return bytes.NewBufferString("logfile does not exist"), nil 146 } 147 148 logs, err := ioutil.ReadFile(logfile) 149 if err != nil { 150 return nil, err 151 } 152 153 done := func() (bool, error) { 154 a, err := m.Read(ctx, applyID) 155 if err != nil { 156 return false, err 157 } 158 if a.Status != tfe.ApplyFinished { 159 return false, nil 160 } 161 return true, nil 162 } 163 164 return &mockLogReader{ 165 done: done, 166 logs: bytes.NewBuffer(logs), 167 }, nil 168 } 169 170 type MockConfigurationVersions struct { 171 client *MockClient 172 configVersions map[string]*tfe.ConfigurationVersion 173 uploadPaths map[string]string 174 uploadURLs map[string]*tfe.ConfigurationVersion 175 } 176 177 func newMockConfigurationVersions(client *MockClient) *MockConfigurationVersions { 178 return &MockConfigurationVersions{ 179 client: client, 180 configVersions: make(map[string]*tfe.ConfigurationVersion), 181 uploadPaths: make(map[string]string), 182 uploadURLs: make(map[string]*tfe.ConfigurationVersion), 183 } 184 } 185 186 func (m *MockConfigurationVersions) List(ctx context.Context, workspaceID string, options *tfe.ConfigurationVersionListOptions) (*tfe.ConfigurationVersionList, error) { 187 cvl := &tfe.ConfigurationVersionList{} 188 for _, cv := range m.configVersions { 189 cvl.Items = append(cvl.Items, cv) 190 } 191 192 cvl.Pagination = &tfe.Pagination{ 193 CurrentPage: 1, 194 NextPage: 1, 195 PreviousPage: 1, 196 TotalPages: 1, 197 TotalCount: len(cvl.Items), 198 } 199 200 return cvl, nil 201 } 202 203 func (m *MockConfigurationVersions) Create(ctx context.Context, workspaceID string, options tfe.ConfigurationVersionCreateOptions) (*tfe.ConfigurationVersion, error) { 204 id := GenerateID("cv-") 205 url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) 206 207 cv := &tfe.ConfigurationVersion{ 208 ID: id, 209 Status: tfe.ConfigurationPending, 210 UploadURL: url, 211 } 212 213 if options.Provisional != nil && *options.Provisional { 214 cv.Provisional = true 215 } 216 217 if options.Speculative != nil && *options.Speculative { 218 cv.Speculative = true 219 } 220 221 m.configVersions[cv.ID] = cv 222 m.uploadURLs[url] = cv 223 224 return cv, nil 225 } 226 227 func (m *MockConfigurationVersions) Read(ctx context.Context, cvID string) (*tfe.ConfigurationVersion, error) { 228 cv, ok := m.configVersions[cvID] 229 if !ok { 230 return nil, tfe.ErrResourceNotFound 231 } 232 return cv, nil 233 } 234 235 func (m *MockConfigurationVersions) ReadWithOptions(ctx context.Context, cvID string, options *tfe.ConfigurationVersionReadOptions) (*tfe.ConfigurationVersion, error) { 236 cv, ok := m.configVersions[cvID] 237 if !ok { 238 return nil, tfe.ErrResourceNotFound 239 } 240 return cv, nil 241 } 242 243 func (m *MockConfigurationVersions) Upload(ctx context.Context, url, path string) error { 244 cv, ok := m.uploadURLs[url] 245 if !ok { 246 return errors.New("404 not found") 247 } 248 m.uploadPaths[cv.ID] = path 249 cv.Status = tfe.ConfigurationUploaded 250 251 return m.UploadTarGzip(ctx, url, nil) 252 } 253 254 func (m *MockConfigurationVersions) UploadTarGzip(ctx context.Context, url string, archive io.Reader) error { 255 return nil 256 } 257 258 func (m *MockConfigurationVersions) Archive(ctx context.Context, cvID string) error { 259 panic("not implemented") 260 } 261 262 func (m *MockConfigurationVersions) Download(ctx context.Context, cvID string) ([]byte, error) { 263 panic("not implemented") 264 } 265 266 type MockCostEstimates struct { 267 client *MockClient 268 Estimations map[string]*tfe.CostEstimate 269 logs map[string]string 270 } 271 272 func newMockCostEstimates(client *MockClient) *MockCostEstimates { 273 return &MockCostEstimates{ 274 client: client, 275 Estimations: make(map[string]*tfe.CostEstimate), 276 logs: make(map[string]string), 277 } 278 } 279 280 // create is a helper function to create a mock cost estimation that uses the 281 // configured working directory to find the logfile. 282 func (m *MockCostEstimates) create(cvID, workspaceID string) (*tfe.CostEstimate, error) { 283 id := GenerateID("ce-") 284 285 ce := &tfe.CostEstimate{ 286 ID: id, 287 MatchedResourcesCount: 1, 288 ResourcesCount: 1, 289 DeltaMonthlyCost: "0.00", 290 ProposedMonthlyCost: "0.00", 291 Status: tfe.CostEstimateFinished, 292 } 293 294 w, ok := m.client.Workspaces.workspaceIDs[workspaceID] 295 if !ok { 296 return nil, tfe.ErrResourceNotFound 297 } 298 299 logfile := filepath.Join( 300 m.client.ConfigurationVersions.uploadPaths[cvID], 301 w.WorkingDirectory, 302 "cost-estimate.log", 303 ) 304 305 if _, err := os.Stat(logfile); os.IsNotExist(err) { 306 return nil, nil 307 } 308 309 m.logs[ce.ID] = logfile 310 m.Estimations[ce.ID] = ce 311 312 return ce, nil 313 } 314 315 func (m *MockCostEstimates) Read(ctx context.Context, costEstimateID string) (*tfe.CostEstimate, error) { 316 ce, ok := m.Estimations[costEstimateID] 317 if !ok { 318 return nil, tfe.ErrResourceNotFound 319 } 320 return ce, nil 321 } 322 323 func (m *MockCostEstimates) Logs(ctx context.Context, costEstimateID string) (io.Reader, error) { 324 ce, ok := m.Estimations[costEstimateID] 325 if !ok { 326 return nil, tfe.ErrResourceNotFound 327 } 328 329 logfile, ok := m.logs[ce.ID] 330 if !ok { 331 return nil, tfe.ErrResourceNotFound 332 } 333 334 if _, err := os.Stat(logfile); os.IsNotExist(err) { 335 return bytes.NewBufferString("logfile does not exist"), nil 336 } 337 338 logs, err := ioutil.ReadFile(logfile) 339 if err != nil { 340 return nil, err 341 } 342 343 ce.Status = tfe.CostEstimateFinished 344 345 return bytes.NewBuffer(logs), nil 346 } 347 348 type MockOrganizations struct { 349 client *MockClient 350 organizations map[string]*tfe.Organization 351 } 352 353 func newMockOrganizations(client *MockClient) *MockOrganizations { 354 return &MockOrganizations{ 355 client: client, 356 organizations: make(map[string]*tfe.Organization), 357 } 358 } 359 360 func (m *MockOrganizations) List(ctx context.Context, options *tfe.OrganizationListOptions) (*tfe.OrganizationList, error) { 361 orgl := &tfe.OrganizationList{} 362 for _, org := range m.organizations { 363 orgl.Items = append(orgl.Items, org) 364 } 365 366 orgl.Pagination = &tfe.Pagination{ 367 CurrentPage: 1, 368 NextPage: 1, 369 PreviousPage: 1, 370 TotalPages: 1, 371 TotalCount: len(orgl.Items), 372 } 373 374 return orgl, nil 375 } 376 377 // mockLogReader is a mock logreader that enables testing queued runs. 378 type mockLogReader struct { 379 done func() (bool, error) 380 logs *bytes.Buffer 381 } 382 383 func (m *mockLogReader) Read(l []byte) (int, error) { 384 for { 385 if written, err := m.read(l); err != io.ErrNoProgress { 386 return written, err 387 } 388 time.Sleep(1 * time.Millisecond) 389 } 390 } 391 392 func (m *mockLogReader) read(l []byte) (int, error) { 393 done, err := m.done() 394 if err != nil { 395 return 0, err 396 } 397 if !done { 398 return 0, io.ErrNoProgress 399 } 400 return m.logs.Read(l) 401 } 402 403 func (m *MockOrganizations) Create(ctx context.Context, options tfe.OrganizationCreateOptions) (*tfe.Organization, error) { 404 org := &tfe.Organization{Name: *options.Name} 405 m.organizations[org.Name] = org 406 return org, nil 407 } 408 409 func (m *MockOrganizations) Read(ctx context.Context, name string) (*tfe.Organization, error) { 410 return m.ReadWithOptions(ctx, name, tfe.OrganizationReadOptions{}) 411 } 412 413 func (m *MockOrganizations) ReadWithOptions(ctx context.Context, name string, options tfe.OrganizationReadOptions) (*tfe.Organization, error) { 414 org, ok := m.organizations[name] 415 if !ok { 416 return nil, tfe.ErrResourceNotFound 417 } 418 return org, nil 419 } 420 421 func (m *MockOrganizations) Update(ctx context.Context, name string, options tfe.OrganizationUpdateOptions) (*tfe.Organization, error) { 422 org, ok := m.organizations[name] 423 if !ok { 424 return nil, tfe.ErrResourceNotFound 425 } 426 org.Name = *options.Name 427 return org, nil 428 429 } 430 431 func (m *MockOrganizations) Delete(ctx context.Context, name string) error { 432 delete(m.organizations, name) 433 return nil 434 } 435 436 func (m *MockOrganizations) ReadCapacity(ctx context.Context, name string) (*tfe.Capacity, error) { 437 var pending, running int 438 for _, r := range m.client.Runs.Runs { 439 if r.Status == tfe.RunPending { 440 pending++ 441 continue 442 } 443 running++ 444 } 445 return &tfe.Capacity{Pending: pending, Running: running}, nil 446 } 447 448 func (m *MockOrganizations) ReadEntitlements(ctx context.Context, name string) (*tfe.Entitlements, error) { 449 return &tfe.Entitlements{ 450 Operations: true, 451 PrivateModuleRegistry: true, 452 Sentinel: true, 453 StateStorage: true, 454 Teams: true, 455 VCSIntegrations: true, 456 }, nil 457 } 458 459 func (m *MockOrganizations) ReadRunQueue(ctx context.Context, name string, options tfe.ReadRunQueueOptions) (*tfe.RunQueue, error) { 460 rq := &tfe.RunQueue{} 461 462 for _, r := range m.client.Runs.Runs { 463 rq.Items = append(rq.Items, r) 464 } 465 466 rq.Pagination = &tfe.Pagination{ 467 CurrentPage: 1, 468 NextPage: 1, 469 PreviousPage: 1, 470 TotalPages: 1, 471 TotalCount: len(rq.Items), 472 } 473 474 return rq, nil 475 } 476 477 type MockRedactedPlans struct { 478 client *MockClient 479 redactedPlans map[string][]byte 480 } 481 482 func newMockRedactedPlans(client *MockClient) *MockRedactedPlans { 483 return &MockRedactedPlans{ 484 client: client, 485 redactedPlans: make(map[string][]byte), 486 } 487 } 488 489 func (m *MockRedactedPlans) create(cvID, workspaceID, planID string) error { 490 w, ok := m.client.Workspaces.workspaceIDs[workspaceID] 491 if !ok { 492 return tfe.ErrResourceNotFound 493 } 494 495 planPath := filepath.Join( 496 m.client.ConfigurationVersions.uploadPaths[cvID], 497 w.WorkingDirectory, 498 "plan-redacted.json", 499 ) 500 501 redactedPlanFile, err := os.Open(planPath) 502 if err != nil { 503 return err 504 } 505 506 raw, err := io.ReadAll(redactedPlanFile) 507 if err != nil { 508 return err 509 } 510 511 m.redactedPlans[planID] = raw 512 513 return nil 514 } 515 516 func (m *MockRedactedPlans) Read(ctx context.Context, hostname, token, planID string) ([]byte, error) { 517 if p, ok := m.redactedPlans[planID]; ok { 518 return p, nil 519 } 520 return nil, tfe.ErrResourceNotFound 521 } 522 523 type MockPlans struct { 524 client *MockClient 525 logs map[string]string 526 planOutputs map[string][]byte 527 plans map[string]*tfe.Plan 528 } 529 530 func newMockPlans(client *MockClient) *MockPlans { 531 return &MockPlans{ 532 client: client, 533 logs: make(map[string]string), 534 planOutputs: make(map[string][]byte), 535 plans: make(map[string]*tfe.Plan), 536 } 537 } 538 539 // create is a helper function to create a mock plan that uses the configured 540 // working directory to find the logfile. 541 func (m *MockPlans) create(cvID, workspaceID string) (*tfe.Plan, error) { 542 id := GenerateID("plan-") 543 url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) 544 545 p := &tfe.Plan{ 546 ID: id, 547 LogReadURL: url, 548 Status: tfe.PlanPending, 549 } 550 551 w, ok := m.client.Workspaces.workspaceIDs[workspaceID] 552 if !ok { 553 return nil, tfe.ErrResourceNotFound 554 } 555 556 m.logs[url] = filepath.Join( 557 m.client.ConfigurationVersions.uploadPaths[cvID], 558 w.WorkingDirectory, 559 "plan.log", 560 ) 561 562 // Try to load unredacted json output, if it exists 563 outputPath := filepath.Join( 564 m.client.ConfigurationVersions.uploadPaths[cvID], 565 w.WorkingDirectory, 566 "plan-unredacted.json", 567 ) 568 if outBytes, err := os.ReadFile(outputPath); err == nil { 569 m.planOutputs[p.ID] = outBytes 570 } 571 572 m.plans[p.ID] = p 573 574 return p, nil 575 } 576 577 func (m *MockPlans) Read(ctx context.Context, planID string) (*tfe.Plan, error) { 578 p, ok := m.plans[planID] 579 if !ok { 580 return nil, tfe.ErrResourceNotFound 581 } 582 // Together with the mockLogReader this allows testing queued runs. 583 if p.Status == tfe.PlanRunning { 584 p.Status = tfe.PlanFinished 585 } 586 return p, nil 587 } 588 589 func (m *MockPlans) Logs(ctx context.Context, planID string) (io.Reader, error) { 590 p, err := m.Read(ctx, planID) 591 if err != nil { 592 return nil, err 593 } 594 595 logfile, ok := m.logs[p.LogReadURL] 596 if !ok { 597 return nil, tfe.ErrResourceNotFound 598 } 599 600 if _, err := os.Stat(logfile); os.IsNotExist(err) { 601 return bytes.NewBufferString("logfile does not exist"), nil 602 } 603 604 logs, err := ioutil.ReadFile(logfile) 605 if err != nil { 606 return nil, err 607 } 608 609 done := func() (bool, error) { 610 p, err := m.Read(ctx, planID) 611 if err != nil { 612 return false, err 613 } 614 if p.Status != tfe.PlanFinished { 615 return false, nil 616 } 617 return true, nil 618 } 619 620 return &mockLogReader{ 621 done: done, 622 logs: bytes.NewBuffer(logs), 623 }, nil 624 } 625 626 func (m *MockPlans) ReadJSONOutput(ctx context.Context, planID string) ([]byte, error) { 627 planOutput, ok := m.planOutputs[planID] 628 if !ok { 629 return nil, tfe.ErrResourceNotFound 630 } 631 632 return planOutput, nil 633 } 634 635 type MockTaskStages struct { 636 client *MockClient 637 } 638 639 func newMockTaskStages(client *MockClient) *MockTaskStages { 640 return &MockTaskStages{ 641 client: client, 642 } 643 } 644 645 func (m *MockTaskStages) Override(ctx context.Context, taskStageID string, options tfe.TaskStageOverrideOptions) (*tfe.TaskStage, error) { 646 switch taskStageID { 647 case "ts-err": 648 return nil, errors.New("test error") 649 650 default: 651 return nil, nil 652 } 653 } 654 655 func (m *MockTaskStages) Read(ctx context.Context, taskStageID string, options *tfe.TaskStageReadOptions) (*tfe.TaskStage, error) { 656 //TODO implement me 657 panic("implement me") 658 } 659 660 func (m *MockTaskStages) List(ctx context.Context, runID string, options *tfe.TaskStageListOptions) (*tfe.TaskStageList, error) { 661 //TODO implement me 662 panic("implement me") 663 } 664 665 type MockPolicySetOutcomes struct { 666 client *MockClient 667 } 668 669 func newMockPolicySetOutcomes(client *MockClient) *MockPolicySetOutcomes { 670 return &MockPolicySetOutcomes{ 671 client: client, 672 } 673 } 674 675 func (m *MockPolicySetOutcomes) List(ctx context.Context, policyEvaluationID string, options *tfe.PolicySetOutcomeListOptions) (*tfe.PolicySetOutcomeList, error) { 676 switch policyEvaluationID { 677 case "pol-pass": 678 return &tfe.PolicySetOutcomeList{ 679 Items: []*tfe.PolicySetOutcome{ 680 { 681 ID: policyEvaluationID, 682 Outcomes: []tfe.Outcome{ 683 { 684 EnforcementLevel: "mandatory", 685 Query: "data.example.rule", 686 Status: "passed", 687 PolicyName: "policy-pass", 688 Description: "This policy will pass", 689 }, 690 }, 691 Overridable: tfe.Bool(true), 692 Error: "", 693 PolicySetName: "policy-set-that-passes", 694 PolicySetDescription: "This policy set will always pass", 695 ResultCount: tfe.PolicyResultCount{ 696 AdvisoryFailed: 0, 697 MandatoryFailed: 0, 698 Passed: 1, 699 Errored: 0, 700 }, 701 }, 702 }, 703 }, nil 704 case "pol-fail": 705 return &tfe.PolicySetOutcomeList{ 706 Items: []*tfe.PolicySetOutcome{ 707 { 708 ID: policyEvaluationID, 709 Outcomes: []tfe.Outcome{ 710 { 711 EnforcementLevel: "mandatory", 712 Query: "data.example.rule", 713 Status: "failed", 714 PolicyName: "policy-fail", 715 Description: "This policy will fail", 716 }, 717 }, 718 Overridable: tfe.Bool(true), 719 Error: "", 720 PolicySetName: "policy-set-that-fails", 721 PolicySetDescription: "This policy set will always fail", 722 ResultCount: tfe.PolicyResultCount{ 723 AdvisoryFailed: 0, 724 MandatoryFailed: 1, 725 Passed: 0, 726 Errored: 0, 727 }, 728 }, 729 }, 730 }, nil 731 732 case "adv-fail": 733 return &tfe.PolicySetOutcomeList{ 734 Items: []*tfe.PolicySetOutcome{ 735 { 736 ID: policyEvaluationID, 737 Outcomes: []tfe.Outcome{ 738 { 739 EnforcementLevel: "advisory", 740 Query: "data.example.rule", 741 Status: "failed", 742 PolicyName: "policy-fail", 743 Description: "This policy will fail", 744 }, 745 }, 746 Overridable: tfe.Bool(true), 747 Error: "", 748 PolicySetName: "policy-set-that-fails", 749 PolicySetDescription: "This policy set will always fail", 750 ResultCount: tfe.PolicyResultCount{ 751 AdvisoryFailed: 1, 752 MandatoryFailed: 0, 753 Passed: 0, 754 Errored: 0, 755 }, 756 }, 757 }, 758 }, nil 759 default: 760 return &tfe.PolicySetOutcomeList{ 761 Items: []*tfe.PolicySetOutcome{ 762 { 763 ID: policyEvaluationID, 764 Outcomes: []tfe.Outcome{ 765 { 766 EnforcementLevel: "mandatory", 767 Query: "data.example.rule", 768 Status: "passed", 769 PolicyName: "policy-pass", 770 Description: "This policy will pass", 771 }, 772 }, 773 Overridable: tfe.Bool(true), 774 Error: "", 775 PolicySetName: "policy-set-that-passes", 776 PolicySetDescription: "This policy set will always pass", 777 ResultCount: tfe.PolicyResultCount{ 778 AdvisoryFailed: 0, 779 MandatoryFailed: 0, 780 Passed: 1, 781 Errored: 0, 782 }, 783 }, 784 }, 785 }, nil 786 } 787 } 788 789 func (m *MockPolicySetOutcomes) Read(ctx context.Context, policySetOutcomeID string) (*tfe.PolicySetOutcome, error) { 790 return nil, nil 791 } 792 793 type MockPolicyChecks struct { 794 client *MockClient 795 checks map[string]*tfe.PolicyCheck 796 logs map[string]string 797 } 798 799 func newMockPolicyChecks(client *MockClient) *MockPolicyChecks { 800 return &MockPolicyChecks{ 801 client: client, 802 checks: make(map[string]*tfe.PolicyCheck), 803 logs: make(map[string]string), 804 } 805 } 806 807 // create is a helper function to create a mock policy check that uses the 808 // configured working directory to find the logfile. 809 func (m *MockPolicyChecks) create(cvID, workspaceID string) (*tfe.PolicyCheck, error) { 810 id := GenerateID("pc-") 811 812 pc := &tfe.PolicyCheck{ 813 ID: id, 814 Actions: &tfe.PolicyActions{}, 815 Permissions: &tfe.PolicyPermissions{}, 816 Scope: tfe.PolicyScopeOrganization, 817 Status: tfe.PolicyPending, 818 } 819 820 w, ok := m.client.Workspaces.workspaceIDs[workspaceID] 821 if !ok { 822 return nil, tfe.ErrResourceNotFound 823 } 824 825 logfile := filepath.Join( 826 m.client.ConfigurationVersions.uploadPaths[cvID], 827 w.WorkingDirectory, 828 "policy.log", 829 ) 830 831 if _, err := os.Stat(logfile); os.IsNotExist(err) { 832 return nil, nil 833 } 834 835 m.logs[pc.ID] = logfile 836 m.checks[pc.ID] = pc 837 838 return pc, nil 839 } 840 841 func (m *MockPolicyChecks) List(ctx context.Context, runID string, options *tfe.PolicyCheckListOptions) (*tfe.PolicyCheckList, error) { 842 _, ok := m.client.Runs.Runs[runID] 843 if !ok { 844 return nil, tfe.ErrResourceNotFound 845 } 846 847 pcl := &tfe.PolicyCheckList{} 848 for _, pc := range m.checks { 849 pcl.Items = append(pcl.Items, pc) 850 } 851 852 pcl.Pagination = &tfe.Pagination{ 853 CurrentPage: 1, 854 NextPage: 1, 855 PreviousPage: 1, 856 TotalPages: 1, 857 TotalCount: len(pcl.Items), 858 } 859 860 return pcl, nil 861 } 862 863 func (m *MockPolicyChecks) Read(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) { 864 pc, ok := m.checks[policyCheckID] 865 if !ok { 866 return nil, tfe.ErrResourceNotFound 867 } 868 869 logfile, ok := m.logs[pc.ID] 870 if !ok { 871 return nil, tfe.ErrResourceNotFound 872 } 873 874 if _, err := os.Stat(logfile); os.IsNotExist(err) { 875 return nil, fmt.Errorf("logfile does not exist") 876 } 877 878 logs, err := ioutil.ReadFile(logfile) 879 if err != nil { 880 return nil, err 881 } 882 883 switch { 884 case bytes.Contains(logs, []byte("Sentinel Result: true")): 885 pc.Status = tfe.PolicyPasses 886 case bytes.Contains(logs, []byte("Sentinel Result: false")): 887 switch { 888 case bytes.Contains(logs, []byte("hard-mandatory")): 889 pc.Status = tfe.PolicyHardFailed 890 case bytes.Contains(logs, []byte("soft-mandatory")): 891 pc.Actions.IsOverridable = true 892 pc.Permissions.CanOverride = true 893 pc.Status = tfe.PolicySoftFailed 894 } 895 default: 896 // As this is an unexpected state, we say the policy errored. 897 pc.Status = tfe.PolicyErrored 898 } 899 900 return pc, nil 901 } 902 903 func (m *MockPolicyChecks) Override(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) { 904 pc, ok := m.checks[policyCheckID] 905 if !ok { 906 return nil, tfe.ErrResourceNotFound 907 } 908 pc.Status = tfe.PolicyOverridden 909 return pc, nil 910 } 911 912 func (m *MockPolicyChecks) Logs(ctx context.Context, policyCheckID string) (io.Reader, error) { 913 pc, ok := m.checks[policyCheckID] 914 if !ok { 915 return nil, tfe.ErrResourceNotFound 916 } 917 918 logfile, ok := m.logs[pc.ID] 919 if !ok { 920 return nil, tfe.ErrResourceNotFound 921 } 922 923 if _, err := os.Stat(logfile); os.IsNotExist(err) { 924 return bytes.NewBufferString("logfile does not exist"), nil 925 } 926 927 logs, err := ioutil.ReadFile(logfile) 928 if err != nil { 929 return nil, err 930 } 931 932 switch { 933 case bytes.Contains(logs, []byte("Sentinel Result: true")): 934 pc.Status = tfe.PolicyPasses 935 case bytes.Contains(logs, []byte("Sentinel Result: false")): 936 switch { 937 case bytes.Contains(logs, []byte("hard-mandatory")): 938 pc.Status = tfe.PolicyHardFailed 939 case bytes.Contains(logs, []byte("soft-mandatory")): 940 pc.Actions.IsOverridable = true 941 pc.Permissions.CanOverride = true 942 pc.Status = tfe.PolicySoftFailed 943 } 944 default: 945 // As this is an unexpected state, we say the policy errored. 946 pc.Status = tfe.PolicyErrored 947 } 948 949 return bytes.NewBuffer(logs), nil 950 } 951 952 type MockProjects struct { 953 client *MockClient 954 projects map[string]*tfe.Project 955 } 956 957 func newMockProjects(client *MockClient) *MockProjects { 958 return &MockProjects{ 959 client: client, 960 projects: make(map[string]*tfe.Project), 961 } 962 } 963 964 func (m *MockProjects) Create(ctx context.Context, organization string, options tfe.ProjectCreateOptions) (*tfe.Project, error) { 965 id := GenerateID("prj-") 966 967 p := &tfe.Project{ 968 ID: id, 969 Name: options.Name, 970 } 971 972 m.projects[p.ID] = p 973 974 return p, nil 975 } 976 977 func (m *MockProjects) List(ctx context.Context, organization string, options *tfe.ProjectListOptions) (*tfe.ProjectList, error) { 978 pl := &tfe.ProjectList{} 979 980 for _, project := range m.projects { 981 pc, err := copystructure.Copy(project) 982 if err != nil { 983 panic(err) 984 } 985 pl.Items = append(pl.Items, pc.(*tfe.Project)) 986 } 987 988 pl.Pagination = &tfe.Pagination{ 989 CurrentPage: 1, 990 NextPage: 1, 991 PreviousPage: 1, 992 TotalPages: 1, 993 TotalCount: len(pl.Items), 994 } 995 996 return pl, nil 997 } 998 999 func (m *MockProjects) Read(ctx context.Context, projectID string) (*tfe.Project, error) { 1000 p, ok := m.projects[projectID] 1001 if !ok { 1002 return nil, tfe.ErrResourceNotFound 1003 } 1004 1005 // we must return a copy for the client 1006 pc, err := copystructure.Copy(p) 1007 if err != nil { 1008 panic(err) 1009 } 1010 1011 return pc.(*tfe.Project), nil 1012 } 1013 1014 func (m *MockProjects) Update(ctx context.Context, projectID string, options tfe.ProjectUpdateOptions) (*tfe.Project, error) { 1015 p, ok := m.projects[projectID] 1016 if !ok { 1017 return nil, tfe.ErrResourceNotFound 1018 } 1019 1020 p.Name = *options.Name 1021 1022 // we must return a copy for the client 1023 pc, err := copystructure.Copy(p) 1024 if err != nil { 1025 panic(err) 1026 } 1027 1028 return pc.(*tfe.Project), nil 1029 } 1030 1031 func (m *MockProjects) Delete(ctx context.Context, projectID string) error { 1032 var p *tfe.Project = nil 1033 for _, p := range m.projects { 1034 if p.ID == projectID { 1035 1036 break 1037 } 1038 } 1039 if p == nil { 1040 return tfe.ErrResourceNotFound 1041 } 1042 1043 delete(m.projects, p.Name) 1044 1045 return nil 1046 } 1047 1048 type MockRuns struct { 1049 sync.Mutex 1050 1051 client *MockClient 1052 Runs map[string]*tfe.Run 1053 workspaces map[string][]*tfe.Run 1054 1055 // If ModifyNewRun is non-nil, the create method will call it just before 1056 // saving a new run in the runs map, so that a calling test can mimic 1057 // side-effects that a real server might apply in certain situations. 1058 ModifyNewRun func(client *MockClient, options tfe.RunCreateOptions, run *tfe.Run) 1059 } 1060 1061 func newMockRuns(client *MockClient) *MockRuns { 1062 return &MockRuns{ 1063 client: client, 1064 Runs: make(map[string]*tfe.Run), 1065 workspaces: make(map[string][]*tfe.Run), 1066 } 1067 } 1068 1069 func (m *MockRuns) List(ctx context.Context, workspaceID string, options *tfe.RunListOptions) (*tfe.RunList, error) { 1070 m.Lock() 1071 defer m.Unlock() 1072 1073 w, ok := m.client.Workspaces.workspaceIDs[workspaceID] 1074 if !ok { 1075 return nil, tfe.ErrResourceNotFound 1076 } 1077 1078 rl := &tfe.RunList{} 1079 for _, run := range m.workspaces[w.ID] { 1080 rc, err := copystructure.Copy(run) 1081 if err != nil { 1082 panic(err) 1083 } 1084 rl.Items = append(rl.Items, rc.(*tfe.Run)) 1085 } 1086 1087 rl.Pagination = &tfe.Pagination{ 1088 CurrentPage: 1, 1089 NextPage: 1, 1090 PreviousPage: 1, 1091 TotalPages: 1, 1092 TotalCount: len(rl.Items), 1093 } 1094 1095 return rl, nil 1096 } 1097 1098 func (m *MockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*tfe.Run, error) { 1099 m.Lock() 1100 defer m.Unlock() 1101 1102 a, err := m.client.Applies.create(options.ConfigurationVersion.ID, options.Workspace.ID) 1103 if err != nil { 1104 return nil, err 1105 } 1106 1107 ce, err := m.client.CostEstimates.create(options.ConfigurationVersion.ID, options.Workspace.ID) 1108 if err != nil { 1109 return nil, err 1110 } 1111 1112 p, err := m.client.Plans.create(options.ConfigurationVersion.ID, options.Workspace.ID) 1113 if err != nil { 1114 return nil, err 1115 } 1116 1117 pc, err := m.client.PolicyChecks.create(options.ConfigurationVersion.ID, options.Workspace.ID) 1118 if err != nil { 1119 return nil, err 1120 } 1121 1122 r := &tfe.Run{ 1123 ID: GenerateID("run-"), 1124 Actions: &tfe.RunActions{IsCancelable: true}, 1125 Apply: a, 1126 CostEstimate: ce, 1127 HasChanges: false, 1128 Permissions: &tfe.RunPermissions{}, 1129 Plan: p, 1130 ReplaceAddrs: options.ReplaceAddrs, 1131 Status: tfe.RunPending, 1132 TargetAddrs: options.TargetAddrs, 1133 AllowConfigGeneration: options.AllowConfigGeneration, 1134 } 1135 1136 if options.Message != nil { 1137 r.Message = *options.Message 1138 } 1139 1140 if pc != nil { 1141 r.PolicyChecks = []*tfe.PolicyCheck{pc} 1142 } 1143 1144 if options.IsDestroy != nil { 1145 r.IsDestroy = *options.IsDestroy 1146 } 1147 1148 if options.Refresh != nil { 1149 r.Refresh = *options.Refresh 1150 } 1151 1152 if options.RefreshOnly != nil { 1153 r.RefreshOnly = *options.RefreshOnly 1154 } 1155 1156 if options.AllowConfigGeneration != nil && *options.AllowConfigGeneration { 1157 r.Plan.GeneratedConfiguration = true 1158 } 1159 1160 w, ok := m.client.Workspaces.workspaceIDs[options.Workspace.ID] 1161 if !ok { 1162 return nil, tfe.ErrResourceNotFound 1163 } 1164 if w.CurrentRun == nil { 1165 w.CurrentRun = r 1166 } 1167 1168 r.Workspace = &tfe.Workspace{ 1169 ID: w.ID, 1170 StructuredRunOutputEnabled: w.StructuredRunOutputEnabled, 1171 TerraformVersion: w.TerraformVersion, 1172 } 1173 1174 if w.StructuredRunOutputEnabled { 1175 err := m.client.RedactedPlans.create(options.ConfigurationVersion.ID, options.Workspace.ID, p.ID) 1176 if err != nil { 1177 return nil, err 1178 } 1179 } 1180 1181 if m.ModifyNewRun != nil { 1182 // caller-provided callback may modify the run in-place to mimic 1183 // side-effects that a real server might take in some situations. 1184 m.ModifyNewRun(m.client, options, r) 1185 } 1186 1187 m.Runs[r.ID] = r 1188 m.workspaces[options.Workspace.ID] = append(m.workspaces[options.Workspace.ID], r) 1189 1190 return r, nil 1191 } 1192 1193 func (m *MockRuns) Read(ctx context.Context, runID string) (*tfe.Run, error) { 1194 return m.ReadWithOptions(ctx, runID, nil) 1195 } 1196 1197 func (m *MockRuns) ReadWithOptions(ctx context.Context, runID string, options *tfe.RunReadOptions) (*tfe.Run, error) { 1198 m.Lock() 1199 defer m.Unlock() 1200 1201 r, ok := m.Runs[runID] 1202 if !ok { 1203 return nil, tfe.ErrResourceNotFound 1204 } 1205 1206 pending := false 1207 for _, r := range m.Runs { 1208 if r.ID != runID && r.Status == tfe.RunPending { 1209 pending = true 1210 break 1211 } 1212 } 1213 1214 if !pending && r.Status == tfe.RunPending { 1215 // Only update the status if there are no other pending runs. 1216 r.Status = tfe.RunPlanning 1217 r.Plan.Status = tfe.PlanRunning 1218 } 1219 1220 logs, _ := ioutil.ReadFile(m.client.Plans.logs[r.Plan.LogReadURL]) 1221 if (r.Status == tfe.RunPlanning || r.Status == tfe.RunPlannedAndSaved) && r.Plan.Status == tfe.PlanFinished { 1222 hasChanges := r.IsDestroy || 1223 bytes.Contains(logs, []byte("1 to add")) || 1224 bytes.Contains(logs, []byte("1 to change")) || 1225 bytes.Contains(logs, []byte("1 to import")) 1226 if hasChanges { 1227 r.Actions.IsCancelable = false 1228 r.Actions.IsConfirmable = true 1229 r.HasChanges = true 1230 r.Plan.HasChanges = true 1231 r.Permissions.CanApply = true 1232 } 1233 1234 hasError := bytes.Contains(logs, []byte("null_resource.foo: 1 error")) || 1235 bytes.Contains(logs, []byte("Error: Unsupported block type")) || 1236 bytes.Contains(logs, []byte("Error: Conflicting configuration arguments")) 1237 if hasError { 1238 r.Actions.IsCancelable = false 1239 r.HasChanges = false 1240 r.Status = tfe.RunErrored 1241 } 1242 } 1243 1244 // we must return a copy for the client 1245 rc, err := copystructure.Copy(r) 1246 if err != nil { 1247 panic(err) 1248 } 1249 r = rc.(*tfe.Run) 1250 1251 // After copying, handle includes... or at least, any includes we're known to rely on. 1252 if options != nil { 1253 for _, n := range options.Include { 1254 switch n { 1255 case tfe.RunWorkspace: 1256 ws, ok := m.client.Workspaces.workspaceIDs[r.Workspace.ID] 1257 if ok { 1258 r.Workspace = ws 1259 } 1260 } 1261 } 1262 } 1263 1264 return r, nil 1265 } 1266 1267 func (m *MockRuns) Apply(ctx context.Context, runID string, options tfe.RunApplyOptions) error { 1268 m.Lock() 1269 defer m.Unlock() 1270 1271 r, ok := m.Runs[runID] 1272 if !ok { 1273 return tfe.ErrResourceNotFound 1274 } 1275 if r.Status != tfe.RunPending { 1276 // Only update the status if the run is not pending anymore. 1277 r.Status = tfe.RunApplying 1278 r.Actions.IsConfirmable = false 1279 r.Apply.Status = tfe.ApplyRunning 1280 } 1281 return nil 1282 } 1283 1284 func (m *MockRuns) Cancel(ctx context.Context, runID string, options tfe.RunCancelOptions) error { 1285 panic("not implemented") 1286 } 1287 1288 func (m *MockRuns) ForceCancel(ctx context.Context, runID string, options tfe.RunForceCancelOptions) error { 1289 panic("not implemented") 1290 } 1291 1292 func (m *MockRuns) ForceExecute(ctx context.Context, runID string) error { 1293 panic("implement me") 1294 } 1295 1296 func (m *MockRuns) Discard(ctx context.Context, runID string, options tfe.RunDiscardOptions) error { 1297 m.Lock() 1298 defer m.Unlock() 1299 1300 r, ok := m.Runs[runID] 1301 if !ok { 1302 return tfe.ErrResourceNotFound 1303 } 1304 r.Status = tfe.RunDiscarded 1305 r.Actions.IsConfirmable = false 1306 return nil 1307 } 1308 1309 type MockRunEvents struct{} 1310 1311 func newMockRunEvents(_ *MockClient) *MockRunEvents { 1312 return &MockRunEvents{} 1313 } 1314 1315 // List all the runs events of the given run. 1316 func (m *MockRunEvents) List(ctx context.Context, runID string, options *tfe.RunEventListOptions) (*tfe.RunEventList, error) { 1317 return &tfe.RunEventList{ 1318 Items: []*tfe.RunEvent{}, 1319 }, nil 1320 } 1321 1322 func (m *MockRunEvents) Read(ctx context.Context, runEventID string) (*tfe.RunEvent, error) { 1323 return m.ReadWithOptions(ctx, runEventID, nil) 1324 } 1325 1326 func (m *MockRunEvents) ReadWithOptions(ctx context.Context, runEventID string, options *tfe.RunEventReadOptions) (*tfe.RunEvent, error) { 1327 return &tfe.RunEvent{ 1328 ID: GenerateID("re-"), 1329 Action: "created", 1330 CreatedAt: time.Now(), 1331 }, nil 1332 } 1333 1334 type MockStateVersions struct { 1335 client *MockClient 1336 states map[string][]byte 1337 stateVersions map[string]*tfe.StateVersion 1338 workspaces map[string][]string 1339 outputStates map[string][]byte 1340 } 1341 1342 func newMockStateVersions(client *MockClient) *MockStateVersions { 1343 return &MockStateVersions{ 1344 client: client, 1345 states: make(map[string][]byte), 1346 stateVersions: make(map[string]*tfe.StateVersion), 1347 workspaces: make(map[string][]string), 1348 outputStates: make(map[string][]byte), 1349 } 1350 } 1351 1352 func (m *MockStateVersions) List(ctx context.Context, options *tfe.StateVersionListOptions) (*tfe.StateVersionList, error) { 1353 svl := &tfe.StateVersionList{} 1354 for _, sv := range m.stateVersions { 1355 svl.Items = append(svl.Items, sv) 1356 } 1357 1358 svl.Pagination = &tfe.Pagination{ 1359 CurrentPage: 1, 1360 NextPage: 1, 1361 PreviousPage: 1, 1362 TotalPages: 1, 1363 TotalCount: len(svl.Items), 1364 } 1365 1366 return svl, nil 1367 } 1368 1369 func (m *MockStateVersions) Create(ctx context.Context, workspaceID string, options tfe.StateVersionCreateOptions) (*tfe.StateVersion, error) { 1370 id := GenerateID("sv-") 1371 runID := os.Getenv("TFE_RUN_ID") 1372 url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) 1373 1374 if runID != "" && (options.Run == nil || runID != options.Run.ID) { 1375 return nil, fmt.Errorf("option.Run.ID does not contain the ID exported by TFE_RUN_ID") 1376 } 1377 1378 sv := &tfe.StateVersion{ 1379 ID: id, 1380 DownloadURL: url, 1381 UploadURL: fmt.Sprintf("/_archivist/upload/%s", id), 1382 Serial: *options.Serial, 1383 } 1384 1385 state, err := base64.StdEncoding.DecodeString(*options.State) 1386 if err != nil { 1387 return nil, err 1388 } 1389 m.states[sv.DownloadURL] = state 1390 m.outputStates[sv.ID] = []byte(*options.JSONStateOutputs) 1391 m.stateVersions[sv.ID] = sv 1392 m.workspaces[workspaceID] = append(m.workspaces[workspaceID], sv.ID) 1393 1394 return sv, nil 1395 } 1396 1397 func (m *MockStateVersions) Upload(ctx context.Context, workspaceID string, options tfe.StateVersionUploadOptions) (*tfe.StateVersion, error) { 1398 createOptions := options.StateVersionCreateOptions 1399 createOptions.State = tfe.String(base64.StdEncoding.EncodeToString(options.RawState)) 1400 1401 return m.Create(ctx, workspaceID, createOptions) 1402 } 1403 1404 func (m *MockStateVersions) Read(ctx context.Context, svID string) (*tfe.StateVersion, error) { 1405 return m.ReadWithOptions(ctx, svID, nil) 1406 } 1407 1408 func (m *MockStateVersions) ReadWithOptions(ctx context.Context, svID string, options *tfe.StateVersionReadOptions) (*tfe.StateVersion, error) { 1409 sv, ok := m.stateVersions[svID] 1410 if !ok { 1411 return nil, tfe.ErrResourceNotFound 1412 } 1413 return sv, nil 1414 } 1415 1416 func (m *MockStateVersions) ReadCurrent(ctx context.Context, workspaceID string) (*tfe.StateVersion, error) { 1417 return m.ReadCurrentWithOptions(ctx, workspaceID, nil) 1418 } 1419 1420 func (m *MockStateVersions) ReadCurrentWithOptions(ctx context.Context, workspaceID string, options *tfe.StateVersionCurrentOptions) (*tfe.StateVersion, error) { 1421 w, ok := m.client.Workspaces.workspaceIDs[workspaceID] 1422 if !ok { 1423 return nil, tfe.ErrResourceNotFound 1424 } 1425 1426 svs, ok := m.workspaces[w.ID] 1427 if !ok || len(svs) == 0 { 1428 return nil, tfe.ErrResourceNotFound 1429 } 1430 1431 sv, ok := m.stateVersions[svs[len(svs)-1]] 1432 if !ok { 1433 return nil, tfe.ErrResourceNotFound 1434 } 1435 1436 return sv, nil 1437 } 1438 1439 func (m *MockStateVersions) Download(ctx context.Context, url string) ([]byte, error) { 1440 state, ok := m.states[url] 1441 if !ok { 1442 return nil, tfe.ErrResourceNotFound 1443 } 1444 return state, nil 1445 } 1446 1447 func (m *MockStateVersions) ListOutputs(ctx context.Context, svID string, options *tfe.StateVersionOutputsListOptions) (*tfe.StateVersionOutputsList, error) { 1448 panic("not implemented") 1449 } 1450 1451 type MockStateVersionOutputs struct { 1452 client *MockClient 1453 outputs map[string]*tfe.StateVersionOutput 1454 } 1455 1456 func newMockStateVersionOutputs(client *MockClient) *MockStateVersionOutputs { 1457 return &MockStateVersionOutputs{ 1458 client: client, 1459 outputs: make(map[string]*tfe.StateVersionOutput), 1460 } 1461 } 1462 1463 // This is a helper function in order to create mocks to be read later 1464 func (m *MockStateVersionOutputs) create(id string, svo *tfe.StateVersionOutput) { 1465 m.outputs[id] = svo 1466 } 1467 1468 func (m *MockStateVersionOutputs) Read(ctx context.Context, outputID string) (*tfe.StateVersionOutput, error) { 1469 result, ok := m.outputs[outputID] 1470 if !ok { 1471 return nil, tfe.ErrResourceNotFound 1472 } 1473 1474 return result, nil 1475 } 1476 1477 func (m *MockStateVersionOutputs) ReadCurrent(ctx context.Context, workspaceID string) (*tfe.StateVersionOutputsList, error) { 1478 svl := &tfe.StateVersionOutputsList{} 1479 for _, sv := range m.outputs { 1480 svl.Items = append(svl.Items, sv) 1481 } 1482 1483 svl.Pagination = &tfe.Pagination{ 1484 CurrentPage: 1, 1485 NextPage: 1, 1486 PreviousPage: 1, 1487 TotalPages: 1, 1488 TotalCount: len(svl.Items), 1489 } 1490 1491 return svl, nil 1492 } 1493 1494 type MockVariables struct { 1495 client *MockClient 1496 workspaces map[string]*tfe.VariableList 1497 } 1498 1499 var _ tfe.Variables = (*MockVariables)(nil) 1500 1501 func newMockVariables(client *MockClient) *MockVariables { 1502 return &MockVariables{ 1503 client: client, 1504 workspaces: make(map[string]*tfe.VariableList), 1505 } 1506 } 1507 1508 func (m *MockVariables) List(ctx context.Context, workspaceID string, options *tfe.VariableListOptions) (*tfe.VariableList, error) { 1509 vl := m.workspaces[workspaceID] 1510 return vl, nil 1511 } 1512 1513 func (m *MockVariables) Create(ctx context.Context, workspaceID string, options tfe.VariableCreateOptions) (*tfe.Variable, error) { 1514 v := &tfe.Variable{ 1515 ID: GenerateID("var-"), 1516 Key: *options.Key, 1517 Category: *options.Category, 1518 } 1519 if options.Value != nil { 1520 v.Value = *options.Value 1521 } 1522 if options.HCL != nil { 1523 v.HCL = *options.HCL 1524 } 1525 if options.Sensitive != nil { 1526 v.Sensitive = *options.Sensitive 1527 } 1528 1529 workspace := workspaceID 1530 1531 if m.workspaces[workspace] == nil { 1532 m.workspaces[workspace] = &tfe.VariableList{} 1533 } 1534 1535 vl := m.workspaces[workspace] 1536 vl.Items = append(vl.Items, v) 1537 1538 return v, nil 1539 } 1540 1541 func (m *MockVariables) Read(ctx context.Context, workspaceID string, variableID string) (*tfe.Variable, error) { 1542 panic("not implemented") 1543 } 1544 1545 func (m *MockVariables) Update(ctx context.Context, workspaceID string, variableID string, options tfe.VariableUpdateOptions) (*tfe.Variable, error) { 1546 panic("not implemented") 1547 } 1548 1549 func (m *MockVariables) Delete(ctx context.Context, workspaceID string, variableID string) error { 1550 panic("not implemented") 1551 } 1552 1553 type MockWorkspaces struct { 1554 client *MockClient 1555 workspaceIDs map[string]*tfe.Workspace 1556 workspaceNames map[string]*tfe.Workspace 1557 } 1558 1559 func newMockWorkspaces(client *MockClient) *MockWorkspaces { 1560 return &MockWorkspaces{ 1561 client: client, 1562 workspaceIDs: make(map[string]*tfe.Workspace), 1563 workspaceNames: make(map[string]*tfe.Workspace), 1564 } 1565 } 1566 1567 func (m *MockWorkspaces) List(ctx context.Context, organization string, options *tfe.WorkspaceListOptions) (*tfe.WorkspaceList, error) { 1568 wl := &tfe.WorkspaceList{} 1569 // Get all the workspaces that match the Search value 1570 searchValue := "" 1571 var ws []*tfe.Workspace 1572 var tags []string 1573 1574 if options != nil { 1575 if len(options.Search) > 0 { 1576 searchValue = options.Search 1577 } 1578 if len(options.Tags) > 0 { 1579 tags = strings.Split(options.Tags, ",") 1580 } 1581 } 1582 1583 for _, w := range m.workspaceIDs { 1584 wTags := make(map[string]struct{}) 1585 for _, wTag := range w.Tags { 1586 wTags[wTag.Name] = struct{}{} 1587 } 1588 1589 if strings.Contains(w.Name, searchValue) { 1590 tagsSatisfied := true 1591 for _, tag := range tags { 1592 if _, ok := wTags[tag]; !ok { 1593 tagsSatisfied = false 1594 } 1595 } 1596 if tagsSatisfied { 1597 ws = append(ws, w) 1598 } 1599 } 1600 } 1601 1602 // Return an empty result if we have no matches. 1603 if len(ws) == 0 { 1604 wl.Pagination = &tfe.Pagination{ 1605 CurrentPage: 1, 1606 } 1607 return wl, nil 1608 } 1609 1610 numPages := (len(ws) / 20) + 1 1611 currentPage := 1 1612 if options != nil { 1613 if options.PageNumber != 0 { 1614 currentPage = options.PageNumber 1615 } 1616 } 1617 previousPage := currentPage - 1 1618 nextPage := currentPage + 1 1619 1620 for i := ((currentPage - 1) * 20); i < ((currentPage-1)*20)+20; i++ { 1621 if i > (len(ws) - 1) { 1622 break 1623 } 1624 wl.Items = append(wl.Items, ws[i]) 1625 } 1626 1627 wl.Pagination = &tfe.Pagination{ 1628 CurrentPage: currentPage, 1629 NextPage: nextPage, 1630 PreviousPage: previousPage, 1631 TotalPages: numPages, 1632 TotalCount: len(wl.Items), 1633 } 1634 1635 return wl, nil 1636 } 1637 1638 func (m *MockWorkspaces) Create(ctx context.Context, organization string, options tfe.WorkspaceCreateOptions) (*tfe.Workspace, error) { 1639 // for TestCloud_setUnavailableTerraformVersion 1640 if *options.Name == "unavailable-terraform-version" && options.TerraformVersion != nil { 1641 return nil, fmt.Errorf("requested Terraform version not available in this TFC instance") 1642 } 1643 if strings.HasSuffix(*options.Name, "no-operations") { 1644 options.Operations = tfe.Bool(false) 1645 options.ExecutionMode = tfe.String("local") 1646 } else if options.Operations == nil { 1647 options.Operations = tfe.Bool(true) 1648 options.ExecutionMode = tfe.String("remote") 1649 } 1650 w := &tfe.Workspace{ 1651 ID: GenerateID("ws-"), 1652 Name: *options.Name, 1653 ExecutionMode: *options.ExecutionMode, 1654 Operations: *options.Operations, 1655 StructuredRunOutputEnabled: false, 1656 Permissions: &tfe.WorkspacePermissions{ 1657 CanQueueApply: true, 1658 CanQueueRun: true, 1659 CanForceDelete: tfe.Bool(true), 1660 }, 1661 Organization: &tfe.Organization{ 1662 Name: organization, 1663 }, 1664 } 1665 if options.Project != nil { 1666 w.Project = options.Project 1667 } 1668 if options.AutoApply != nil { 1669 w.AutoApply = *options.AutoApply 1670 } 1671 if options.VCSRepo != nil { 1672 w.VCSRepo = &tfe.VCSRepo{} 1673 } 1674 1675 if options.TerraformVersion != nil { 1676 w.TerraformVersion = *options.TerraformVersion 1677 } else { 1678 w.TerraformVersion = tfversion.String() 1679 } 1680 1681 var tags []*tfe.Tag 1682 for _, tag := range options.Tags { 1683 tags = append(tags, tag) 1684 w.TagNames = append(w.TagNames, tag.Name) 1685 } 1686 w.Tags = tags 1687 m.workspaceIDs[w.ID] = w 1688 m.workspaceNames[w.Name] = w 1689 return w, nil 1690 } 1691 1692 func (m *MockWorkspaces) Read(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) { 1693 // custom error for TestCloud_plan500 in backend_plan_test.go 1694 if workspace == "network-error" { 1695 return nil, errors.New("I'm a little teacup") 1696 } 1697 1698 w, ok := m.workspaceNames[workspace] 1699 if !ok { 1700 return nil, tfe.ErrResourceNotFound 1701 } 1702 return w, nil 1703 } 1704 1705 func (m *MockWorkspaces) ReadByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { 1706 w, ok := m.workspaceIDs[workspaceID] 1707 if !ok { 1708 return nil, tfe.ErrResourceNotFound 1709 } 1710 return w, nil 1711 } 1712 1713 func (m *MockWorkspaces) ReadWithOptions(ctx context.Context, organization string, workspace string, options *tfe.WorkspaceReadOptions) (*tfe.Workspace, error) { 1714 panic("not implemented") 1715 } 1716 1717 func (m *MockWorkspaces) ReadByIDWithOptions(ctx context.Context, workspaceID string, options *tfe.WorkspaceReadOptions) (*tfe.Workspace, error) { 1718 w, ok := m.workspaceIDs[workspaceID] 1719 if !ok { 1720 return nil, tfe.ErrResourceNotFound 1721 } 1722 return w, nil 1723 } 1724 1725 func (m *MockWorkspaces) Update(ctx context.Context, organization, workspace string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) { 1726 w, ok := m.workspaceNames[workspace] 1727 if !ok { 1728 return nil, tfe.ErrResourceNotFound 1729 } 1730 1731 err := updateMockWorkspaceAttributes(w, options) 1732 if err != nil { 1733 return nil, err 1734 } 1735 1736 delete(m.workspaceNames, workspace) 1737 m.workspaceNames[w.Name] = w 1738 1739 return w, nil 1740 } 1741 1742 func (m *MockWorkspaces) UpdateByID(ctx context.Context, workspaceID string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) { 1743 w, ok := m.workspaceIDs[workspaceID] 1744 if !ok { 1745 return nil, tfe.ErrResourceNotFound 1746 } 1747 1748 originalName := w.Name 1749 err := updateMockWorkspaceAttributes(w, options) 1750 if err != nil { 1751 return nil, err 1752 } 1753 1754 delete(m.workspaceNames, originalName) 1755 m.workspaceNames[w.Name] = w 1756 1757 return w, nil 1758 } 1759 1760 func updateMockWorkspaceAttributes(w *tfe.Workspace, options tfe.WorkspaceUpdateOptions) error { 1761 // for TestCloud_setUnavailableTerraformVersion 1762 if w.Name == "unavailable-terraform-version" && options.TerraformVersion != nil { 1763 return fmt.Errorf("requested Terraform version not available in this TFC instance") 1764 } 1765 1766 if options.Operations != nil { 1767 w.Operations = *options.Operations 1768 } 1769 if options.ExecutionMode != nil { 1770 w.ExecutionMode = *options.ExecutionMode 1771 } 1772 if options.Name != nil { 1773 w.Name = *options.Name 1774 } 1775 if options.TerraformVersion != nil { 1776 w.TerraformVersion = *options.TerraformVersion 1777 } 1778 if options.WorkingDirectory != nil { 1779 w.WorkingDirectory = *options.WorkingDirectory 1780 } 1781 1782 if options.StructuredRunOutputEnabled != nil { 1783 w.StructuredRunOutputEnabled = *options.StructuredRunOutputEnabled 1784 } 1785 1786 return nil 1787 } 1788 1789 func (m *MockWorkspaces) Delete(ctx context.Context, organization, workspace string) error { 1790 if w, ok := m.workspaceNames[workspace]; ok { 1791 delete(m.workspaceIDs, w.ID) 1792 } 1793 delete(m.workspaceNames, workspace) 1794 return nil 1795 } 1796 1797 func (m *MockWorkspaces) DeleteByID(ctx context.Context, workspaceID string) error { 1798 if w, ok := m.workspaceIDs[workspaceID]; ok { 1799 delete(m.workspaceIDs, w.Name) 1800 } 1801 delete(m.workspaceIDs, workspaceID) 1802 return nil 1803 } 1804 1805 func (m *MockWorkspaces) SafeDelete(ctx context.Context, organization, workspace string) error { 1806 w, ok := m.client.Workspaces.workspaceNames[workspace] 1807 1808 if !ok { 1809 return tfe.ErrResourceNotFound 1810 } 1811 1812 if w.Locked { 1813 return errors.New("cannot safe delete locked workspace") 1814 } 1815 1816 if w.ResourceCount > 0 { 1817 return fmt.Errorf("cannot safe delete workspace with %d resources", w.ResourceCount) 1818 } 1819 1820 return m.Delete(ctx, organization, workspace) 1821 } 1822 1823 func (m *MockWorkspaces) SafeDeleteByID(ctx context.Context, workspaceID string) error { 1824 w, ok := m.client.Workspaces.workspaceIDs[workspaceID] 1825 if !ok { 1826 return tfe.ErrResourceNotFound 1827 } 1828 1829 if w.Locked { 1830 return errors.New("cannot safe delete locked workspace") 1831 } 1832 1833 if w.ResourceCount > 0 { 1834 return fmt.Errorf("cannot safe delete workspace with %d resources", w.ResourceCount) 1835 } 1836 1837 return m.DeleteByID(ctx, workspaceID) 1838 } 1839 1840 func (m *MockWorkspaces) RemoveVCSConnection(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) { 1841 w, ok := m.workspaceNames[workspace] 1842 if !ok { 1843 return nil, tfe.ErrResourceNotFound 1844 } 1845 w.VCSRepo = nil 1846 return w, nil 1847 } 1848 1849 func (m *MockWorkspaces) RemoveVCSConnectionByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { 1850 w, ok := m.workspaceIDs[workspaceID] 1851 if !ok { 1852 return nil, tfe.ErrResourceNotFound 1853 } 1854 w.VCSRepo = nil 1855 return w, nil 1856 } 1857 1858 func (m *MockWorkspaces) Lock(ctx context.Context, workspaceID string, options tfe.WorkspaceLockOptions) (*tfe.Workspace, error) { 1859 w, ok := m.workspaceIDs[workspaceID] 1860 if !ok { 1861 return nil, tfe.ErrResourceNotFound 1862 } 1863 if w.Locked { 1864 return nil, tfe.ErrWorkspaceLocked 1865 } 1866 w.Locked = true 1867 return w, nil 1868 } 1869 1870 func (m *MockWorkspaces) Unlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { 1871 w, ok := m.workspaceIDs[workspaceID] 1872 if !ok { 1873 return nil, tfe.ErrResourceNotFound 1874 } 1875 if !w.Locked { 1876 return nil, tfe.ErrWorkspaceNotLocked 1877 } 1878 w.Locked = false 1879 return w, nil 1880 } 1881 1882 func (m *MockWorkspaces) ForceUnlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { 1883 w, ok := m.workspaceIDs[workspaceID] 1884 if !ok { 1885 return nil, tfe.ErrResourceNotFound 1886 } 1887 if !w.Locked { 1888 return nil, tfe.ErrWorkspaceNotLocked 1889 } 1890 w.Locked = false 1891 return w, nil 1892 } 1893 1894 func (m *MockWorkspaces) AssignSSHKey(ctx context.Context, workspaceID string, options tfe.WorkspaceAssignSSHKeyOptions) (*tfe.Workspace, error) { 1895 panic("not implemented") 1896 } 1897 1898 func (m *MockWorkspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { 1899 panic("not implemented") 1900 } 1901 1902 func (m *MockWorkspaces) ListRemoteStateConsumers(ctx context.Context, workspaceID string, options *tfe.RemoteStateConsumersListOptions) (*tfe.WorkspaceList, error) { 1903 panic("not implemented") 1904 } 1905 1906 func (m *MockWorkspaces) AddRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceAddRemoteStateConsumersOptions) error { 1907 panic("not implemented") 1908 } 1909 1910 func (m *MockWorkspaces) RemoveRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceRemoveRemoteStateConsumersOptions) error { 1911 panic("not implemented") 1912 } 1913 1914 func (m *MockWorkspaces) UpdateRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceUpdateRemoteStateConsumersOptions) error { 1915 panic("not implemented") 1916 } 1917 1918 func (m *MockWorkspaces) Readme(ctx context.Context, workspaceID string) (io.Reader, error) { 1919 panic("not implemented") 1920 } 1921 1922 func (m *MockWorkspaces) ListTags(ctx context.Context, workspaceID string, options *tfe.WorkspaceTagListOptions) (*tfe.TagList, error) { 1923 panic("not implemented") 1924 } 1925 1926 func (m *MockWorkspaces) AddTags(ctx context.Context, workspaceID string, options tfe.WorkspaceAddTagsOptions) error { 1927 return nil 1928 } 1929 1930 func (m *MockWorkspaces) RemoveTags(ctx context.Context, workspaceID string, options tfe.WorkspaceRemoveTagsOptions) error { 1931 panic("not implemented") 1932 } 1933 1934 const alphanumeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 1935 1936 func GenerateID(s string) string { 1937 b := make([]byte, 16) 1938 for i := range b { 1939 b[i] = alphanumeric[rand.Intn(len(alphanumeric))] 1940 } 1941 return s + string(b) 1942 }