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).
This commit is contained in:
Sander van Harmelen 2018-12-05 12:29:08 +01:00
parent 03ac6ec774
commit 9062d887b8
4 changed files with 87 additions and 24 deletions

View File

@ -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
}

View File

@ -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{}

View File

@ -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()

View File

@ -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
}
}
}`)