From 0e0a4cef26f388494e7930da19b1e3d52aa96ba9 Mon Sep 17 00:00:00 2001 From: Matt Keeler Date: Mon, 7 Oct 2019 15:19:38 -0400 Subject: [PATCH] Add support for the Namespace HTTP API in the API Client (#6581) --- api/acl.go | 14 ++--- api/namespace.go | 131 ++++++++++++++++++++++++++++++++++++++++++ api/namespace_test.go | 128 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 264 insertions(+), 9 deletions(-) create mode 100644 api/namespace.go create mode 100644 api/namespace_test.go diff --git a/api/acl.go b/api/acl.go index 124409ff2..d2669752c 100644 --- a/api/acl.go +++ b/api/acl.go @@ -18,15 +18,14 @@ const ( ACLManagementType = "management" ) -type ACLTokenPolicyLink struct { - ID string - Name string -} -type ACLTokenRoleLink struct { +type ACLLink struct { ID string Name string } +type ACLTokenPolicyLink = ACLLink +type ACLTokenRoleLink = ACLLink + // ACLToken represents an ACL Token type ACLToken struct { CreateIndex uint64 @@ -117,10 +116,7 @@ type ACLPolicyListEntry struct { ModifyIndex uint64 } -type ACLRolePolicyLink struct { - ID string - Name string -} +type ACLRolePolicyLink = ACLLink // ACLRole represents an ACL Role. type ACLRole struct { diff --git a/api/namespace.go b/api/namespace.go new file mode 100644 index 000000000..cfcc1b815 --- /dev/null +++ b/api/namespace.go @@ -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 +} diff --git a/api/namespace_test.go b/api/namespace_test.go new file mode 100644 index 000000000..be776a579 --- /dev/null +++ b/api/namespace_test.go @@ -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()) + } + }) +}