Simplify overlay mtime api and add tests

Signed-off-by: Jonathon Anderson <janderson@ciq.com>
This commit is contained in:
Jonathon Anderson
2025-07-11 19:50:35 -06:00
parent a3dbf6aeee
commit a0752a5bd0
3 changed files with 89 additions and 24 deletions

View File

@@ -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()))
})
})

View File

@@ -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

View File

@@ -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": "<<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("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))
})
}