Skip to content

Commit

Permalink
Merge pull request #9135 from hashicorp/f-namespaces
Browse files Browse the repository at this point in the history
core: open source namespaces
  • Loading branch information
schmichael committed Oct 23, 2020
2 parents 2e8f0f3 + 47b8db8 commit d002a8f
Show file tree
Hide file tree
Showing 36 changed files with 2,706 additions and 121 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
FEATURES:

* **Event Stream**: Subscribe to change events as they occur in real time. [[GH-9013](https://github.com/hashicorp/nomad/issues/9013)]
* **Namespaces OSS**: Namespaces are now available in open source Nomad. [[GH-9135](https://github.com/hashicorp/nomad/issues/9135)]
* **Topology Visualization**: See all of the clients and allocations in a cluster at once. [[GH-9077](https://github.com/hashicorp/nomad/issues/9077)]

IMPROVEMENTS:
Expand Down
4 changes: 4 additions & 0 deletions command/agent/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,10 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {

s.mux.HandleFunc("/v1/event/stream", s.wrap(s.EventStream))

s.mux.HandleFunc("/v1/namespaces", s.wrap(s.NamespacesRequest))
s.mux.HandleFunc("/v1/namespace", s.wrap(s.NamespaceCreateRequest))
s.mux.HandleFunc("/v1/namespace/", s.wrap(s.NamespaceSpecificRequest))

if uiEnabled {
s.mux.Handle("/ui/", http.StripPrefix("/ui/", s.handleUI(http.FileServer(&UIAssetWrapper{FileSystem: assetFS()}))))
} else {
Expand Down
4 changes: 0 additions & 4 deletions command/agent/http_oss.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ import (

// registerEnterpriseHandlers is a no-op for the oss release
func (s *HTTPServer) registerEnterpriseHandlers() {
s.mux.HandleFunc("/v1/namespaces", s.wrap(s.entOnly))
s.mux.HandleFunc("/v1/namespace", s.wrap(s.entOnly))
s.mux.HandleFunc("/v1/namespace/", s.wrap(s.entOnly))

s.mux.HandleFunc("/v1/sentinel/policies", s.wrap(s.entOnly))
s.mux.HandleFunc("/v1/sentinel/policy/", s.wrap(s.entOnly))

Expand Down
119 changes: 119 additions & 0 deletions command/agent/namespace_endpoint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package agent

import (
"net/http"
"strings"

"github.com/hashicorp/nomad/nomad/structs"
)

func (s *HTTPServer) NamespacesRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if req.Method != "GET" {
return nil, CodedError(405, ErrInvalidMethod)
}

args := structs.NamespaceListRequest{}
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
return nil, nil
}

var out structs.NamespaceListResponse
if err := s.agent.RPC("Namespace.ListNamespaces", &args, &out); err != nil {
return nil, err
}

setMeta(resp, &out.QueryMeta)
if out.Namespaces == nil {
out.Namespaces = make([]*structs.Namespace, 0)
}
return out.Namespaces, nil
}

func (s *HTTPServer) NamespaceSpecificRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
name := strings.TrimPrefix(req.URL.Path, "/v1/namespace/")
if len(name) == 0 {
return nil, CodedError(400, "Missing Namespace Name")
}
switch req.Method {
case "GET":
return s.namespaceQuery(resp, req, name)
case "PUT", "POST":
return s.namespaceUpdate(resp, req, name)
case "DELETE":
return s.namespaceDelete(resp, req, name)
default:
return nil, CodedError(405, ErrInvalidMethod)
}
}

func (s *HTTPServer) NamespaceCreateRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if req.Method != "PUT" && req.Method != "POST" {
return nil, CodedError(405, ErrInvalidMethod)
}

return s.namespaceUpdate(resp, req, "")
}

func (s *HTTPServer) namespaceQuery(resp http.ResponseWriter, req *http.Request,
namespaceName string) (interface{}, error) {
args := structs.NamespaceSpecificRequest{
Name: namespaceName,
}
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
return nil, nil
}

var out structs.SingleNamespaceResponse
if err := s.agent.RPC("Namespace.GetNamespace", &args, &out); err != nil {
return nil, err
}

setMeta(resp, &out.QueryMeta)
if out.Namespace == nil {
return nil, CodedError(404, "Namespace not found")
}
return out.Namespace, nil
}

func (s *HTTPServer) namespaceUpdate(resp http.ResponseWriter, req *http.Request,
namespaceName string) (interface{}, error) {
// Parse the namespace
var namespace structs.Namespace
if err := decodeBody(req, &namespace); err != nil {
return nil, CodedError(500, err.Error())
}

// Ensure the namespace name matches
if namespaceName != "" && namespace.Name != namespaceName {
return nil, CodedError(400, "Namespace name does not match request path")
}

// Format the request
args := structs.NamespaceUpsertRequest{
Namespaces: []*structs.Namespace{&namespace},
}
s.parseWriteRequest(req, &args.WriteRequest)

var out structs.GenericResponse
if err := s.agent.RPC("Namespace.UpsertNamespaces", &args, &out); err != nil {
return nil, err
}
setIndex(resp, out.Index)
return nil, nil
}

func (s *HTTPServer) namespaceDelete(resp http.ResponseWriter, req *http.Request,
namespaceName string) (interface{}, error) {

args := structs.NamespaceDeleteRequest{
Namespaces: []string{namespaceName},
}
s.parseWriteRequest(req, &args.WriteRequest)

var out structs.GenericResponse
if err := s.agent.RPC("Namespace.DeleteNamespaces", &args, &out); err != nil {
return nil, err
}
setIndex(resp, out.Index)
return nil, nil
}
172 changes: 172 additions & 0 deletions command/agent/namespace_endpoint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// +build ent

package agent

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/stretchr/testify/assert"
)

func TestHTTP_NamespaceList(t *testing.T) {
assert := assert.New(t)
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
ns1 := mock.Namespace()
ns2 := mock.Namespace()
ns3 := mock.Namespace()
args := structs.NamespaceUpsertRequest{
Namespaces: []*structs.Namespace{ns1, ns2, ns3},
WriteRequest: structs.WriteRequest{Region: "global"},
}
var resp structs.GenericResponse
assert.Nil(s.Agent.RPC("Namespace.UpsertNamespaces", &args, &resp))

// Make the HTTP request
req, err := http.NewRequest("GET", "/v1/namespaces", nil)
assert.Nil(err)
respW := httptest.NewRecorder()

// Make the request
obj, err := s.Server.NamespacesRequest(respW, req)
assert.Nil(err)

// Check for the index
assert.NotZero(respW.HeaderMap.Get("X-Nomad-Index"))
assert.Equal("true", respW.HeaderMap.Get("X-Nomad-KnownLeader"))
assert.NotZero(respW.HeaderMap.Get("X-Nomad-LastContact"))

// Check the output (the 3 we register + default)
assert.Len(obj.([]*structs.Namespace), 4)
})
}

func TestHTTP_NamespaceQuery(t *testing.T) {
assert := assert.New(t)
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
ns1 := mock.Namespace()
args := structs.NamespaceUpsertRequest{
Namespaces: []*structs.Namespace{ns1},
WriteRequest: structs.WriteRequest{Region: "global"},
}
var resp structs.GenericResponse
assert.Nil(s.Agent.RPC("Namespace.UpsertNamespaces", &args, &resp))

// Make the HTTP request
req, err := http.NewRequest("GET", "/v1/namespace/"+ns1.Name, nil)
assert.Nil(err)
respW := httptest.NewRecorder()

// Make the request
obj, err := s.Server.NamespaceSpecificRequest(respW, req)
assert.Nil(err)

// Check for the index
assert.NotZero(respW.HeaderMap.Get("X-Nomad-Index"))
assert.Equal("true", respW.HeaderMap.Get("X-Nomad-KnownLeader"))
assert.NotZero(respW.HeaderMap.Get("X-Nomad-LastContact"))

// Check the output
assert.Equal(ns1.Name, obj.(*structs.Namespace).Name)
})
}

func TestHTTP_NamespaceCreate(t *testing.T) {
assert := assert.New(t)
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
// Make the HTTP request
ns1 := mock.Namespace()
buf := encodeReq(ns1)
req, err := http.NewRequest("PUT", "/v1/namespace", buf)
assert.Nil(err)
respW := httptest.NewRecorder()

// Make the request
obj, err := s.Server.NamespaceCreateRequest(respW, req)
assert.Nil(err)
assert.Nil(obj)

// Check for the index
assert.NotZero(respW.HeaderMap.Get("X-Nomad-Index"))

// Check policy was created
state := s.Agent.server.State()
out, err := state.NamespaceByName(nil, ns1.Name)
assert.Nil(err)
assert.NotNil(out)

ns1.CreateIndex, ns1.ModifyIndex = out.CreateIndex, out.ModifyIndex
assert.Equal(ns1.Name, out.Name)
assert.Equal(ns1, out)
})
}

func TestHTTP_NamespaceUpdate(t *testing.T) {
assert := assert.New(t)
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
// Make the HTTP request
ns1 := mock.Namespace()
buf := encodeReq(ns1)
req, err := http.NewRequest("PUT", "/v1/namespace/"+ns1.Name, buf)
assert.Nil(err)
respW := httptest.NewRecorder()

// Make the request
obj, err := s.Server.NamespaceSpecificRequest(respW, req)
assert.Nil(err)
assert.Nil(obj)

// Check for the index
assert.NotZero(respW.HeaderMap.Get("X-Nomad-Index"))

// Check policy was created
state := s.Agent.server.State()
out, err := state.NamespaceByName(nil, ns1.Name)
assert.Nil(err)
assert.NotNil(out)

ns1.CreateIndex, ns1.ModifyIndex = out.CreateIndex, out.ModifyIndex
assert.Equal(ns1.Name, out.Name)
assert.Equal(ns1, out)
})
}

func TestHTTP_NamespaceDelete(t *testing.T) {
assert := assert.New(t)
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
ns1 := mock.Namespace()
args := structs.NamespaceUpsertRequest{
Namespaces: []*structs.Namespace{ns1},
WriteRequest: structs.WriteRequest{Region: "global"},
}
var resp structs.GenericResponse
assert.Nil(s.Agent.RPC("Namespace.UpsertNamespaces", &args, &resp))

// Make the HTTP request
req, err := http.NewRequest("DELETE", "/v1/namespace/"+ns1.Name, nil)
assert.Nil(err)
respW := httptest.NewRecorder()

// Make the request
obj, err := s.Server.NamespaceSpecificRequest(respW, req)
assert.Nil(err)
assert.Nil(obj)

// Check for the index
assert.NotZero(respW.HeaderMap.Get("X-Nomad-Index"))

// Check policy was created
state := s.Agent.server.State()
out, err := state.NamespaceByName(nil, ns1.Name)
assert.Nil(err)
assert.Nil(out)
})
}
7 changes: 5 additions & 2 deletions helper/raftutil/generate_msgtypes.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@ var msgTypeNames = map[structs.MessageType]string{
EOF

cat ../../nomad/structs/structs.go \
| grep -A500 'MessageType = iota' \
| grep -v -e '//' \
| grep -A500 'MessageType = 0' \
| grep -v -e '//' \
| grep -v -e '^$' \
| awk '/^\)$/ { exit; } /.*/ { printf " structs.%s: \"%s\",\n", $1, $1}'

echo '}'
}

echo "==> Generating type map..."
generate_file > msgtypes.go

echo "==> Formatting type map..."
gofmt -w msgtypes.go
2 changes: 2 additions & 0 deletions helper/raftutil/msgtypes.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit d002a8f

Please sign in to comment.