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