diff --git a/internal/pkg/warewulfd/api/api.go b/internal/pkg/warewulfd/api/api.go index 7f4d9229..d1f6e2dd 100644 --- a/internal/pkg/warewulfd/api/api.go +++ b/internal/pkg/warewulfd/api/api.go @@ -34,7 +34,7 @@ func Handler(auth *config.Authentication, allowedNets []net.IPNet) *web.Service r.Method(http.MethodGet, "/{id}/fields", nethttp.NewHandler(getNodeFields())) r.Method(http.MethodPost, "/overlays/build", nethttp.NewHandler(buildAllOverlays())) r.Method(http.MethodPost, "/{id}/overlays/build", nethttp.NewHandler(buildOverlays())) - r.Method(http.MethodGet, "/{id}/overlays", nethttp.NewHandler(getNodeOverlays())) + r.Method(http.MethodGet, "/{id}/overlays", nethttp.NewHandler(getNodeOverlayInfo())) }) }) diff --git a/internal/pkg/warewulfd/api/node.go b/internal/pkg/warewulfd/api/node.go index faa4588c..f596e349 100644 --- a/internal/pkg/warewulfd/api/node.go +++ b/internal/pkg/warewulfd/api/node.go @@ -42,23 +42,20 @@ func getNodes() usecase.Interactor { return u } -func getNodeOverlays() usecase.Interactor { +func getNodeOverlayInfo() usecase.Interactor { type getOverlaysInput struct { ID string `path:"id" description:"ID of node to retrieve overlays for"` } - type buildEntry struct { - MTime string `json:"mtime,omitempty" yaml:"mtime,omitempty"` - } - type overlayEntry struct { - Overlays []string `json:"overlays,omitempty" yaml:"overlays,omitempty"` - Build *buildEntry `json:"build,omitempty" yaml:"build,omitempty"` + type overlayInfo struct { + Overlays []string `json:"overlays,omitempty" yaml:"overlays,omitempty"` + MTime *time.Time `json:"mtime,omitempty" yaml:"mtime,omitempty"` } type getOverlaysOutput struct { - SystemOverlay *overlayEntry `json:"system overlay,omitempty" yaml:"system overlay,omitempty"` - RuntimeOverlay *overlayEntry `json:"runtime overlay,omitempty" yaml:"runtime overlay,omitempty"` + SystemOverlay *overlayInfo `json:"system overlay,omitempty" yaml:"system overlay,omitempty"` + RuntimeOverlay *overlayInfo `json:"runtime overlay,omitempty" yaml:"runtime overlay,omitempty"` } u := usecase.NewInteractor(func(ctx context.Context, input *getOverlaysInput, output *getOverlaysOutput) error { - wwlog.Debug("api.getNodeOverlays()") + wwlog.Debug("api.getNodeOverlayInfo()") if registry, err := node.New(); err != nil { return err } else { @@ -66,23 +63,23 @@ func getNodeOverlays() usecase.Interactor { return status.Wrap(err, status.NotFound) } else { out := getOverlaysOutput{ - SystemOverlay: &overlayEntry{ + SystemOverlay: &overlayInfo{ Overlays: node_.SystemOverlay, - Build: &buildEntry{}, }, - RuntimeOverlay: &overlayEntry{ + RuntimeOverlay: &overlayInfo{ Overlays: node_.RuntimeOverlay, - Build: &buildEntry{}, }, } sysImagePath := overlay.OverlayImage(input.ID, "system", node_.SystemOverlay) if sysImageStat, err := os.Stat(sysImagePath); err == nil { - out.SystemOverlay.Build.MTime = sysImageStat.ModTime().Format(time.RFC3339) + mtime := sysImageStat.ModTime() + out.SystemOverlay.MTime = &mtime } runtimeImagePath := overlay.OverlayImage(input.ID, "runtime", node_.RuntimeOverlay) if runtimeImageStat, err := os.Stat(runtimeImagePath); err == nil { - out.RuntimeOverlay.Build.MTime = runtimeImageStat.ModTime().Format(time.RFC3339) + mtime := runtimeImageStat.ModTime() + out.RuntimeOverlay.MTime = &mtime } *output = out return nil diff --git a/internal/pkg/warewulfd/api/node_test.go b/internal/pkg/warewulfd/api/node_test.go index 92c72045..8c2daa9f 100644 --- a/internal/pkg/warewulfd/api/node_test.go +++ b/internal/pkg/warewulfd/api/node_test.go @@ -2,12 +2,15 @@ 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" @@ -16,6 +19,8 @@ import ( 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{ @@ -32,6 +37,8 @@ func TestNodeAPI(t *testing.T) { testNode := `{ "node":{ + "system overlay": ["so1"], + "runtime overlay": ["ro1"], "kernel": { "version": "v1.0.0", "args": ["kernel-args"] @@ -48,7 +55,7 @@ func TestNodeAPI(t *testing.T) { assert.NoError(t, err) assert.NoError(t, resp.Body.Close()) - assert.JSONEq(t, `{"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) + 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) { @@ -64,7 +71,7 @@ func TestNodeAPI(t *testing.T) { assert.NoError(t, err) assert.NoError(t, resp.Body.Close()) - assert.JSONEq(t, `{"node1": {}, "test": {"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}}`, string(body)) + assert.JSONEq(t, `{"node1": {}, "test": {"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}}`, string(body)) }) t.Run("get one specific node", func(t *testing.T) { @@ -80,7 +87,31 @@ func TestNodeAPI(t *testing.T) { assert.NoError(t, err) assert.NoError(t, resp.Body.Close()) - assert.JSONEq(t, `{"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) + 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) { @@ -101,7 +132,7 @@ func TestNodeAPI(t *testing.T) { assert.NoError(t, err) assert.NoError(t, resp.Body.Close()) - assert.JSONEq(t, `{"kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body)) + 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) { @@ -115,7 +146,7 @@ func TestNodeAPI(t *testing.T) { assert.NoError(t, err) assert.NoError(t, resp.Body.Close()) - assert.JSONEq(t, `{"kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body)) + 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) { @@ -129,7 +160,7 @@ func TestNodeAPI(t *testing.T) { assert.NoError(t, err) assert.NoError(t, resp.Body.Close()) - assert.JSONEq(t, `{"kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body)) + 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) { @@ -160,6 +191,43 @@ func TestNodeAPI(t *testing.T) { 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": "<>" + }, + "runtime overlay": { + "overlays": ["ro1"], + "mtime": "<>" + } + }`) + + 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("test delete nodes", func(t *testing.T) { req, err := http.NewRequest(http.MethodDelete, srv.URL+"/api/nodes/test", nil) assert.NoError(t, err) @@ -171,6 +239,6 @@ func TestNodeAPI(t *testing.T) { assert.NoError(t, err) assert.NoError(t, resp.Body.Close()) - assert.JSONEq(t, `{"kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body)) + assert.JSONEq(t, `{"system overlay": ["so1"], "runtime overlay": ["ro1"], "kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body)) }) }