Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: open source namespaces #9135

Merged
merged 3 commits into from
Oct 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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