HTTP PUT is supposed to be idempotent, so PUT for an existing node or profile should replace the node or profile, not throw an error. Generating an error on duplicate can now be requested using the If-None-Match header with a `*` value. Signed-off-by: Jonathon Anderson <janderson@ciq.com>
325 lines
9.0 KiB
Go
325 lines
9.0 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/kinbiko/jsonassert"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/warewulf/warewulf/internal/pkg/testenv"
|
|
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
|
|
)
|
|
|
|
func TestNodeAPI(t *testing.T) {
|
|
warewulfd.SetNoDaemon()
|
|
env := testenv.New(t)
|
|
env.MkdirAll("/var/lib/warewulf/overlays/so1")
|
|
env.MkdirAll("/var/lib/warewulf/overlays/ro1")
|
|
defer env.RemoveAll()
|
|
|
|
allowedNets := []net.IPNet{
|
|
{
|
|
IP: net.IPv4(127, 0, 0, 0),
|
|
Mask: net.CIDRMask(8, 32),
|
|
},
|
|
}
|
|
srv := httptest.NewServer(Handler(nil, allowedNets))
|
|
defer srv.Close()
|
|
|
|
t.Run("add a node", func(t *testing.T) {
|
|
// prepareration
|
|
|
|
testNode := `{
|
|
"node":{
|
|
"system overlay": ["so1"],
|
|
"runtime overlay": ["ro1"],
|
|
"kernel": {
|
|
"version": "v1.0.0",
|
|
"args": ["kernel-args"]
|
|
}
|
|
}
|
|
}`
|
|
req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/nodes/test", bytes.NewBuffer([]byte(testNode)))
|
|
assert.NoError(t, err)
|
|
|
|
resp, err := http.DefaultTransport.RoundTrip(req)
|
|
assert.NoError(t, err)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, resp.Body.Close())
|
|
|
|
assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body))
|
|
})
|
|
|
|
t.Run("read all nodes", func(t *testing.T) {
|
|
req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/nodes", nil)
|
|
assert.NoError(t, err)
|
|
|
|
// send request
|
|
resp, err := http.DefaultTransport.RoundTrip(req)
|
|
assert.NoError(t, err)
|
|
|
|
// validate the resp
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, resp.Body.Close())
|
|
|
|
assert.JSONEq(t, `{"node1": {}, "test": {"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}}`, string(body))
|
|
})
|
|
|
|
t.Run("test idempotency (put same node again)", func(t *testing.T) {
|
|
// prepareration
|
|
|
|
testNode := `{
|
|
"node":{
|
|
"system overlay": ["so1"],
|
|
"runtime overlay": ["ro1"],
|
|
"kernel": {
|
|
"version": "v1.0.0",
|
|
"args": ["kernel-args"]
|
|
}
|
|
}
|
|
}`
|
|
req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/nodes/test", bytes.NewBuffer([]byte(testNode)))
|
|
assert.NoError(t, err)
|
|
|
|
resp, err := http.DefaultTransport.RoundTrip(req)
|
|
assert.NoError(t, err)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, resp.Body.Close())
|
|
|
|
assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body))
|
|
})
|
|
|
|
t.Run("fail if node already exists (given appropriate header)", func(t *testing.T) {
|
|
// prepareration
|
|
|
|
testNode := `{
|
|
"node":{
|
|
"system overlay": ["so1"],
|
|
"runtime overlay": ["ro1"],
|
|
"kernel": {
|
|
"version": "v1.0.0",
|
|
"args": ["kernel-args"]
|
|
}
|
|
}
|
|
}`
|
|
req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/nodes/test", bytes.NewBuffer([]byte(testNode)))
|
|
assert.NoError(t, err)
|
|
req.Header.Set("If-None-Match", "*")
|
|
|
|
resp, err := http.DefaultTransport.RoundTrip(req)
|
|
assert.NoError(t, err)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, resp.Body.Close())
|
|
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
assert.Contains(t, string(body), "node 'test' already exists")
|
|
})
|
|
|
|
t.Run("get one specific node", func(t *testing.T) {
|
|
req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/nodes/test", nil)
|
|
assert.NoError(t, err)
|
|
|
|
// send request
|
|
resp, err := http.DefaultTransport.RoundTrip(req)
|
|
assert.NoError(t, err)
|
|
|
|
// validate the resp
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, resp.Body.Close())
|
|
|
|
assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body))
|
|
})
|
|
|
|
t.Run("get unbuilt overlay info for the node", func(t *testing.T) {
|
|
req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/nodes/test/overlays", nil)
|
|
assert.NoError(t, err)
|
|
|
|
// send request
|
|
resp, err := http.DefaultTransport.RoundTrip(req)
|
|
assert.NoError(t, err)
|
|
|
|
// validate the response
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, resp.Body.Close())
|
|
|
|
ja := jsonassert.New(t)
|
|
ja.Assertf(string(body), `{
|
|
"system overlay": {
|
|
"overlays": ["so1"]
|
|
},
|
|
"runtime overlay": {
|
|
"overlays": ["ro1"]
|
|
}
|
|
}`)
|
|
})
|
|
|
|
t.Run("update the node", func(t *testing.T) {
|
|
updateNode := `{
|
|
"node":{
|
|
"kernel": {
|
|
"version": "v1.0.1-newversion"
|
|
}
|
|
}
|
|
}`
|
|
req, err := http.NewRequest(http.MethodPatch, srv.URL+"/api/nodes/test", bytes.NewBuffer([]byte(updateNode)))
|
|
assert.NoError(t, err)
|
|
|
|
resp, err := http.DefaultTransport.RoundTrip(req)
|
|
assert.NoError(t, err)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, resp.Body.Close())
|
|
|
|
assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body))
|
|
})
|
|
|
|
t.Run("get one specific node (again)", func(t *testing.T) {
|
|
req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/nodes/test", nil)
|
|
assert.NoError(t, err)
|
|
|
|
resp, err := http.DefaultTransport.RoundTrip(req)
|
|
assert.NoError(t, err)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, resp.Body.Close())
|
|
|
|
assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body))
|
|
})
|
|
|
|
t.Run("get one specific (raw) node", func(t *testing.T) {
|
|
req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/nodes/test/raw", nil)
|
|
assert.NoError(t, err)
|
|
|
|
resp, err := http.DefaultTransport.RoundTrip(req)
|
|
assert.NoError(t, err)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, resp.Body.Close())
|
|
|
|
assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body))
|
|
})
|
|
|
|
t.Run("test build all nodes overlays", func(t *testing.T) {
|
|
req, err := http.NewRequest(http.MethodPost, srv.URL+"/api/nodes/overlays/build", nil)
|
|
assert.NoError(t, err)
|
|
|
|
resp, err := http.DefaultTransport.RoundTrip(req)
|
|
assert.NoError(t, err)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, resp.Body.Close())
|
|
|
|
assert.JSONEq(t, `["node1", "test"]`, string(body))
|
|
})
|
|
|
|
t.Run("test build one node's overlays", func(t *testing.T) {
|
|
req, err := http.NewRequest(http.MethodPost, srv.URL+"/api/nodes/test/overlays/build", nil)
|
|
assert.NoError(t, err)
|
|
|
|
resp, err := http.DefaultTransport.RoundTrip(req)
|
|
assert.NoError(t, err)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, resp.Body.Close())
|
|
|
|
assert.JSONEq(t, `"test"`, string(body))
|
|
})
|
|
|
|
t.Run("get built overlay info for the node", func(t *testing.T) {
|
|
req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/nodes/test/overlays", nil)
|
|
assert.NoError(t, err)
|
|
|
|
// send request
|
|
resp, err := http.DefaultTransport.RoundTrip(req)
|
|
assert.NoError(t, err)
|
|
|
|
// validate the response
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, resp.Body.Close())
|
|
|
|
ja := jsonassert.New(t)
|
|
ja.Assertf(string(body), `{
|
|
"system overlay": {
|
|
"overlays": ["so1"],
|
|
"mtime": "<<PRESENCE>>"
|
|
},
|
|
"runtime overlay": {
|
|
"overlays": ["ro1"],
|
|
"mtime": "<<PRESENCE>>"
|
|
}
|
|
}`)
|
|
|
|
data := map[string]any{}
|
|
assert.NoError(t, json.Unmarshal(body, &data))
|
|
{
|
|
_, err := time.Parse(time.RFC3339, data["system overlay"].(map[string]any)["mtime"].(string))
|
|
assert.NoError(t, err)
|
|
}
|
|
{
|
|
_, err := time.Parse(time.RFC3339, data["runtime overlay"].(map[string]any)["mtime"].(string))
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
|
|
t.Run("replace a node", func(t *testing.T) {
|
|
// prepareration
|
|
|
|
testNode := `{
|
|
"node":{
|
|
"system overlay": ["so1"],
|
|
"runtime overlay": ["ro1"],
|
|
"kernel": {
|
|
"version": "v1.0.0",
|
|
"args": ["kernel-args"]
|
|
}
|
|
}
|
|
}`
|
|
req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/nodes/test", bytes.NewBuffer([]byte(testNode)))
|
|
assert.NoError(t, err)
|
|
|
|
resp, err := http.DefaultTransport.RoundTrip(req)
|
|
assert.NoError(t, err)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, resp.Body.Close())
|
|
|
|
assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body))
|
|
})
|
|
|
|
t.Run("test delete nodes", func(t *testing.T) {
|
|
req, err := http.NewRequest(http.MethodDelete, srv.URL+"/api/nodes/test", nil)
|
|
assert.NoError(t, err)
|
|
|
|
resp, err := http.DefaultTransport.RoundTrip(req)
|
|
assert.NoError(t, err)
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, resp.Body.Close())
|
|
|
|
assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body))
|
|
})
|
|
}
|