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