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 } } }`) diff --git a/go.mod b/go.mod index 0d5763df0..772caa15c 100644 --- a/go.mod +++ b/go.mod @@ -68,7 +68,7 @@ require ( github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90 github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc // indirect github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 // indirect - github.com/hashicorp/go-tfe v0.3.3 + github.com/hashicorp/go-tfe v0.3.4 github.com/hashicorp/go-uuid v1.0.0 github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577 github.com/hashicorp/golang-lru v0.5.0 // indirect diff --git a/go.sum b/go.sum index 2c7399b0d..aa71f39ad 100644 --- a/go.sum +++ b/go.sum @@ -153,8 +153,8 @@ github.com/hashicorp/go-slug v0.2.0 h1:gekvezBc+9LwN3qC+lesrz0Qg36hhgge9z/an1FCH github.com/hashicorp/go-slug v0.2.0/go.mod h1:+zDycQOzGqOqMW7Kn2fp9vz/NtqpMLQlgb9JUF+0km4= github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 h1:7YOlAIO2YWnJZkQp7B5eFykaIY7C9JndqAFQyVV5BhM= github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-tfe v0.3.3 h1:v17u0VdSy54n6Xn575cTzLrNJ0gn+Y7mq5J+A/p1fkw= -github.com/hashicorp/go-tfe v0.3.3/go.mod h1:Vssg8/lwVz+PyJ/nAK97zYmXxxLe28MCIMhKo+rva1o= +github.com/hashicorp/go-tfe v0.3.4 h1:A9pKjZMDTSGozXf2wQlWhBI7QoxCoas14Xg/TSiEAV8= +github.com/hashicorp/go-tfe v0.3.4/go.mod h1:Vssg8/lwVz+PyJ/nAK97zYmXxxLe28MCIMhKo+rva1o= github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577 h1:at4+18LrM8myamuV7/vT6x2s1JNXp2k4PsSbt4I02X4= diff --git a/vendor/github.com/hashicorp/go-tfe/organization.go b/vendor/github.com/hashicorp/go-tfe/organization.go index 32e289ed6..edd6d3f47 100644 --- a/vendor/github.com/hashicorp/go-tfe/organization.go +++ b/vendor/github.com/hashicorp/go-tfe/organization.go @@ -35,6 +35,9 @@ type Organizations interface { // Capacity shows the current run capacity of an organization. Capacity(ctx context.Context, organization string) (*Capacity, error) + // Entitlements shows the entitlements of an organization. + Entitlements(ctx context.Context, organization string) (*Entitlements, error) + // RunQueue shows the current run queue of an organization. RunQueue(ctx context.Context, organization string, options RunQueueOptions) (*RunQueue, error) } @@ -93,6 +96,17 @@ type Capacity struct { Running int `jsonapi:"attr,running"` } +// Entitlements represents the entitlements of an organization. +type Entitlements struct { + ID string `jsonapi:"primary,entitlement-sets"` + StateStorage bool `jsonapi:"attr,state-storage"` + Operations bool `jsonapi:"attr,operations"` + VCSIntegrations bool `jsonapi:"attr,vcs-integrations"` + Sentinel bool `jsonapi:"attr,sentinel"` + PrivateModuleRegistry bool `jsonapi:"attr,private-module-registry"` + Teams bool `jsonapi:"attr,teams"` +} + // RunQueue represents the current run queue of an organization. type RunQueue struct { *Pagination @@ -283,6 +297,27 @@ func (s *organizations) Capacity(ctx context.Context, organization string) (*Cap return c, nil } +// Entitlements shows the entitlements of an organization. +func (s *organizations) Entitlements(ctx context.Context, organization string) (*Entitlements, error) { + if !validStringID(&organization) { + return nil, errors.New("invalid value for organization") + } + + u := fmt.Sprintf("organizations/%s/entitlement-set", url.QueryEscape(organization)) + req, err := s.client.newRequest("GET", u, nil) + if err != nil { + return nil, err + } + + e := &Entitlements{} + err = s.client.do(ctx, req, e) + if err != nil { + return nil, err + } + + return e, nil +} + // RunQueueOptions represents the options for showing the queue. type RunQueueOptions struct { ListOptions diff --git a/vendor/modules.txt b/vendor/modules.txt index 68e349b64..ee2abe56c 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -327,7 +327,7 @@ github.com/hashicorp/go-rootcerts github.com/hashicorp/go-safetemp # github.com/hashicorp/go-slug v0.2.0 github.com/hashicorp/go-slug -# github.com/hashicorp/go-tfe v0.3.3 +# github.com/hashicorp/go-tfe v0.3.4 github.com/hashicorp/go-tfe # github.com/hashicorp/go-uuid v1.0.0 github.com/hashicorp/go-uuid