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:
parent
03ac6ec774
commit
9062d887b8
|
@ -271,15 +271,15 @@ func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics {
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the organization exists.
|
// Check if the organization exists by reading its entitlements.
|
||||||
_, err = b.client.Organizations.Read(context.Background(), b.organization)
|
entitlements, err := b.client.Organizations.Entitlements(context.Background(), b.organization)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == tfe.ErrResourceNotFound {
|
if err == tfe.ErrResourceNotFound {
|
||||||
err = fmt.Errorf("organization %s does not exist", b.organization)
|
err = fmt.Errorf("organization %s does not exist", b.organization)
|
||||||
}
|
}
|
||||||
diags = diags.Append(tfdiags.AttributeValue(
|
diags = diags.Append(tfdiags.AttributeValue(
|
||||||
tfdiags.Error,
|
tfdiags.Error,
|
||||||
"Failed to read organization settings",
|
"Failed to read organization entitlements",
|
||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
`The "remote" backend encountered an unexpected error while reading the `+
|
`The "remote" backend encountered an unexpected error while reading the `+
|
||||||
`organization settings: %s.`, err,
|
`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.
|
// Configure a local backend for when we need to run operations locally.
|
||||||
b.local = backendLocal.NewWithBackend(b)
|
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
|
return diags
|
||||||
}
|
}
|
||||||
|
|
|
@ -322,6 +322,17 @@ func (m *mockOrganizations) Capacity(ctx context.Context, name string) (*tfe.Cap
|
||||||
return &tfe.Capacity{Pending: pending, Running: running}, nil
|
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) {
|
func (m *mockOrganizations) RunQueue(ctx context.Context, name string, options tfe.RunQueueOptions) (*tfe.RunQueue, error) {
|
||||||
rq := &tfe.RunQueue{}
|
rq := &tfe.RunQueue{}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
func TestRemote_planWorkspaceWithoutOperations(t *testing.T) {
|
||||||
b := testBackendNoDefault(t)
|
b := testBackendNoDefault(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
|
@ -66,6 +66,19 @@ func testBackendNoDefault(t *testing.T) *Remote {
|
||||||
return testBackend(t, obj)
|
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 {
|
func testRemoteClient(t *testing.T) remote.Client {
|
||||||
b := testBackendDefault(t)
|
b := testBackendDefault(t)
|
||||||
raw, err := b.StateMgr(backend.DefaultStateName)
|
raw, err := b.StateMgr(backend.DefaultStateName)
|
||||||
|
@ -171,31 +184,40 @@ func testServer(t *testing.T) *httptest.Server {
|
||||||
io.WriteString(w, `{"tfe.v2":"/api/v2/"}`)
|
io.WriteString(w, `{"tfe.v2":"/api/v2/"}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Respond to the initial query to read the organization settings.
|
// Respond to the initial query to read the hashicorp org entitlements.
|
||||||
mux.HandleFunc("/api/v2/organizations/hashicorp", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/v2/organizations/hashicorp/entitlement-set", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/vnd.api+json")
|
w.Header().Set("Content-Type", "application/vnd.api+json")
|
||||||
io.WriteString(w, `{
|
io.WriteString(w, `{
|
||||||
"data": {
|
"data": {
|
||||||
"id": "hashicorp",
|
"id": "org-GExadygjSbKP8hsY",
|
||||||
"type": "organizations",
|
"type": "entitlement-sets",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
"name": "hashicorp",
|
"operations": true,
|
||||||
"created-at": "2017-09-07T14:34:40.492Z",
|
"private-module-registry": true,
|
||||||
"email": "user@example.com",
|
"sentinel": true,
|
||||||
"collaborator-auth-policy": "password",
|
"state-storage": true,
|
||||||
"enterprise-plan": "premium",
|
"teams": true,
|
||||||
"permissions": {
|
"vcs-integrations": true
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}`)
|
}`)
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue