github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/cloud/tfe_client_mock.go (about) 1 package cloud 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/base64" 7 "errors" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "math/rand" 12 "os" 13 "path/filepath" 14 "strings" 15 "sync" 16 "time" 17 18 tfe "github.com/hashicorp/go-tfe" 19 tfversion "github.com/cycloidio/terraform/version" 20 "github.com/mitchellh/copystructure" 21 ) 22 23 type MockClient struct { 24 Applies *MockApplies 25 ConfigurationVersions *MockConfigurationVersions 26 CostEstimates *MockCostEstimates 27 Organizations *MockOrganizations 28 Plans *MockPlans 29 PolicyChecks *MockPolicyChecks 30 Runs *MockRuns 31 StateVersions *MockStateVersions 32 Variables *MockVariables 33 Workspaces *MockWorkspaces 34 } 35 36 func NewMockClient() *MockClient { 37 c := &MockClient{} 38 c.Applies = newMockApplies(c) 39 c.ConfigurationVersions = newMockConfigurationVersions(c) 40 c.CostEstimates = newMockCostEstimates(c) 41 c.Organizations = newMockOrganizations(c) 42 c.Plans = newMockPlans(c) 43 c.PolicyChecks = newMockPolicyChecks(c) 44 c.Runs = newMockRuns(c) 45 c.StateVersions = newMockStateVersions(c) 46 c.Variables = newMockVariables(c) 47 c.Workspaces = newMockWorkspaces(c) 48 return c 49 } 50 51 type MockApplies struct { 52 client *MockClient 53 applies map[string]*tfe.Apply 54 logs map[string]string 55 } 56 57 func newMockApplies(client *MockClient) *MockApplies { 58 return &MockApplies{ 59 client: client, 60 applies: make(map[string]*tfe.Apply), 61 logs: make(map[string]string), 62 } 63 } 64 65 // create is a helper function to create a mock apply that uses the configured 66 // working directory to find the logfile. 67 func (m *MockApplies) create(cvID, workspaceID string) (*tfe.Apply, error) { 68 c, ok := m.client.ConfigurationVersions.configVersions[cvID] 69 if !ok { 70 return nil, tfe.ErrResourceNotFound 71 } 72 if c.Speculative { 73 // Speculative means its plan-only so we don't create a Apply. 74 return nil, nil 75 } 76 77 id := GenerateID("apply-") 78 url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) 79 80 a := &tfe.Apply{ 81 ID: id, 82 LogReadURL: url, 83 Status: tfe.ApplyPending, 84 } 85 86 w, ok := m.client.Workspaces.workspaceIDs[workspaceID] 87 if !ok { 88 return nil, tfe.ErrResourceNotFound 89 } 90 91 if w.AutoApply { 92 a.Status = tfe.ApplyRunning 93 } 94 95 m.logs[url] = filepath.Join( 96 m.client.ConfigurationVersions.uploadPaths[cvID], 97 w.WorkingDirectory, 98 "apply.log", 99 ) 100 m.applies[a.ID] = a 101 102 return a, nil 103 } 104 105 func (m *MockApplies) Read(ctx context.Context, applyID string) (*tfe.Apply, error) { 106 a, ok := m.applies[applyID] 107 if !ok { 108 return nil, tfe.ErrResourceNotFound 109 } 110 // Together with the mockLogReader this allows testing queued runs. 111 if a.Status == tfe.ApplyRunning { 112 a.Status = tfe.ApplyFinished 113 } 114 return a, nil 115 } 116 117 func (m *MockApplies) Logs(ctx context.Context, applyID string) (io.Reader, error) { 118 a, err := m.Read(ctx, applyID) 119 if err != nil { 120 return nil, err 121 } 122 123 logfile, ok := m.logs[a.LogReadURL] 124 if !ok { 125 return nil, tfe.ErrResourceNotFound 126 } 127 128 if _, err := os.Stat(logfile); os.IsNotExist(err) { 129 return bytes.NewBufferString("logfile does not exist"), nil 130 } 131 132 logs, err := ioutil.ReadFile(logfile) 133 if err != nil { 134 return nil, err 135 } 136 137 done := func() (bool, error) { 138 a, err := m.Read(ctx, applyID) 139 if err != nil { 140 return false, err 141 } 142 if a.Status != tfe.ApplyFinished { 143 return false, nil 144 } 145 return true, nil 146 } 147 148 return &mockLogReader{ 149 done: done, 150 logs: bytes.NewBuffer(logs), 151 }, nil 152 } 153 154 type MockConfigurationVersions struct { 155 client *MockClient 156 configVersions map[string]*tfe.ConfigurationVersion 157 uploadPaths map[string]string 158 uploadURLs map[string]*tfe.ConfigurationVersion 159 } 160 161 func newMockConfigurationVersions(client *MockClient) *MockConfigurationVersions { 162 return &MockConfigurationVersions{ 163 client: client, 164 configVersions: make(map[string]*tfe.ConfigurationVersion), 165 uploadPaths: make(map[string]string), 166 uploadURLs: make(map[string]*tfe.ConfigurationVersion), 167 } 168 } 169 170 func (m *MockConfigurationVersions) List(ctx context.Context, workspaceID string, options tfe.ConfigurationVersionListOptions) (*tfe.ConfigurationVersionList, error) { 171 cvl := &tfe.ConfigurationVersionList{} 172 for _, cv := range m.configVersions { 173 cvl.Items = append(cvl.Items, cv) 174 } 175 176 cvl.Pagination = &tfe.Pagination{ 177 CurrentPage: 1, 178 NextPage: 1, 179 PreviousPage: 1, 180 TotalPages: 1, 181 TotalCount: len(cvl.Items), 182 } 183 184 return cvl, nil 185 } 186 187 func (m *MockConfigurationVersions) Create(ctx context.Context, workspaceID string, options tfe.ConfigurationVersionCreateOptions) (*tfe.ConfigurationVersion, error) { 188 id := GenerateID("cv-") 189 url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) 190 191 cv := &tfe.ConfigurationVersion{ 192 ID: id, 193 Status: tfe.ConfigurationPending, 194 UploadURL: url, 195 } 196 197 m.configVersions[cv.ID] = cv 198 m.uploadURLs[url] = cv 199 200 return cv, nil 201 } 202 203 func (m *MockConfigurationVersions) Read(ctx context.Context, cvID string) (*tfe.ConfigurationVersion, error) { 204 cv, ok := m.configVersions[cvID] 205 if !ok { 206 return nil, tfe.ErrResourceNotFound 207 } 208 return cv, nil 209 } 210 211 func (m *MockConfigurationVersions) ReadWithOptions(ctx context.Context, cvID string, options *tfe.ConfigurationVersionReadOptions) (*tfe.ConfigurationVersion, error) { 212 cv, ok := m.configVersions[cvID] 213 if !ok { 214 return nil, tfe.ErrResourceNotFound 215 } 216 return cv, nil 217 } 218 219 func (m *MockConfigurationVersions) Upload(ctx context.Context, url, path string) error { 220 cv, ok := m.uploadURLs[url] 221 if !ok { 222 return errors.New("404 not found") 223 } 224 m.uploadPaths[cv.ID] = path 225 cv.Status = tfe.ConfigurationUploaded 226 return nil 227 } 228 229 type MockCostEstimates struct { 230 client *MockClient 231 Estimations map[string]*tfe.CostEstimate 232 logs map[string]string 233 } 234 235 func newMockCostEstimates(client *MockClient) *MockCostEstimates { 236 return &MockCostEstimates{ 237 client: client, 238 Estimations: make(map[string]*tfe.CostEstimate), 239 logs: make(map[string]string), 240 } 241 } 242 243 // create is a helper function to create a mock cost estimation that uses the 244 // configured working directory to find the logfile. 245 func (m *MockCostEstimates) create(cvID, workspaceID string) (*tfe.CostEstimate, error) { 246 id := GenerateID("ce-") 247 248 ce := &tfe.CostEstimate{ 249 ID: id, 250 MatchedResourcesCount: 1, 251 ResourcesCount: 1, 252 DeltaMonthlyCost: "0.00", 253 ProposedMonthlyCost: "0.00", 254 Status: tfe.CostEstimateFinished, 255 } 256 257 w, ok := m.client.Workspaces.workspaceIDs[workspaceID] 258 if !ok { 259 return nil, tfe.ErrResourceNotFound 260 } 261 262 logfile := filepath.Join( 263 m.client.ConfigurationVersions.uploadPaths[cvID], 264 w.WorkingDirectory, 265 "cost-estimate.log", 266 ) 267 268 if _, err := os.Stat(logfile); os.IsNotExist(err) { 269 return nil, nil 270 } 271 272 m.logs[ce.ID] = logfile 273 m.Estimations[ce.ID] = ce 274 275 return ce, nil 276 } 277 278 func (m *MockCostEstimates) Read(ctx context.Context, costEstimateID string) (*tfe.CostEstimate, error) { 279 ce, ok := m.Estimations[costEstimateID] 280 if !ok { 281 return nil, tfe.ErrResourceNotFound 282 } 283 return ce, nil 284 } 285 286 func (m *MockCostEstimates) Logs(ctx context.Context, costEstimateID string) (io.Reader, error) { 287 ce, ok := m.Estimations[costEstimateID] 288 if !ok { 289 return nil, tfe.ErrResourceNotFound 290 } 291 292 logfile, ok := m.logs[ce.ID] 293 if !ok { 294 return nil, tfe.ErrResourceNotFound 295 } 296 297 if _, err := os.Stat(logfile); os.IsNotExist(err) { 298 return bytes.NewBufferString("logfile does not exist"), nil 299 } 300 301 logs, err := ioutil.ReadFile(logfile) 302 if err != nil { 303 return nil, err 304 } 305 306 ce.Status = tfe.CostEstimateFinished 307 308 return bytes.NewBuffer(logs), nil 309 } 310 311 type MockOrganizations struct { 312 client *MockClient 313 organizations map[string]*tfe.Organization 314 } 315 316 func newMockOrganizations(client *MockClient) *MockOrganizations { 317 return &MockOrganizations{ 318 client: client, 319 organizations: make(map[string]*tfe.Organization), 320 } 321 } 322 323 func (m *MockOrganizations) List(ctx context.Context, options tfe.OrganizationListOptions) (*tfe.OrganizationList, error) { 324 orgl := &tfe.OrganizationList{} 325 for _, org := range m.organizations { 326 orgl.Items = append(orgl.Items, org) 327 } 328 329 orgl.Pagination = &tfe.Pagination{ 330 CurrentPage: 1, 331 NextPage: 1, 332 PreviousPage: 1, 333 TotalPages: 1, 334 TotalCount: len(orgl.Items), 335 } 336 337 return orgl, nil 338 } 339 340 // mockLogReader is a mock logreader that enables testing queued runs. 341 type mockLogReader struct { 342 done func() (bool, error) 343 logs *bytes.Buffer 344 } 345 346 func (m *mockLogReader) Read(l []byte) (int, error) { 347 for { 348 if written, err := m.read(l); err != io.ErrNoProgress { 349 return written, err 350 } 351 time.Sleep(1 * time.Millisecond) 352 } 353 } 354 355 func (m *mockLogReader) read(l []byte) (int, error) { 356 done, err := m.done() 357 if err != nil { 358 return 0, err 359 } 360 if !done { 361 return 0, io.ErrNoProgress 362 } 363 return m.logs.Read(l) 364 } 365 366 func (m *MockOrganizations) Create(ctx context.Context, options tfe.OrganizationCreateOptions) (*tfe.Organization, error) { 367 org := &tfe.Organization{Name: *options.Name} 368 m.organizations[org.Name] = org 369 return org, nil 370 } 371 372 func (m *MockOrganizations) Read(ctx context.Context, name string) (*tfe.Organization, error) { 373 org, ok := m.organizations[name] 374 if !ok { 375 return nil, tfe.ErrResourceNotFound 376 } 377 return org, nil 378 } 379 380 func (m *MockOrganizations) Update(ctx context.Context, name string, options tfe.OrganizationUpdateOptions) (*tfe.Organization, error) { 381 org, ok := m.organizations[name] 382 if !ok { 383 return nil, tfe.ErrResourceNotFound 384 } 385 org.Name = *options.Name 386 return org, nil 387 388 } 389 390 func (m *MockOrganizations) Delete(ctx context.Context, name string) error { 391 delete(m.organizations, name) 392 return nil 393 } 394 395 func (m *MockOrganizations) Capacity(ctx context.Context, name string) (*tfe.Capacity, error) { 396 var pending, running int 397 for _, r := range m.client.Runs.Runs { 398 if r.Status == tfe.RunPending { 399 pending++ 400 continue 401 } 402 running++ 403 } 404 return &tfe.Capacity{Pending: pending, Running: running}, nil 405 } 406 407 func (m *MockOrganizations) Entitlements(ctx context.Context, name string) (*tfe.Entitlements, error) { 408 return &tfe.Entitlements{ 409 Operations: true, 410 PrivateModuleRegistry: true, 411 Sentinel: true, 412 StateStorage: true, 413 Teams: true, 414 VCSIntegrations: true, 415 }, nil 416 } 417 418 func (m *MockOrganizations) RunQueue(ctx context.Context, name string, options tfe.RunQueueOptions) (*tfe.RunQueue, error) { 419 rq := &tfe.RunQueue{} 420 421 for _, r := range m.client.Runs.Runs { 422 rq.Items = append(rq.Items, r) 423 } 424 425 rq.Pagination = &tfe.Pagination{ 426 CurrentPage: 1, 427 NextPage: 1, 428 PreviousPage: 1, 429 TotalPages: 1, 430 TotalCount: len(rq.Items), 431 } 432 433 return rq, nil 434 } 435 436 type MockPlans struct { 437 client *MockClient 438 logs map[string]string 439 planOutputs map[string]string 440 plans map[string]*tfe.Plan 441 } 442 443 func newMockPlans(client *MockClient) *MockPlans { 444 return &MockPlans{ 445 client: client, 446 logs: make(map[string]string), 447 planOutputs: make(map[string]string), 448 plans: make(map[string]*tfe.Plan), 449 } 450 } 451 452 // create is a helper function to create a mock plan that uses the configured 453 // working directory to find the logfile. 454 func (m *MockPlans) create(cvID, workspaceID string) (*tfe.Plan, error) { 455 id := GenerateID("plan-") 456 url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) 457 458 p := &tfe.Plan{ 459 ID: id, 460 LogReadURL: url, 461 Status: tfe.PlanPending, 462 } 463 464 w, ok := m.client.Workspaces.workspaceIDs[workspaceID] 465 if !ok { 466 return nil, tfe.ErrResourceNotFound 467 } 468 469 m.logs[url] = filepath.Join( 470 m.client.ConfigurationVersions.uploadPaths[cvID], 471 w.WorkingDirectory, 472 "plan.log", 473 ) 474 m.plans[p.ID] = p 475 476 return p, nil 477 } 478 479 func (m *MockPlans) Read(ctx context.Context, planID string) (*tfe.Plan, error) { 480 p, ok := m.plans[planID] 481 if !ok { 482 return nil, tfe.ErrResourceNotFound 483 } 484 // Together with the mockLogReader this allows testing queued runs. 485 if p.Status == tfe.PlanRunning { 486 p.Status = tfe.PlanFinished 487 } 488 return p, nil 489 } 490 491 func (m *MockPlans) Logs(ctx context.Context, planID string) (io.Reader, error) { 492 p, err := m.Read(ctx, planID) 493 if err != nil { 494 return nil, err 495 } 496 497 logfile, ok := m.logs[p.LogReadURL] 498 if !ok { 499 return nil, tfe.ErrResourceNotFound 500 } 501 502 if _, err := os.Stat(logfile); os.IsNotExist(err) { 503 return bytes.NewBufferString("logfile does not exist"), nil 504 } 505 506 logs, err := ioutil.ReadFile(logfile) 507 if err != nil { 508 return nil, err 509 } 510 511 done := func() (bool, error) { 512 p, err := m.Read(ctx, planID) 513 if err != nil { 514 return false, err 515 } 516 if p.Status != tfe.PlanFinished { 517 return false, nil 518 } 519 return true, nil 520 } 521 522 return &mockLogReader{ 523 done: done, 524 logs: bytes.NewBuffer(logs), 525 }, nil 526 } 527 528 func (m *MockPlans) JSONOutput(ctx context.Context, planID string) ([]byte, error) { 529 planOutput, ok := m.planOutputs[planID] 530 if !ok { 531 return nil, tfe.ErrResourceNotFound 532 } 533 534 return []byte(planOutput), nil 535 } 536 537 type MockPolicyChecks struct { 538 client *MockClient 539 checks map[string]*tfe.PolicyCheck 540 logs map[string]string 541 } 542 543 func newMockPolicyChecks(client *MockClient) *MockPolicyChecks { 544 return &MockPolicyChecks{ 545 client: client, 546 checks: make(map[string]*tfe.PolicyCheck), 547 logs: make(map[string]string), 548 } 549 } 550 551 // create is a helper function to create a mock policy check that uses the 552 // configured working directory to find the logfile. 553 func (m *MockPolicyChecks) create(cvID, workspaceID string) (*tfe.PolicyCheck, error) { 554 id := GenerateID("pc-") 555 556 pc := &tfe.PolicyCheck{ 557 ID: id, 558 Actions: &tfe.PolicyActions{}, 559 Permissions: &tfe.PolicyPermissions{}, 560 Scope: tfe.PolicyScopeOrganization, 561 Status: tfe.PolicyPending, 562 } 563 564 w, ok := m.client.Workspaces.workspaceIDs[workspaceID] 565 if !ok { 566 return nil, tfe.ErrResourceNotFound 567 } 568 569 logfile := filepath.Join( 570 m.client.ConfigurationVersions.uploadPaths[cvID], 571 w.WorkingDirectory, 572 "policy.log", 573 ) 574 575 if _, err := os.Stat(logfile); os.IsNotExist(err) { 576 return nil, nil 577 } 578 579 m.logs[pc.ID] = logfile 580 m.checks[pc.ID] = pc 581 582 return pc, nil 583 } 584 585 func (m *MockPolicyChecks) List(ctx context.Context, runID string, options tfe.PolicyCheckListOptions) (*tfe.PolicyCheckList, error) { 586 _, ok := m.client.Runs.Runs[runID] 587 if !ok { 588 return nil, tfe.ErrResourceNotFound 589 } 590 591 pcl := &tfe.PolicyCheckList{} 592 for _, pc := range m.checks { 593 pcl.Items = append(pcl.Items, pc) 594 } 595 596 pcl.Pagination = &tfe.Pagination{ 597 CurrentPage: 1, 598 NextPage: 1, 599 PreviousPage: 1, 600 TotalPages: 1, 601 TotalCount: len(pcl.Items), 602 } 603 604 return pcl, nil 605 } 606 607 func (m *MockPolicyChecks) Read(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) { 608 pc, ok := m.checks[policyCheckID] 609 if !ok { 610 return nil, tfe.ErrResourceNotFound 611 } 612 613 logfile, ok := m.logs[pc.ID] 614 if !ok { 615 return nil, tfe.ErrResourceNotFound 616 } 617 618 if _, err := os.Stat(logfile); os.IsNotExist(err) { 619 return nil, fmt.Errorf("logfile does not exist") 620 } 621 622 logs, err := ioutil.ReadFile(logfile) 623 if err != nil { 624 return nil, err 625 } 626 627 switch { 628 case bytes.Contains(logs, []byte("Sentinel Result: true")): 629 pc.Status = tfe.PolicyPasses 630 case bytes.Contains(logs, []byte("Sentinel Result: false")): 631 switch { 632 case bytes.Contains(logs, []byte("hard-mandatory")): 633 pc.Status = tfe.PolicyHardFailed 634 case bytes.Contains(logs, []byte("soft-mandatory")): 635 pc.Actions.IsOverridable = true 636 pc.Permissions.CanOverride = true 637 pc.Status = tfe.PolicySoftFailed 638 } 639 default: 640 // As this is an unexpected state, we say the policy errored. 641 pc.Status = tfe.PolicyErrored 642 } 643 644 return pc, nil 645 } 646 647 func (m *MockPolicyChecks) Override(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) { 648 pc, ok := m.checks[policyCheckID] 649 if !ok { 650 return nil, tfe.ErrResourceNotFound 651 } 652 pc.Status = tfe.PolicyOverridden 653 return pc, nil 654 } 655 656 func (m *MockPolicyChecks) Logs(ctx context.Context, policyCheckID string) (io.Reader, error) { 657 pc, ok := m.checks[policyCheckID] 658 if !ok { 659 return nil, tfe.ErrResourceNotFound 660 } 661 662 logfile, ok := m.logs[pc.ID] 663 if !ok { 664 return nil, tfe.ErrResourceNotFound 665 } 666 667 if _, err := os.Stat(logfile); os.IsNotExist(err) { 668 return bytes.NewBufferString("logfile does not exist"), nil 669 } 670 671 logs, err := ioutil.ReadFile(logfile) 672 if err != nil { 673 return nil, err 674 } 675 676 switch { 677 case bytes.Contains(logs, []byte("Sentinel Result: true")): 678 pc.Status = tfe.PolicyPasses 679 case bytes.Contains(logs, []byte("Sentinel Result: false")): 680 switch { 681 case bytes.Contains(logs, []byte("hard-mandatory")): 682 pc.Status = tfe.PolicyHardFailed 683 case bytes.Contains(logs, []byte("soft-mandatory")): 684 pc.Actions.IsOverridable = true 685 pc.Permissions.CanOverride = true 686 pc.Status = tfe.PolicySoftFailed 687 } 688 default: 689 // As this is an unexpected state, we say the policy errored. 690 pc.Status = tfe.PolicyErrored 691 } 692 693 return bytes.NewBuffer(logs), nil 694 } 695 696 type MockRuns struct { 697 sync.Mutex 698 699 client *MockClient 700 Runs map[string]*tfe.Run 701 workspaces map[string][]*tfe.Run 702 703 // If ModifyNewRun is non-nil, the create method will call it just before 704 // saving a new run in the runs map, so that a calling test can mimic 705 // side-effects that a real server might apply in certain situations. 706 ModifyNewRun func(client *MockClient, options tfe.RunCreateOptions, run *tfe.Run) 707 } 708 709 func newMockRuns(client *MockClient) *MockRuns { 710 return &MockRuns{ 711 client: client, 712 Runs: make(map[string]*tfe.Run), 713 workspaces: make(map[string][]*tfe.Run), 714 } 715 } 716 717 func (m *MockRuns) List(ctx context.Context, workspaceID string, options tfe.RunListOptions) (*tfe.RunList, error) { 718 m.Lock() 719 defer m.Unlock() 720 721 w, ok := m.client.Workspaces.workspaceIDs[workspaceID] 722 if !ok { 723 return nil, tfe.ErrResourceNotFound 724 } 725 726 rl := &tfe.RunList{} 727 for _, run := range m.workspaces[w.ID] { 728 rc, err := copystructure.Copy(run) 729 if err != nil { 730 panic(err) 731 } 732 rl.Items = append(rl.Items, rc.(*tfe.Run)) 733 } 734 735 rl.Pagination = &tfe.Pagination{ 736 CurrentPage: 1, 737 NextPage: 1, 738 PreviousPage: 1, 739 TotalPages: 1, 740 TotalCount: len(rl.Items), 741 } 742 743 return rl, nil 744 } 745 746 func (m *MockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*tfe.Run, error) { 747 m.Lock() 748 defer m.Unlock() 749 750 a, err := m.client.Applies.create(options.ConfigurationVersion.ID, options.Workspace.ID) 751 if err != nil { 752 return nil, err 753 } 754 755 ce, err := m.client.CostEstimates.create(options.ConfigurationVersion.ID, options.Workspace.ID) 756 if err != nil { 757 return nil, err 758 } 759 760 p, err := m.client.Plans.create(options.ConfigurationVersion.ID, options.Workspace.ID) 761 if err != nil { 762 return nil, err 763 } 764 765 pc, err := m.client.PolicyChecks.create(options.ConfigurationVersion.ID, options.Workspace.ID) 766 if err != nil { 767 return nil, err 768 } 769 770 r := &tfe.Run{ 771 ID: GenerateID("run-"), 772 Actions: &tfe.RunActions{IsCancelable: true}, 773 Apply: a, 774 CostEstimate: ce, 775 HasChanges: false, 776 Permissions: &tfe.RunPermissions{}, 777 Plan: p, 778 ReplaceAddrs: options.ReplaceAddrs, 779 Status: tfe.RunPending, 780 TargetAddrs: options.TargetAddrs, 781 } 782 783 if options.Message != nil { 784 r.Message = *options.Message 785 } 786 787 if pc != nil { 788 r.PolicyChecks = []*tfe.PolicyCheck{pc} 789 } 790 791 if options.IsDestroy != nil { 792 r.IsDestroy = *options.IsDestroy 793 } 794 795 if options.Refresh != nil { 796 r.Refresh = *options.Refresh 797 } 798 799 if options.RefreshOnly != nil { 800 r.RefreshOnly = *options.RefreshOnly 801 } 802 803 w, ok := m.client.Workspaces.workspaceIDs[options.Workspace.ID] 804 if !ok { 805 return nil, tfe.ErrResourceNotFound 806 } 807 if w.CurrentRun == nil { 808 w.CurrentRun = r 809 } 810 811 if m.ModifyNewRun != nil { 812 // caller-provided callback may modify the run in-place to mimic 813 // side-effects that a real server might take in some situations. 814 m.ModifyNewRun(m.client, options, r) 815 } 816 817 m.Runs[r.ID] = r 818 m.workspaces[options.Workspace.ID] = append(m.workspaces[options.Workspace.ID], r) 819 820 return r, nil 821 } 822 823 func (m *MockRuns) Read(ctx context.Context, runID string) (*tfe.Run, error) { 824 return m.ReadWithOptions(ctx, runID, nil) 825 } 826 827 func (m *MockRuns) ReadWithOptions(ctx context.Context, runID string, _ *tfe.RunReadOptions) (*tfe.Run, error) { 828 m.Lock() 829 defer m.Unlock() 830 831 r, ok := m.Runs[runID] 832 if !ok { 833 return nil, tfe.ErrResourceNotFound 834 } 835 836 pending := false 837 for _, r := range m.Runs { 838 if r.ID != runID && r.Status == tfe.RunPending { 839 pending = true 840 break 841 } 842 } 843 844 if !pending && r.Status == tfe.RunPending { 845 // Only update the status if there are no other pending runs. 846 r.Status = tfe.RunPlanning 847 r.Plan.Status = tfe.PlanRunning 848 } 849 850 logs, _ := ioutil.ReadFile(m.client.Plans.logs[r.Plan.LogReadURL]) 851 if r.Status == tfe.RunPlanning && r.Plan.Status == tfe.PlanFinished { 852 if r.IsDestroy || bytes.Contains(logs, []byte("1 to add, 0 to change, 0 to destroy")) { 853 r.Actions.IsCancelable = false 854 r.Actions.IsConfirmable = true 855 r.HasChanges = true 856 r.Permissions.CanApply = true 857 } 858 859 if bytes.Contains(logs, []byte("null_resource.foo: 1 error")) { 860 r.Actions.IsCancelable = false 861 r.HasChanges = false 862 r.Status = tfe.RunErrored 863 } 864 } 865 866 // we must return a copy for the client 867 rc, err := copystructure.Copy(r) 868 if err != nil { 869 panic(err) 870 } 871 872 return rc.(*tfe.Run), nil 873 } 874 875 func (m *MockRuns) Apply(ctx context.Context, runID string, options tfe.RunApplyOptions) error { 876 m.Lock() 877 defer m.Unlock() 878 879 r, ok := m.Runs[runID] 880 if !ok { 881 return tfe.ErrResourceNotFound 882 } 883 if r.Status != tfe.RunPending { 884 // Only update the status if the run is not pending anymore. 885 r.Status = tfe.RunApplying 886 r.Actions.IsConfirmable = false 887 r.Apply.Status = tfe.ApplyRunning 888 } 889 return nil 890 } 891 892 func (m *MockRuns) Cancel(ctx context.Context, runID string, options tfe.RunCancelOptions) error { 893 panic("not implemented") 894 } 895 896 func (m *MockRuns) ForceCancel(ctx context.Context, runID string, options tfe.RunForceCancelOptions) error { 897 panic("not implemented") 898 } 899 900 func (m *MockRuns) Discard(ctx context.Context, runID string, options tfe.RunDiscardOptions) error { 901 m.Lock() 902 defer m.Unlock() 903 904 r, ok := m.Runs[runID] 905 if !ok { 906 return tfe.ErrResourceNotFound 907 } 908 r.Status = tfe.RunDiscarded 909 r.Actions.IsConfirmable = false 910 return nil 911 } 912 913 type MockStateVersions struct { 914 client *MockClient 915 states map[string][]byte 916 stateVersions map[string]*tfe.StateVersion 917 workspaces map[string][]string 918 } 919 920 func newMockStateVersions(client *MockClient) *MockStateVersions { 921 return &MockStateVersions{ 922 client: client, 923 states: make(map[string][]byte), 924 stateVersions: make(map[string]*tfe.StateVersion), 925 workspaces: make(map[string][]string), 926 } 927 } 928 929 func (m *MockStateVersions) List(ctx context.Context, options tfe.StateVersionListOptions) (*tfe.StateVersionList, error) { 930 svl := &tfe.StateVersionList{} 931 for _, sv := range m.stateVersions { 932 svl.Items = append(svl.Items, sv) 933 } 934 935 svl.Pagination = &tfe.Pagination{ 936 CurrentPage: 1, 937 NextPage: 1, 938 PreviousPage: 1, 939 TotalPages: 1, 940 TotalCount: len(svl.Items), 941 } 942 943 return svl, nil 944 } 945 946 func (m *MockStateVersions) Create(ctx context.Context, workspaceID string, options tfe.StateVersionCreateOptions) (*tfe.StateVersion, error) { 947 id := GenerateID("sv-") 948 runID := os.Getenv("TFE_RUN_ID") 949 url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) 950 951 if runID != "" && (options.Run == nil || runID != options.Run.ID) { 952 return nil, fmt.Errorf("option.Run.ID does not contain the ID exported by TFE_RUN_ID") 953 } 954 955 sv := &tfe.StateVersion{ 956 ID: id, 957 DownloadURL: url, 958 Serial: *options.Serial, 959 } 960 961 state, err := base64.StdEncoding.DecodeString(*options.State) 962 if err != nil { 963 return nil, err 964 } 965 966 m.states[sv.DownloadURL] = state 967 m.stateVersions[sv.ID] = sv 968 m.workspaces[workspaceID] = append(m.workspaces[workspaceID], sv.ID) 969 970 return sv, nil 971 } 972 973 func (m *MockStateVersions) Read(ctx context.Context, svID string) (*tfe.StateVersion, error) { 974 return m.ReadWithOptions(ctx, svID, nil) 975 } 976 977 func (m *MockStateVersions) ReadWithOptions(ctx context.Context, svID string, options *tfe.StateVersionReadOptions) (*tfe.StateVersion, error) { 978 sv, ok := m.stateVersions[svID] 979 if !ok { 980 return nil, tfe.ErrResourceNotFound 981 } 982 return sv, nil 983 } 984 985 func (m *MockStateVersions) Current(ctx context.Context, workspaceID string) (*tfe.StateVersion, error) { 986 return m.CurrentWithOptions(ctx, workspaceID, nil) 987 } 988 989 func (m *MockStateVersions) CurrentWithOptions(ctx context.Context, workspaceID string, options *tfe.StateVersionCurrentOptions) (*tfe.StateVersion, error) { 990 w, ok := m.client.Workspaces.workspaceIDs[workspaceID] 991 if !ok { 992 return nil, tfe.ErrResourceNotFound 993 } 994 995 svs, ok := m.workspaces[w.ID] 996 if !ok || len(svs) == 0 { 997 return nil, tfe.ErrResourceNotFound 998 } 999 1000 sv, ok := m.stateVersions[svs[len(svs)-1]] 1001 if !ok { 1002 return nil, tfe.ErrResourceNotFound 1003 } 1004 1005 return sv, nil 1006 } 1007 1008 func (m *MockStateVersions) Download(ctx context.Context, url string) ([]byte, error) { 1009 state, ok := m.states[url] 1010 if !ok { 1011 return nil, tfe.ErrResourceNotFound 1012 } 1013 return state, nil 1014 } 1015 1016 func (m *MockStateVersions) Outputs(ctx context.Context, svID string, options tfe.StateVersionOutputsListOptions) ([]*tfe.StateVersionOutput, error) { 1017 panic("not implemented") 1018 } 1019 1020 type MockVariables struct { 1021 client *MockClient 1022 workspaces map[string]*tfe.VariableList 1023 } 1024 1025 var _ tfe.Variables = (*MockVariables)(nil) 1026 1027 func newMockVariables(client *MockClient) *MockVariables { 1028 return &MockVariables{ 1029 client: client, 1030 workspaces: make(map[string]*tfe.VariableList), 1031 } 1032 } 1033 1034 func (m *MockVariables) List(ctx context.Context, workspaceID string, options tfe.VariableListOptions) (*tfe.VariableList, error) { 1035 vl := m.workspaces[workspaceID] 1036 return vl, nil 1037 } 1038 1039 func (m *MockVariables) Create(ctx context.Context, workspaceID string, options tfe.VariableCreateOptions) (*tfe.Variable, error) { 1040 v := &tfe.Variable{ 1041 ID: GenerateID("var-"), 1042 Key: *options.Key, 1043 Category: *options.Category, 1044 } 1045 if options.Value != nil { 1046 v.Value = *options.Value 1047 } 1048 if options.HCL != nil { 1049 v.HCL = *options.HCL 1050 } 1051 if options.Sensitive != nil { 1052 v.Sensitive = *options.Sensitive 1053 } 1054 1055 workspace := workspaceID 1056 1057 if m.workspaces[workspace] == nil { 1058 m.workspaces[workspace] = &tfe.VariableList{} 1059 } 1060 1061 vl := m.workspaces[workspace] 1062 vl.Items = append(vl.Items, v) 1063 1064 return v, nil 1065 } 1066 1067 func (m *MockVariables) Read(ctx context.Context, workspaceID string, variableID string) (*tfe.Variable, error) { 1068 panic("not implemented") 1069 } 1070 1071 func (m *MockVariables) Update(ctx context.Context, workspaceID string, variableID string, options tfe.VariableUpdateOptions) (*tfe.Variable, error) { 1072 panic("not implemented") 1073 } 1074 1075 func (m *MockVariables) Delete(ctx context.Context, workspaceID string, variableID string) error { 1076 panic("not implemented") 1077 } 1078 1079 type MockWorkspaces struct { 1080 client *MockClient 1081 workspaceIDs map[string]*tfe.Workspace 1082 workspaceNames map[string]*tfe.Workspace 1083 } 1084 1085 func newMockWorkspaces(client *MockClient) *MockWorkspaces { 1086 return &MockWorkspaces{ 1087 client: client, 1088 workspaceIDs: make(map[string]*tfe.Workspace), 1089 workspaceNames: make(map[string]*tfe.Workspace), 1090 } 1091 } 1092 1093 func (m *MockWorkspaces) List(ctx context.Context, organization string, options tfe.WorkspaceListOptions) (*tfe.WorkspaceList, error) { 1094 wl := &tfe.WorkspaceList{} 1095 1096 // Get all the workspaces that match the Search value 1097 searchValue := "" 1098 if options.Search != nil { 1099 searchValue = *options.Search 1100 } 1101 1102 var ws []*tfe.Workspace 1103 var tags []string 1104 1105 if options.Tags != nil { 1106 tags = strings.Split(*options.Tags, ",") 1107 } 1108 for _, w := range m.workspaceIDs { 1109 wTags := make(map[string]struct{}) 1110 for _, wTag := range w.Tags { 1111 wTags[wTag.Name] = struct{}{} 1112 } 1113 1114 if strings.Contains(w.Name, searchValue) { 1115 tagsSatisfied := true 1116 for _, tag := range tags { 1117 if _, ok := wTags[tag]; !ok { 1118 tagsSatisfied = false 1119 } 1120 } 1121 if tagsSatisfied { 1122 ws = append(ws, w) 1123 } 1124 } 1125 } 1126 1127 // Return an empty result if we have no matches. 1128 if len(ws) == 0 { 1129 wl.Pagination = &tfe.Pagination{ 1130 CurrentPage: 1, 1131 } 1132 return wl, nil 1133 } 1134 1135 numPages := (len(ws) / 20) + 1 1136 currentPage := 1 1137 if options.PageNumber != 0 { 1138 currentPage = options.PageNumber 1139 } 1140 previousPage := currentPage - 1 1141 nextPage := currentPage + 1 1142 1143 for i := ((currentPage - 1) * 20); i < ((currentPage-1)*20)+20; i++ { 1144 if i > (len(ws) - 1) { 1145 break 1146 } 1147 wl.Items = append(wl.Items, ws[i]) 1148 } 1149 1150 wl.Pagination = &tfe.Pagination{ 1151 CurrentPage: currentPage, 1152 NextPage: nextPage, 1153 PreviousPage: previousPage, 1154 TotalPages: numPages, 1155 TotalCount: len(wl.Items), 1156 } 1157 1158 return wl, nil 1159 } 1160 1161 func (m *MockWorkspaces) Create(ctx context.Context, organization string, options tfe.WorkspaceCreateOptions) (*tfe.Workspace, error) { 1162 // for TestCloud_setUnavailableTerraformVersion 1163 if *options.Name == "unavailable-terraform-version" && options.TerraformVersion != nil { 1164 return nil, fmt.Errorf("requested Terraform version not available in this TFC instance") 1165 } 1166 if strings.HasSuffix(*options.Name, "no-operations") { 1167 options.Operations = tfe.Bool(false) 1168 options.ExecutionMode = tfe.String("local") 1169 } else if options.Operations == nil { 1170 options.Operations = tfe.Bool(true) 1171 options.ExecutionMode = tfe.String("remote") 1172 } 1173 w := &tfe.Workspace{ 1174 ID: GenerateID("ws-"), 1175 Name: *options.Name, 1176 ExecutionMode: *options.ExecutionMode, 1177 Operations: *options.Operations, 1178 Permissions: &tfe.WorkspacePermissions{ 1179 CanQueueApply: true, 1180 CanQueueRun: true, 1181 }, 1182 } 1183 if options.AutoApply != nil { 1184 w.AutoApply = *options.AutoApply 1185 } 1186 if options.VCSRepo != nil { 1187 w.VCSRepo = &tfe.VCSRepo{} 1188 } 1189 if options.TerraformVersion != nil { 1190 w.TerraformVersion = *options.TerraformVersion 1191 } else { 1192 w.TerraformVersion = tfversion.String() 1193 } 1194 var tags []*tfe.Tag 1195 for _, tag := range options.Tags { 1196 tags = append(tags, tag) 1197 w.TagNames = append(w.TagNames, tag.Name) 1198 } 1199 w.Tags = tags 1200 m.workspaceIDs[w.ID] = w 1201 m.workspaceNames[w.Name] = w 1202 return w, nil 1203 } 1204 1205 func (m *MockWorkspaces) Read(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) { 1206 // custom error for TestCloud_plan500 in backend_plan_test.go 1207 if workspace == "network-error" { 1208 return nil, errors.New("I'm a little teacup") 1209 } 1210 1211 w, ok := m.workspaceNames[workspace] 1212 if !ok { 1213 return nil, tfe.ErrResourceNotFound 1214 } 1215 return w, nil 1216 } 1217 1218 func (m *MockWorkspaces) ReadByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { 1219 w, ok := m.workspaceIDs[workspaceID] 1220 if !ok { 1221 return nil, tfe.ErrResourceNotFound 1222 } 1223 return w, nil 1224 } 1225 1226 func (m *MockWorkspaces) ReadWithOptions(ctx context.Context, organization string, workspace string, options *tfe.WorkspaceReadOptions) (*tfe.Workspace, error) { 1227 panic("not implemented") 1228 } 1229 1230 func (m *MockWorkspaces) ReadByIDWithOptions(ctx context.Context, workspaceID string, options *tfe.WorkspaceReadOptions) (*tfe.Workspace, error) { 1231 w, ok := m.workspaceIDs[workspaceID] 1232 if !ok { 1233 return nil, tfe.ErrResourceNotFound 1234 } 1235 return w, nil 1236 } 1237 1238 func (m *MockWorkspaces) Update(ctx context.Context, organization, workspace string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) { 1239 w, ok := m.workspaceNames[workspace] 1240 if !ok { 1241 return nil, tfe.ErrResourceNotFound 1242 } 1243 1244 err := updateMockWorkspaceAttributes(w, options) 1245 if err != nil { 1246 return nil, err 1247 } 1248 1249 delete(m.workspaceNames, workspace) 1250 m.workspaceNames[w.Name] = w 1251 1252 return w, nil 1253 } 1254 1255 func (m *MockWorkspaces) UpdateByID(ctx context.Context, workspaceID string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) { 1256 w, ok := m.workspaceIDs[workspaceID] 1257 if !ok { 1258 return nil, tfe.ErrResourceNotFound 1259 } 1260 1261 originalName := w.Name 1262 err := updateMockWorkspaceAttributes(w, options) 1263 if err != nil { 1264 return nil, err 1265 } 1266 1267 delete(m.workspaceNames, originalName) 1268 m.workspaceNames[w.Name] = w 1269 1270 return w, nil 1271 } 1272 1273 func updateMockWorkspaceAttributes(w *tfe.Workspace, options tfe.WorkspaceUpdateOptions) error { 1274 // for TestCloud_setUnavailableTerraformVersion 1275 if w.Name == "unavailable-terraform-version" && options.TerraformVersion != nil { 1276 return fmt.Errorf("requested Terraform version not available in this TFC instance") 1277 } 1278 1279 if options.Operations != nil { 1280 w.Operations = *options.Operations 1281 } 1282 if options.ExecutionMode != nil { 1283 w.ExecutionMode = *options.ExecutionMode 1284 } 1285 if options.Name != nil { 1286 w.Name = *options.Name 1287 } 1288 if options.TerraformVersion != nil { 1289 w.TerraformVersion = *options.TerraformVersion 1290 } 1291 if options.WorkingDirectory != nil { 1292 w.WorkingDirectory = *options.WorkingDirectory 1293 } 1294 return nil 1295 } 1296 1297 func (m *MockWorkspaces) Delete(ctx context.Context, organization, workspace string) error { 1298 if w, ok := m.workspaceNames[workspace]; ok { 1299 delete(m.workspaceIDs, w.ID) 1300 } 1301 delete(m.workspaceNames, workspace) 1302 return nil 1303 } 1304 1305 func (m *MockWorkspaces) DeleteByID(ctx context.Context, workspaceID string) error { 1306 if w, ok := m.workspaceIDs[workspaceID]; ok { 1307 delete(m.workspaceIDs, w.Name) 1308 } 1309 delete(m.workspaceIDs, workspaceID) 1310 return nil 1311 } 1312 1313 func (m *MockWorkspaces) RemoveVCSConnection(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) { 1314 w, ok := m.workspaceNames[workspace] 1315 if !ok { 1316 return nil, tfe.ErrResourceNotFound 1317 } 1318 w.VCSRepo = nil 1319 return w, nil 1320 } 1321 1322 func (m *MockWorkspaces) RemoveVCSConnectionByID(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { 1323 w, ok := m.workspaceIDs[workspaceID] 1324 if !ok { 1325 return nil, tfe.ErrResourceNotFound 1326 } 1327 w.VCSRepo = nil 1328 return w, nil 1329 } 1330 1331 func (m *MockWorkspaces) Lock(ctx context.Context, workspaceID string, options tfe.WorkspaceLockOptions) (*tfe.Workspace, error) { 1332 w, ok := m.workspaceIDs[workspaceID] 1333 if !ok { 1334 return nil, tfe.ErrResourceNotFound 1335 } 1336 if w.Locked { 1337 return nil, tfe.ErrWorkspaceLocked 1338 } 1339 w.Locked = true 1340 return w, nil 1341 } 1342 1343 func (m *MockWorkspaces) Unlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { 1344 w, ok := m.workspaceIDs[workspaceID] 1345 if !ok { 1346 return nil, tfe.ErrResourceNotFound 1347 } 1348 if !w.Locked { 1349 return nil, tfe.ErrWorkspaceNotLocked 1350 } 1351 w.Locked = false 1352 return w, nil 1353 } 1354 1355 func (m *MockWorkspaces) ForceUnlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { 1356 w, ok := m.workspaceIDs[workspaceID] 1357 if !ok { 1358 return nil, tfe.ErrResourceNotFound 1359 } 1360 if !w.Locked { 1361 return nil, tfe.ErrWorkspaceNotLocked 1362 } 1363 w.Locked = false 1364 return w, nil 1365 } 1366 1367 func (m *MockWorkspaces) AssignSSHKey(ctx context.Context, workspaceID string, options tfe.WorkspaceAssignSSHKeyOptions) (*tfe.Workspace, error) { 1368 panic("not implemented") 1369 } 1370 1371 func (m *MockWorkspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { 1372 panic("not implemented") 1373 } 1374 1375 func (m *MockWorkspaces) RemoteStateConsumers(ctx context.Context, workspaceID string, options *tfe.RemoteStateConsumersListOptions) (*tfe.WorkspaceList, error) { 1376 panic("not implemented") 1377 } 1378 1379 func (m *MockWorkspaces) AddRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceAddRemoteStateConsumersOptions) error { 1380 panic("not implemented") 1381 } 1382 1383 func (m *MockWorkspaces) RemoveRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceRemoveRemoteStateConsumersOptions) error { 1384 panic("not implemented") 1385 } 1386 1387 func (m *MockWorkspaces) UpdateRemoteStateConsumers(ctx context.Context, workspaceID string, options tfe.WorkspaceUpdateRemoteStateConsumersOptions) error { 1388 panic("not implemented") 1389 } 1390 1391 func (m *MockWorkspaces) Readme(ctx context.Context, workspaceID string) (io.Reader, error) { 1392 panic("not implemented") 1393 } 1394 1395 func (m *MockWorkspaces) Tags(ctx context.Context, workspaceID string, options tfe.WorkspaceTagListOptions) (*tfe.TagList, error) { 1396 panic("not implemented") 1397 } 1398 1399 func (m *MockWorkspaces) AddTags(ctx context.Context, workspaceID string, options tfe.WorkspaceAddTagsOptions) error { 1400 return nil 1401 } 1402 1403 func (m *MockWorkspaces) RemoveTags(ctx context.Context, workspaceID string, options tfe.WorkspaceRemoveTagsOptions) error { 1404 panic("not implemented") 1405 } 1406 1407 const alphanumeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 1408 1409 func GenerateID(s string) string { 1410 b := make([]byte, 16) 1411 for i := range b { 1412 b[i] = alphanumeric[rand.Intn(len(alphanumeric))] 1413 } 1414 return s + string(b) 1415 }