github.com/rvichery/terraform@v0.11.10/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) RunQueue(ctx context.Context, name string, options tfe.RunQueueOptions) (*tfe.RunQueue, error) { 326 rq := &tfe.RunQueue{} 327 328 for _, r := range m.client.Runs.runs { 329 rq.Items = append(rq.Items, r) 330 } 331 332 rq.Pagination = &tfe.Pagination{ 333 CurrentPage: 1, 334 NextPage: 1, 335 PreviousPage: 1, 336 TotalPages: 1, 337 TotalCount: len(rq.Items), 338 } 339 340 return rq, nil 341 } 342 343 type mockPlans struct { 344 client *mockClient 345 logs map[string]string 346 plans map[string]*tfe.Plan 347 } 348 349 func newMockPlans(client *mockClient) *mockPlans { 350 return &mockPlans{ 351 client: client, 352 logs: make(map[string]string), 353 plans: make(map[string]*tfe.Plan), 354 } 355 } 356 357 // create is a helper function to create a mock plan that uses the configured 358 // working directory to find the logfile. 359 func (m *mockPlans) create(cvID, workspaceID string) (*tfe.Plan, error) { 360 id := generateID("plan-") 361 url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) 362 363 p := &tfe.Plan{ 364 ID: id, 365 LogReadURL: url, 366 Status: tfe.PlanPending, 367 } 368 369 w, ok := m.client.Workspaces.workspaceIDs[workspaceID] 370 if !ok { 371 return nil, tfe.ErrResourceNotFound 372 } 373 374 m.logs[url] = filepath.Join( 375 m.client.ConfigurationVersions.uploadPaths[cvID], 376 w.WorkingDirectory, 377 "plan.log", 378 ) 379 m.plans[p.ID] = p 380 381 return p, nil 382 } 383 384 func (m *mockPlans) Read(ctx context.Context, planID string) (*tfe.Plan, error) { 385 p, ok := m.plans[planID] 386 if !ok { 387 return nil, tfe.ErrResourceNotFound 388 } 389 // Together with the mockLogReader this allows testing queued runs. 390 if p.Status == tfe.PlanRunning { 391 p.Status = tfe.PlanFinished 392 } 393 return p, nil 394 } 395 396 func (m *mockPlans) Logs(ctx context.Context, planID string) (io.Reader, error) { 397 p, err := m.Read(ctx, planID) 398 if err != nil { 399 return nil, err 400 } 401 402 logfile, ok := m.logs[p.LogReadURL] 403 if !ok { 404 return nil, tfe.ErrResourceNotFound 405 } 406 407 if _, err := os.Stat(logfile); os.IsNotExist(err) { 408 return bytes.NewBufferString("logfile does not exist"), nil 409 } 410 411 logs, err := ioutil.ReadFile(logfile) 412 if err != nil { 413 return nil, err 414 } 415 416 done := func() (bool, error) { 417 p, err := m.Read(ctx, planID) 418 if err != nil { 419 return false, err 420 } 421 if p.Status != tfe.PlanFinished { 422 return false, nil 423 } 424 return true, nil 425 } 426 427 return &mockLogReader{ 428 done: done, 429 logs: bytes.NewBuffer(logs), 430 }, nil 431 } 432 433 type mockPolicyChecks struct { 434 client *mockClient 435 checks map[string]*tfe.PolicyCheck 436 logs map[string]string 437 } 438 439 func newMockPolicyChecks(client *mockClient) *mockPolicyChecks { 440 return &mockPolicyChecks{ 441 client: client, 442 checks: make(map[string]*tfe.PolicyCheck), 443 logs: make(map[string]string), 444 } 445 } 446 447 // create is a helper function to create a mock policy check that uses the 448 // configured working directory to find the logfile. 449 func (m *mockPolicyChecks) create(cvID, workspaceID string) (*tfe.PolicyCheck, error) { 450 id := generateID("pc-") 451 452 pc := &tfe.PolicyCheck{ 453 ID: id, 454 Actions: &tfe.PolicyActions{}, 455 Permissions: &tfe.PolicyPermissions{}, 456 Scope: tfe.PolicyScopeOrganization, 457 Status: tfe.PolicyPending, 458 } 459 460 w, ok := m.client.Workspaces.workspaceIDs[workspaceID] 461 if !ok { 462 return nil, tfe.ErrResourceNotFound 463 } 464 465 logfile := filepath.Join( 466 m.client.ConfigurationVersions.uploadPaths[cvID], 467 w.WorkingDirectory, 468 "policy.log", 469 ) 470 471 if _, err := os.Stat(logfile); os.IsNotExist(err) { 472 return nil, nil 473 } 474 475 m.logs[pc.ID] = logfile 476 m.checks[pc.ID] = pc 477 478 return pc, nil 479 } 480 481 func (m *mockPolicyChecks) List(ctx context.Context, runID string, options tfe.PolicyCheckListOptions) (*tfe.PolicyCheckList, error) { 482 _, ok := m.client.Runs.runs[runID] 483 if !ok { 484 return nil, tfe.ErrResourceNotFound 485 } 486 487 pcl := &tfe.PolicyCheckList{} 488 for _, pc := range m.checks { 489 pcl.Items = append(pcl.Items, pc) 490 } 491 492 pcl.Pagination = &tfe.Pagination{ 493 CurrentPage: 1, 494 NextPage: 1, 495 PreviousPage: 1, 496 TotalPages: 1, 497 TotalCount: len(pcl.Items), 498 } 499 500 return pcl, nil 501 } 502 503 func (m *mockPolicyChecks) Read(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) { 504 pc, ok := m.checks[policyCheckID] 505 if !ok { 506 return nil, tfe.ErrResourceNotFound 507 } 508 509 logfile, ok := m.logs[pc.ID] 510 if !ok { 511 return nil, tfe.ErrResourceNotFound 512 } 513 514 if _, err := os.Stat(logfile); os.IsNotExist(err) { 515 return nil, fmt.Errorf("logfile does not exist") 516 } 517 518 logs, err := ioutil.ReadFile(logfile) 519 if err != nil { 520 return nil, err 521 } 522 523 switch { 524 case bytes.Contains(logs, []byte("Sentinel Result: true")): 525 pc.Status = tfe.PolicyPasses 526 case bytes.Contains(logs, []byte("Sentinel Result: false")): 527 switch { 528 case bytes.Contains(logs, []byte("hard-mandatory")): 529 pc.Status = tfe.PolicyHardFailed 530 case bytes.Contains(logs, []byte("soft-mandatory")): 531 pc.Actions.IsOverridable = true 532 pc.Permissions.CanOverride = true 533 pc.Status = tfe.PolicySoftFailed 534 } 535 default: 536 // As this is an unexpected state, we say the policy errored. 537 pc.Status = tfe.PolicyErrored 538 } 539 540 return pc, nil 541 } 542 543 func (m *mockPolicyChecks) Override(ctx context.Context, policyCheckID string) (*tfe.PolicyCheck, error) { 544 pc, ok := m.checks[policyCheckID] 545 if !ok { 546 return nil, tfe.ErrResourceNotFound 547 } 548 pc.Status = tfe.PolicyOverridden 549 return pc, nil 550 } 551 552 func (m *mockPolicyChecks) Logs(ctx context.Context, policyCheckID string) (io.Reader, error) { 553 pc, ok := m.checks[policyCheckID] 554 if !ok { 555 return nil, tfe.ErrResourceNotFound 556 } 557 558 logfile, ok := m.logs[pc.ID] 559 if !ok { 560 return nil, tfe.ErrResourceNotFound 561 } 562 563 if _, err := os.Stat(logfile); os.IsNotExist(err) { 564 return bytes.NewBufferString("logfile does not exist"), nil 565 } 566 567 logs, err := ioutil.ReadFile(logfile) 568 if err != nil { 569 return nil, err 570 } 571 572 switch { 573 case bytes.Contains(logs, []byte("Sentinel Result: true")): 574 pc.Status = tfe.PolicyPasses 575 case bytes.Contains(logs, []byte("Sentinel Result: false")): 576 switch { 577 case bytes.Contains(logs, []byte("hard-mandatory")): 578 pc.Status = tfe.PolicyHardFailed 579 case bytes.Contains(logs, []byte("soft-mandatory")): 580 pc.Actions.IsOverridable = true 581 pc.Permissions.CanOverride = true 582 pc.Status = tfe.PolicySoftFailed 583 } 584 default: 585 // As this is an unexpected state, we say the policy errored. 586 pc.Status = tfe.PolicyErrored 587 } 588 589 return bytes.NewBuffer(logs), nil 590 } 591 592 type mockRuns struct { 593 client *mockClient 594 runs map[string]*tfe.Run 595 workspaces map[string][]*tfe.Run 596 } 597 598 func newMockRuns(client *mockClient) *mockRuns { 599 return &mockRuns{ 600 client: client, 601 runs: make(map[string]*tfe.Run), 602 workspaces: make(map[string][]*tfe.Run), 603 } 604 } 605 606 func (m *mockRuns) List(ctx context.Context, workspaceID string, options tfe.RunListOptions) (*tfe.RunList, error) { 607 w, ok := m.client.Workspaces.workspaceIDs[workspaceID] 608 if !ok { 609 return nil, tfe.ErrResourceNotFound 610 } 611 612 rl := &tfe.RunList{} 613 for _, r := range m.workspaces[w.ID] { 614 rl.Items = append(rl.Items, r) 615 } 616 617 rl.Pagination = &tfe.Pagination{ 618 CurrentPage: 1, 619 NextPage: 1, 620 PreviousPage: 1, 621 TotalPages: 1, 622 TotalCount: len(rl.Items), 623 } 624 625 return rl, nil 626 } 627 628 func (m *mockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*tfe.Run, error) { 629 a, err := m.client.Applies.create(options.ConfigurationVersion.ID, options.Workspace.ID) 630 if err != nil { 631 return nil, err 632 } 633 634 p, err := m.client.Plans.create(options.ConfigurationVersion.ID, options.Workspace.ID) 635 if err != nil { 636 return nil, err 637 } 638 639 pc, err := m.client.PolicyChecks.create(options.ConfigurationVersion.ID, options.Workspace.ID) 640 if err != nil { 641 return nil, err 642 } 643 644 r := &tfe.Run{ 645 ID: generateID("run-"), 646 Actions: &tfe.RunActions{IsCancelable: true}, 647 Apply: a, 648 HasChanges: false, 649 Permissions: &tfe.RunPermissions{}, 650 Plan: p, 651 Status: tfe.RunPending, 652 } 653 654 if pc != nil { 655 r.PolicyChecks = []*tfe.PolicyCheck{pc} 656 } 657 658 if options.IsDestroy != nil { 659 r.IsDestroy = *options.IsDestroy 660 } 661 662 w, ok := m.client.Workspaces.workspaceIDs[options.Workspace.ID] 663 if !ok { 664 return nil, tfe.ErrResourceNotFound 665 } 666 if w.CurrentRun == nil { 667 w.CurrentRun = r 668 } 669 670 m.runs[r.ID] = r 671 m.workspaces[options.Workspace.ID] = append(m.workspaces[options.Workspace.ID], r) 672 673 return r, nil 674 } 675 676 func (m *mockRuns) Read(ctx context.Context, runID string) (*tfe.Run, error) { 677 r, ok := m.runs[runID] 678 if !ok { 679 return nil, tfe.ErrResourceNotFound 680 } 681 682 pending := false 683 for _, r := range m.runs { 684 if r.ID != runID && r.Status == tfe.RunPending { 685 pending = true 686 break 687 } 688 } 689 690 if !pending && r.Status == tfe.RunPending { 691 // Only update the status if there are no other pending runs. 692 r.Status = tfe.RunPlanning 693 r.Plan.Status = tfe.PlanRunning 694 } 695 696 logs, _ := ioutil.ReadFile(m.client.Plans.logs[r.Plan.LogReadURL]) 697 if r.Plan.Status == tfe.PlanFinished { 698 if r.IsDestroy || bytes.Contains(logs, []byte("1 to add, 0 to change, 0 to destroy")) { 699 r.Actions.IsCancelable = false 700 r.Actions.IsConfirmable = true 701 r.HasChanges = true 702 r.Permissions.CanApply = true 703 } 704 705 if bytes.Contains(logs, []byte("null_resource.foo: 1 error")) { 706 r.Actions.IsCancelable = false 707 r.HasChanges = false 708 r.Status = tfe.RunErrored 709 } 710 } 711 712 return r, nil 713 } 714 715 func (m *mockRuns) Apply(ctx context.Context, runID string, options tfe.RunApplyOptions) error { 716 r, ok := m.runs[runID] 717 if !ok { 718 return tfe.ErrResourceNotFound 719 } 720 if r.Status != tfe.RunPending { 721 // Only update the status if the run is not pending anymore. 722 r.Status = tfe.RunApplying 723 r.Apply.Status = tfe.ApplyRunning 724 } 725 return nil 726 } 727 728 func (m *mockRuns) Cancel(ctx context.Context, runID string, options tfe.RunCancelOptions) error { 729 panic("not implemented") 730 } 731 732 func (m *mockRuns) ForceCancel(ctx context.Context, runID string, options tfe.RunForceCancelOptions) error { 733 panic("not implemented") 734 } 735 736 func (m *mockRuns) Discard(ctx context.Context, runID string, options tfe.RunDiscardOptions) error { 737 panic("not implemented") 738 } 739 740 type mockStateVersions struct { 741 client *mockClient 742 states map[string][]byte 743 stateVersions map[string]*tfe.StateVersion 744 workspaces map[string][]string 745 } 746 747 func newMockStateVersions(client *mockClient) *mockStateVersions { 748 return &mockStateVersions{ 749 client: client, 750 states: make(map[string][]byte), 751 stateVersions: make(map[string]*tfe.StateVersion), 752 workspaces: make(map[string][]string), 753 } 754 } 755 756 func (m *mockStateVersions) List(ctx context.Context, options tfe.StateVersionListOptions) (*tfe.StateVersionList, error) { 757 svl := &tfe.StateVersionList{} 758 for _, sv := range m.stateVersions { 759 svl.Items = append(svl.Items, sv) 760 } 761 762 svl.Pagination = &tfe.Pagination{ 763 CurrentPage: 1, 764 NextPage: 1, 765 PreviousPage: 1, 766 TotalPages: 1, 767 TotalCount: len(svl.Items), 768 } 769 770 return svl, nil 771 } 772 773 func (m *mockStateVersions) Create(ctx context.Context, workspaceID string, options tfe.StateVersionCreateOptions) (*tfe.StateVersion, error) { 774 id := generateID("sv-") 775 runID := os.Getenv("TFE_RUN_ID") 776 url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) 777 778 if runID != "" && (options.Run == nil || runID != options.Run.ID) { 779 return nil, fmt.Errorf("option.Run.ID does not contain the ID exported by TFE_RUN_ID") 780 } 781 782 sv := &tfe.StateVersion{ 783 ID: id, 784 DownloadURL: url, 785 Serial: *options.Serial, 786 } 787 788 state, err := base64.StdEncoding.DecodeString(*options.State) 789 if err != nil { 790 return nil, err 791 } 792 793 m.states[sv.DownloadURL] = state 794 m.stateVersions[sv.ID] = sv 795 m.workspaces[workspaceID] = append(m.workspaces[workspaceID], sv.ID) 796 797 return sv, nil 798 } 799 800 func (m *mockStateVersions) Read(ctx context.Context, svID string) (*tfe.StateVersion, error) { 801 sv, ok := m.stateVersions[svID] 802 if !ok { 803 return nil, tfe.ErrResourceNotFound 804 } 805 return sv, nil 806 } 807 808 func (m *mockStateVersions) Current(ctx context.Context, workspaceID string) (*tfe.StateVersion, error) { 809 w, ok := m.client.Workspaces.workspaceIDs[workspaceID] 810 if !ok { 811 return nil, tfe.ErrResourceNotFound 812 } 813 814 svs, ok := m.workspaces[w.ID] 815 if !ok || len(svs) == 0 { 816 return nil, tfe.ErrResourceNotFound 817 } 818 819 sv, ok := m.stateVersions[svs[len(svs)-1]] 820 if !ok { 821 return nil, tfe.ErrResourceNotFound 822 } 823 824 return sv, nil 825 } 826 827 func (m *mockStateVersions) Download(ctx context.Context, url string) ([]byte, error) { 828 state, ok := m.states[url] 829 if !ok { 830 return nil, tfe.ErrResourceNotFound 831 } 832 return state, nil 833 } 834 835 type mockWorkspaces struct { 836 client *mockClient 837 workspaceIDs map[string]*tfe.Workspace 838 workspaceNames map[string]*tfe.Workspace 839 } 840 841 func newMockWorkspaces(client *mockClient) *mockWorkspaces { 842 return &mockWorkspaces{ 843 client: client, 844 workspaceIDs: make(map[string]*tfe.Workspace), 845 workspaceNames: make(map[string]*tfe.Workspace), 846 } 847 } 848 849 func (m *mockWorkspaces) List(ctx context.Context, organization string, options tfe.WorkspaceListOptions) (*tfe.WorkspaceList, error) { 850 dummyWorkspaces := 10 851 wl := &tfe.WorkspaceList{} 852 853 // Get the prefix from the search options. 854 prefix := "" 855 if options.Search != nil { 856 prefix = *options.Search 857 } 858 859 // Get all the workspaces that match the prefix. 860 var ws []*tfe.Workspace 861 for _, w := range m.workspaceIDs { 862 if strings.HasPrefix(w.Name, prefix) { 863 ws = append(ws, w) 864 } 865 } 866 867 // Return an empty result if we have no matches. 868 if len(ws) == 0 { 869 wl.Pagination = &tfe.Pagination{ 870 CurrentPage: 1, 871 } 872 return wl, nil 873 } 874 875 // Return dummy workspaces for the first page to test pagination. 876 if options.PageNumber <= 1 { 877 for i := 0; i < dummyWorkspaces; i++ { 878 wl.Items = append(wl.Items, &tfe.Workspace{ 879 ID: generateID("ws-"), 880 Name: fmt.Sprintf("dummy-workspace-%d", i), 881 }) 882 } 883 884 wl.Pagination = &tfe.Pagination{ 885 CurrentPage: 1, 886 NextPage: 2, 887 TotalPages: 2, 888 TotalCount: len(wl.Items) + len(ws), 889 } 890 891 return wl, nil 892 } 893 894 // Return the actual workspaces that matched as the second page. 895 wl.Items = ws 896 wl.Pagination = &tfe.Pagination{ 897 CurrentPage: 2, 898 PreviousPage: 1, 899 TotalPages: 2, 900 TotalCount: len(wl.Items) + dummyWorkspaces, 901 } 902 903 return wl, nil 904 } 905 906 func (m *mockWorkspaces) Create(ctx context.Context, organization string, options tfe.WorkspaceCreateOptions) (*tfe.Workspace, error) { 907 w := &tfe.Workspace{ 908 ID: generateID("ws-"), 909 Name: *options.Name, 910 Permissions: &tfe.WorkspacePermissions{ 911 CanQueueRun: true, 912 CanUpdate: true, 913 }, 914 } 915 if options.AutoApply != nil { 916 w.AutoApply = *options.AutoApply 917 } 918 if options.VCSRepo != nil { 919 w.VCSRepo = &tfe.VCSRepo{} 920 } 921 m.workspaceIDs[w.ID] = w 922 m.workspaceNames[w.Name] = w 923 return w, nil 924 } 925 926 func (m *mockWorkspaces) Read(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) { 927 w, ok := m.workspaceNames[workspace] 928 if !ok { 929 return nil, tfe.ErrResourceNotFound 930 } 931 return w, nil 932 } 933 934 func (m *mockWorkspaces) Update(ctx context.Context, organization, workspace string, options tfe.WorkspaceUpdateOptions) (*tfe.Workspace, error) { 935 w, ok := m.workspaceNames[workspace] 936 if !ok { 937 return nil, tfe.ErrResourceNotFound 938 } 939 940 if options.Name != nil { 941 w.Name = *options.Name 942 } 943 if options.TerraformVersion != nil { 944 w.TerraformVersion = *options.TerraformVersion 945 } 946 if options.WorkingDirectory != nil { 947 w.WorkingDirectory = *options.WorkingDirectory 948 } 949 950 delete(m.workspaceNames, workspace) 951 m.workspaceNames[w.Name] = w 952 953 return w, nil 954 } 955 956 func (m *mockWorkspaces) Delete(ctx context.Context, organization, workspace string) error { 957 if w, ok := m.workspaceNames[workspace]; ok { 958 delete(m.workspaceIDs, w.ID) 959 } 960 delete(m.workspaceNames, workspace) 961 return nil 962 } 963 964 func (m *mockWorkspaces) Lock(ctx context.Context, workspaceID string, options tfe.WorkspaceLockOptions) (*tfe.Workspace, error) { 965 w, ok := m.workspaceIDs[workspaceID] 966 if !ok { 967 return nil, tfe.ErrResourceNotFound 968 } 969 w.Locked = true 970 return w, nil 971 } 972 973 func (m *mockWorkspaces) Unlock(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { 974 w, ok := m.workspaceIDs[workspaceID] 975 if !ok { 976 return nil, tfe.ErrResourceNotFound 977 } 978 w.Locked = false 979 return w, nil 980 } 981 982 func (m *mockWorkspaces) AssignSSHKey(ctx context.Context, workspaceID string, options tfe.WorkspaceAssignSSHKeyOptions) (*tfe.Workspace, error) { 983 panic("not implemented") 984 } 985 986 func (m *mockWorkspaces) UnassignSSHKey(ctx context.Context, workspaceID string) (*tfe.Workspace, error) { 987 panic("not implemented") 988 } 989 990 const alphanumeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 991 992 func generateID(s string) string { 993 b := make([]byte, 16) 994 for i := range b { 995 b[i] = alphanumeric[rand.Intn(len(alphanumeric))] 996 } 997 return s + string(b) 998 }