Merge pull request #19555 from hashicorp/svh/f-entitlements

backend/remote: use entitlements to select backends
This commit is contained in:
Sander van Harmelen 2018-12-07 09:13:35 +01:00 committed by GitHub
commit 70689f5aa1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 126 additions and 28 deletions

View File

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

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

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) { func TestRemote_planWorkspaceWithoutOperations(t *testing.T) {
b := testBackendNoDefault(t) b := testBackendNoDefault(t)
ctx := context.Background() ctx := context.Background()

View File

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

2
go.mod
View File

@ -68,7 +68,7 @@ require (
github.com/hashicorp/go-rootcerts v0.0.0-20160503143440-6bb64b370b90 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-safetemp v0.0.0-20180326211150-b1a1dbde6fdc // indirect
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 // 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-uuid v1.0.0
github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577 github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577
github.com/hashicorp/golang-lru v0.5.0 // indirect github.com/hashicorp/golang-lru v0.5.0 // indirect

4
go.sum
View File

@ -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-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 h1:7YOlAIO2YWnJZkQp7B5eFykaIY7C9JndqAFQyVV5BhM=
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 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.4 h1:A9pKjZMDTSGozXf2wQlWhBI7QoxCoas14Xg/TSiEAV8=
github.com/hashicorp/go-tfe v0.3.3/go.mod h1:Vssg8/lwVz+PyJ/nAK97zYmXxxLe28MCIMhKo+rva1o= 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 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 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= github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577 h1:at4+18LrM8myamuV7/vT6x2s1JNXp2k4PsSbt4I02X4=

View File

@ -35,6 +35,9 @@ type Organizations interface {
// Capacity shows the current run capacity of an organization. // Capacity shows the current run capacity of an organization.
Capacity(ctx context.Context, organization string) (*Capacity, error) 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 shows the current run queue of an organization.
RunQueue(ctx context.Context, organization string, options RunQueueOptions) (*RunQueue, error) RunQueue(ctx context.Context, organization string, options RunQueueOptions) (*RunQueue, error)
} }
@ -93,6 +96,17 @@ type Capacity struct {
Running int `jsonapi:"attr,running"` 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. // RunQueue represents the current run queue of an organization.
type RunQueue struct { type RunQueue struct {
*Pagination *Pagination
@ -283,6 +297,27 @@ func (s *organizations) Capacity(ctx context.Context, organization string) (*Cap
return c, nil 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. // RunQueueOptions represents the options for showing the queue.
type RunQueueOptions struct { type RunQueueOptions struct {
ListOptions ListOptions

2
vendor/modules.txt vendored
View File

@ -327,7 +327,7 @@ github.com/hashicorp/go-rootcerts
github.com/hashicorp/go-safetemp github.com/hashicorp/go-safetemp
# github.com/hashicorp/go-slug v0.2.0 # github.com/hashicorp/go-slug v0.2.0
github.com/hashicorp/go-slug 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-tfe
# github.com/hashicorp/go-uuid v1.0.0 # github.com/hashicorp/go-uuid v1.0.0
github.com/hashicorp/go-uuid github.com/hashicorp/go-uuid