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