From 9062d887b83a80ab9fb86c6231dc619036a5ba8c Mon Sep 17 00:00:00 2001 From: Sander van Harmelen Date: Wed, 5 Dec 2018 12:29:08 +0100 Subject: [PATCH] backend/remote: use entitlements to select backends Use the entitlements to a) determine if the organization exists, and b) as a means to select which backend to use (the local backend with remote state, or the remote backend). --- backend/remote/backend.go | 8 ++-- backend/remote/backend_mock.go | 11 +++++ backend/remote/backend_plan_test.go | 30 ++++++++++++++ backend/remote/testing.go | 62 +++++++++++++++++++---------- 4 files changed, 87 insertions(+), 24 deletions(-) diff --git a/backend/remote/backend.go b/backend/remote/backend.go index 7f4c24326..17bb4c912 100644 --- a/backend/remote/backend.go +++ b/backend/remote/backend.go @@ -271,15 +271,15 @@ func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics { return diags } - // Check if the organization exists. - _, err = b.client.Organizations.Read(context.Background(), b.organization) + // Check if the organization exists by reading its entitlements. + entitlements, err := b.client.Organizations.Entitlements(context.Background(), b.organization) if err != nil { if err == tfe.ErrResourceNotFound { err = fmt.Errorf("organization %s does not exist", b.organization) } diags = diags.Append(tfdiags.AttributeValue( tfdiags.Error, - "Failed to read organization settings", + "Failed to read organization entitlements", fmt.Sprintf( `The "remote" backend encountered an unexpected error while reading the `+ `organization settings: %s.`, err, @@ -291,7 +291,7 @@ func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics { // Configure a local backend for when we need to run operations locally. b.local = backendLocal.NewWithBackend(b) - b.forceLocal = os.Getenv("TF_FORCE_LOCAL_BACKEND") != "" + b.forceLocal = !entitlements.Operations || os.Getenv("TF_FORCE_LOCAL_BACKEND") != "" return diags } diff --git a/backend/remote/backend_mock.go b/backend/remote/backend_mock.go index 8f9a41f03..636062577 100644 --- a/backend/remote/backend_mock.go +++ b/backend/remote/backend_mock.go @@ -322,6 +322,17 @@ func (m *mockOrganizations) Capacity(ctx context.Context, name string) (*tfe.Cap return &tfe.Capacity{Pending: pending, Running: running}, nil } +func (m *mockOrganizations) Entitlements(ctx context.Context, name string) (*tfe.Entitlements, error) { + return &tfe.Entitlements{ + Operations: true, + PrivateModuleRegistry: true, + Sentinel: true, + StateStorage: true, + Teams: true, + VCSIntegrations: true, + }, nil +} + func (m *mockOrganizations) RunQueue(ctx context.Context, name string, options tfe.RunQueueOptions) (*tfe.RunQueue, error) { rq := &tfe.RunQueue{} diff --git a/backend/remote/backend_plan_test.go b/backend/remote/backend_plan_test.go index 403af271b..5c96ebdd5 100644 --- a/backend/remote/backend_plan_test.go +++ b/backend/remote/backend_plan_test.go @@ -354,6 +354,36 @@ func TestRemote_planForceLocal(t *testing.T) { } } +func TestRemote_planWithoutOperationsEntitlement(t *testing.T) { + b := testBackendNoOperations(t) + + op, configCleanup := testOperationPlan(t, "./test-fixtures/plan") + defer configCleanup() + + op.Workspace = backend.DefaultStateName + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + if run.Result != backend.OperationSuccess { + t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) + } + if run.PlanEmpty { + t.Fatalf("expected a non-empty plan") + } + + output := b.CLI.(*cli.MockUi).OutputWriter.String() + if strings.Contains(output, "Running plan in the remote backend") { + t.Fatalf("unexpected remote backend header in output: %s", output) + } + if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { + t.Fatalf("expected plan summery in output: %s", output) + } +} + func TestRemote_planWorkspaceWithoutOperations(t *testing.T) { b := testBackendNoDefault(t) ctx := context.Background() diff --git a/backend/remote/testing.go b/backend/remote/testing.go index 54083dc23..de2d26866 100644 --- a/backend/remote/testing.go +++ b/backend/remote/testing.go @@ -66,6 +66,19 @@ func testBackendNoDefault(t *testing.T) *Remote { return testBackend(t, obj) } +func testBackendNoOperations(t *testing.T) *Remote { + obj := cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "organization": cty.StringVal("no-operations"), + "token": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("prod"), + "prefix": cty.NullVal(cty.String), + }), + }) + return testBackend(t, obj) +} + func testRemoteClient(t *testing.T) remote.Client { b := testBackendDefault(t) raw, err := b.StateMgr(backend.DefaultStateName) @@ -171,30 +184,39 @@ func testServer(t *testing.T) *httptest.Server { io.WriteString(w, `{"tfe.v2":"/api/v2/"}`) }) - // Respond to the initial query to read the organization settings. - mux.HandleFunc("/api/v2/organizations/hashicorp", func(w http.ResponseWriter, r *http.Request) { + // Respond to the initial query to read the hashicorp org entitlements. + mux.HandleFunc("/api/v2/organizations/hashicorp/entitlement-set", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/vnd.api+json") io.WriteString(w, `{ "data": { - "id": "hashicorp", - "type": "organizations", + "id": "org-GExadygjSbKP8hsY", + "type": "entitlement-sets", "attributes": { - "name": "hashicorp", - "created-at": "2017-09-07T14:34:40.492Z", - "email": "user@example.com", - "collaborator-auth-policy": "password", - "enterprise-plan": "premium", - "permissions": { - "can-update": true, - "can-destroy": true, - "can-create-team": true, - "can-create-workspace": true, - "can-update-oauth": true, - "can-update-api-token": true, - "can-update-sentinel": true, - "can-traverse": true, - "can-create-workspace-migration": true - } + "operations": true, + "private-module-registry": true, + "sentinel": true, + "state-storage": true, + "teams": true, + "vcs-integrations": true + } + } +}`) + }) + + // Respond to the initial query to read the no-operations org entitlements. + mux.HandleFunc("/api/v2/organizations/no-operations/entitlement-set", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.api+json") + io.WriteString(w, `{ + "data": { + "id": "org-ufxa3y8jSbKP8hsT", + "type": "entitlement-sets", + "attributes": { + "operations": false, + "private-module-registry": true, + "sentinel": true, + "state-storage": true, + "teams": true, + "vcs-integrations": true } } }`)