Add support for the Namespace HTTP API in the API Client (#6581)

This commit is contained in:
Matt Keeler 2019-10-07 15:19:38 -04:00 committed by GitHub
parent 825feaadd5
commit 0e0a4cef26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 264 additions and 9 deletions

View File

@ -18,15 +18,14 @@ const (
ACLManagementType = "management" ACLManagementType = "management"
) )
type ACLTokenPolicyLink struct { type ACLLink struct {
ID string
Name string
}
type ACLTokenRoleLink struct {
ID string ID string
Name string Name string
} }
type ACLTokenPolicyLink = ACLLink
type ACLTokenRoleLink = ACLLink
// ACLToken represents an ACL Token // ACLToken represents an ACL Token
type ACLToken struct { type ACLToken struct {
CreateIndex uint64 CreateIndex uint64
@ -117,10 +116,7 @@ type ACLPolicyListEntry struct {
ModifyIndex uint64 ModifyIndex uint64
} }
type ACLRolePolicyLink struct { type ACLRolePolicyLink = ACLLink
ID string
Name string
}
// ACLRole represents an ACL Role. // ACLRole represents an ACL Role.
type ACLRole struct { type ACLRole struct {

131
api/namespace.go Normal file
View File

@ -0,0 +1,131 @@
package api
import (
"fmt"
"time"
)
// Namespace is the configuration of a single namespace. Namespacing is a Consul Enterprise feature.
type Namespace struct {
// Name is the name of the Namespace. It must be unique and
// must be a DNS hostname. There are also other reserved names
// that may not be used.
Name string `json:"Name"`
// Description is where the user puts any information they want
// about the namespace. It is not used internally.
Description string `json:"Description,omitempty"`
// ACLs is the configuration of ACLs for this namespace. It has its
// own struct so that we can add more to it in the future.
// This is nullable so that we can omit if empty when encoding in JSON
ACLs *NamespaceACLConfig `json:"ACLs,omitempty"`
// DeletedAt is the time when the Namespace was marked for deletion
// This is nullable so that we can omit if empty when encoding in JSON
DeletedAt *time.Time `json:"DeletedAt,omitempty"`
// CreateIndex is the Raft index at which the Namespace was created
CreateIndex uint64 `json:"CreateIndex,omitempty"`
// ModifyIndex is the latest Raft index at which the Namespace was modified.
ModifyIndex uint64 `json:"ModifyIndex,omitempty"`
}
// NamespaceACLConfig is the Namespace specific ACL configuration container
type NamespaceACLConfig struct {
// PolicyDefaults is the list of policies that should be used for the parent authorizer
// of all tokens in the associated namespace.
PolicyDefaults []ACLLink `json:"PolicyDefaults"`
// RoleDefaults is the list of roles that should be used for the parent authorizer
// of all tokens in the associated namespace.
RoleDefaults []ACLLink `json:"RoleDefaults"`
}
// Namespaces can be used to manage Namespaces in Consul Enterprise..
type Namespaces struct {
c *Client
}
// Operator returns a handle to the operator endpoints.
func (c *Client) Namespaces() *Namespaces {
return &Namespaces{c}
}
func (n *Namespaces) Create(ns *Namespace, q *WriteOptions) (*Namespace, *WriteMeta, error) {
if ns.Name == "" {
return nil, nil, fmt.Errorf("Must specify a Name for Namespace creation")
}
r := n.c.newRequest("PUT", "/v1/namespace")
r.setWriteOptions(q)
r.obj = ns
rtt, resp, err := requireOK(n.c.doRequest(r))
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
wm := &WriteMeta{RequestTime: rtt}
var out Namespace
if err := decodeBody(resp, &out); err != nil {
return nil, nil, err
}
return &out, wm, nil
}
func (n *Namespaces) Update(ns *Namespace, q *WriteOptions) (*Namespace, *WriteMeta, error) {
if ns.Name == "" {
return nil, nil, fmt.Errorf("Must specify a Name for Namespace updating")
}
r := n.c.newRequest("PUT", "/v1/namespace/"+ns.Name)
r.setWriteOptions(q)
r.obj = ns
rtt, resp, err := requireOK(n.c.doRequest(r))
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
wm := &WriteMeta{RequestTime: rtt}
var out Namespace
if err := decodeBody(resp, &out); err != nil {
return nil, nil, err
}
return &out, wm, nil
}
func (n *Namespaces) Read(name string, q *QueryOptions) (*Namespace, *QueryMeta, error) {
var out Namespace
qm, err := n.c.query("/v1/namespace/"+name, &out, q)
if err != nil {
return nil, nil, err
}
return &out, qm, nil
}
func (n *Namespaces) Delete(name string, q *WriteOptions) (*WriteMeta, error) {
r := n.c.newRequest("DELETE", "/v1/namespace/"+name)
r.setWriteOptions(q)
rtt, resp, err := requireOK(n.c.doRequest(r))
if err != nil {
return nil, err
}
resp.Body.Close()
wm := &WriteMeta{RequestTime: rtt}
return wm, nil
}
func (n *Namespaces) List(q *QueryOptions) ([]*Namespace, *QueryMeta, error) {
var out []*Namespace
qm, err := n.c.query("/v1/namespaces", &out, q)
if err != nil {
return nil, nil, err
}
return out, qm, nil
}

128
api/namespace_test.go Normal file
View File

@ -0,0 +1,128 @@
// +build consulent
package api
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestAPI_Namespaces(t *testing.T) {
t.Parallel()
c, s := makeACLClient(t)
defer s.Stop()
namespaces := c.Namespaces()
acl := c.ACL()
nsPolicy, _, err := acl.PolicyCreate(&ACLPolicy{
Name: "ns-policy",
Rules: `operator = "write"`,
}, nil)
require.NoError(t, err)
nsRole, _, err := acl.RoleCreate(&ACLRole{
Name: "ns-role",
Policies: []*ACLRolePolicyLink{
&ACLRolePolicyLink{
ID: nsPolicy.ID,
},
},
}, nil)
require.NoError(t, err)
t.Run("Create Nameless", func(t *testing.T) {
ns := Namespace{
Description: "foo",
}
_, _, err := namespaces.Create(&ns, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "Must specify a Name for Namespace creation")
})
t.Run("Create", func(t *testing.T) {
ns, _, err := namespaces.Create(&Namespace{
Name: "foo",
}, nil)
require.NoError(t, err)
require.NotNil(t, ns)
require.Equal(t, "foo", ns.Name)
require.Nil(t, ns.ACLs)
ns, _, err = namespaces.Create(&Namespace{
Name: "acls",
Description: "This namespace has ACL config attached",
ACLs: &NamespaceACLConfig{
PolicyDefaults: []ACLLink{
ACLLink{ID: nsPolicy.ID},
},
RoleDefaults: []ACLLink{
ACLLink{ID: nsRole.ID},
},
},
}, nil)
require.NoError(t, err)
require.NotNil(t, ns)
require.NotNil(t, ns.ACLs)
require.Nil(t, ns.DeletedAt)
})
t.Run("Update Nameless", func(t *testing.T) {
ns := Namespace{
Description: "foo",
}
_, _, err := namespaces.Update(&ns, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "Must specify a Name for Namespace updating")
})
t.Run("Update", func(t *testing.T) {
ns, _, err := namespaces.Update(&Namespace{
Name: "foo",
Description: "updated description",
}, nil)
require.NoError(t, err)
require.NotNil(t, ns)
require.Equal(t, "updated description", ns.Description)
})
t.Run("List", func(t *testing.T) {
nsList, _, err := namespaces.List(nil)
require.NoError(t, err)
require.Len(t, nsList, 3)
found := make(map[string]struct{})
for _, ns := range nsList {
found[ns.Name] = struct{}{}
}
require.Contains(t, found, "default")
require.Contains(t, found, "foo")
require.Contains(t, found, "acls")
})
t.Run("Delete", func(t *testing.T) {
_, err := namespaces.Delete("foo", nil)
require.NoError(t, err)
// due to deferred deletion the namespace might still exist
// this checks that either it is in fact gone and we get a 404 or
// that the namespace is still there but marked for deletion
ns, _, err := namespaces.Read("foo", nil)
if err != nil {
require.Contains(t, err.Error(), "Unexpected response code: 404")
require.Nil(t, ns)
} else {
require.NotNil(t, ns)
require.NotNil(t, ns.DeletedAt)
require.False(t, ns.DeletedAt.IsZero())
}
})
}