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