From 755411d3cbea8e23ce404f6eb02617f16cea8523 Mon Sep 17 00:00:00 2001 From: Christian Goll Date: Tue, 28 Jan 2025 13:52:42 -0700 Subject: [PATCH] Added warewulfd:/overlay-file/{overlay}/{path...}?render={nodeID} Co-authored-by: Jonathon Anderson Signed-off-by: Christian Goll Signed-off-by: Jonathon Anderson --- CHANGELOG.md | 1 + internal/pkg/testenv/testenv.go | 10 +- internal/pkg/warewulfd/overlay.go | 157 +++++++++++++++++++++++++ internal/pkg/warewulfd/overlay_test.go | 110 +++++++++++++++++ internal/pkg/warewulfd/warewulfd.go | 1 + 5 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 internal/pkg/warewulfd/overlay.go create mode 100644 internal/pkg/warewulfd/overlay_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e6afe94..82f3fa96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Add Dev Container support #1653 - Add man pages and command reference to userdocs. #1488 - Document building images from scratch with Apptainer. #1485 +- Added warewulfd:/overlay-file/{overlay}/{path...}?render={id} ### Changed diff --git a/internal/pkg/testenv/testenv.go b/internal/pkg/testenv/testenv.go index 24f9dc40..8a612c11 100644 --- a/internal/pkg/testenv/testenv.go +++ b/internal/pkg/testenv/testenv.go @@ -87,10 +87,13 @@ func (env *TestEnv) init() { env.WriteFile(path.Join(Sysconfdir, "warewulf/warewulf.conf"), initWarewulfConf) // re-read warewulf.conf - conf := config.New() - err = conf.Read(env.GetPath(path.Join(Sysconfdir, "warewulf/warewulf.conf"))) - env.assertNoError(err) + _ = env.Configure() +} +func (env *TestEnv) Configure() *config.WarewulfYaml { + conf := config.New() + err := conf.Read(env.GetPath(path.Join(Sysconfdir, "warewulf/warewulf.conf"))) + env.assertNoError(err) conf.Paths.Sysconfdir = env.GetPath(Sysconfdir) conf.Paths.Bindir = env.GetPath(Bindir) conf.Paths.Datadir = env.GetPath(Datadir) @@ -120,6 +123,7 @@ func (env *TestEnv) init() { } { env.MkdirAll(confPath) } + return conf } // assertNoError handles error conditions generated by env, diff --git a/internal/pkg/warewulfd/overlay.go b/internal/pkg/warewulfd/overlay.go new file mode 100644 index 00000000..1b299b9b --- /dev/null +++ b/internal/pkg/warewulfd/overlay.go @@ -0,0 +1,157 @@ +package warewulfd + +import ( + "fmt" + "net" + "net/http" + "os" + "path" + "strconv" + "strings" + + "github.com/warewulf/warewulf/internal/pkg/config" + "github.com/warewulf/warewulf/internal/pkg/node" + "github.com/warewulf/warewulf/internal/pkg/overlay" + "github.com/warewulf/warewulf/internal/pkg/util" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +func OverlaySend(w http.ResponseWriter, req *http.Request) { + rinfo, err := parseReqRender(req) + if err != nil { + message := "error parsing request: %s" + wwlog.ErrorExc(err, message, err) + http.Error(w, fmt.Sprintf(message, err), http.StatusBadRequest) + return + } + + o := overlay.GetOverlay(rinfo.overlay) + if !o.Exists() { + message := "overlay not found: %s" + wwlog.Error(message, rinfo.overlay) + http.Error(w, fmt.Sprintf(message, rinfo.overlay), http.StatusNoContent) + return + } + + wwlog.Info("recv: render req overlay: %s, path: %s, node: %s", rinfo.overlay, rinfo.path, rinfo.node) + if config.Get().Warewulf.Secure() && rinfo.remoteport >= 1024 { + message := "non-privileged port: %s" + wwlog.Denied(message, req.RemoteAddr) + http.Error(w, fmt.Sprintf(message, req.RemoteAddr), http.StatusUnauthorized) + return + } + + overlayFile := o.File(rinfo.path) + if !path.IsAbs(overlayFile) { + message := "Path %s isn't absolute" + wwlog.Denied(message, overlayFile) + http.Error(w, fmt.Sprintf(message, overlayFile), http.StatusNotFound) + return + } + + if !util.IsFile(overlayFile) { + if rinfo.node != "" && util.IsFile(overlayFile+".ww") { + wwlog.Debug("appending .ww for file: %s", overlayFile) + overlayFile += ".ww" + } else { + message := "file doesn't exists: %s" + wwlog.Denied(message, overlayFile) + http.Error(w, fmt.Sprintf(message, overlayFile), http.StatusNotFound) + return + } + } + + if strings.HasSuffix(overlayFile, ".ww") && rinfo.node != "" { + nodeDB, err := node.New() + if err != nil { + message := "error opening node database: %s" + wwlog.ErrorExc(err, message, err) + http.Error(w, fmt.Sprintf(message, err), http.StatusNotFound) + return + } + + node, err := nodeDB.GetNode(rinfo.node) + if err != nil { + message := "error getting node: %s" + wwlog.ErrorExc(err, message, err) + http.Error(w, fmt.Sprintf(message, err), http.StatusNotFound) + return + } + + allNodes, err := nodeDB.FindAllNodes() + if err != nil { + message := "error loading nodes from registry: %s" + wwlog.ErrorExc(err, message, err) + http.Error(w, fmt.Sprintf(message, err), http.StatusInternalServerError) + return + } + + tstruct, err := overlay.InitStruct(overlayFile, node, allNodes) + if err != nil { + message := "error initializing template data: %s" + wwlog.ErrorExc(err, message, err) + http.Error(w, fmt.Sprintf(message, err), http.StatusInternalServerError) + return + } + tstruct.BuildSource = overlayFile + + buffer, _, _, err := overlay.RenderTemplateFile(overlayFile, tstruct) + if err != nil { + message := "error rendering overlay template: %s" + wwlog.ErrorExc(err, message, err) + http.Error(w, fmt.Sprintf(message, err), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Length", strconv.Itoa(buffer.Len())) + _, err = buffer.WriteTo(w) + if err != nil { + message := "error writing overlay template over http connection: %s" + wwlog.ErrorExc(err, message, err) + http.Error(w, fmt.Sprintf(message, err), http.StatusInternalServerError) + } + wwlog.Info("%s: %s", node.Id(), overlayFile) + } else { + fileBytes, err := os.ReadFile(overlayFile) + if err != nil { + message := "error reading file: %s" + wwlog.ErrorExc(err, message, err) + http.Error(w, fmt.Sprintf(message, err), http.StatusInternalServerError) + return + } + _, err = w.Write(fileBytes) + if err != nil { + message := "error writing overlay file over http connection: %s" + wwlog.ErrorExc(err, message, err) + http.Error(w, fmt.Sprintf(message, err), http.StatusInternalServerError) + } + wwlog.Info("send overlay file for node %s: %s", rinfo.node, overlayFile) + } +} + +type parserInfoRender struct { + overlay string + path string + node string + remoteport int +} + +func parseReqRender(req *http.Request) (ret parserInfoRender, err error) { + parts := strings.Split(req.URL.Path, "/") + ret.overlay = parts[2] + if ret.overlay == "" { + return ret, fmt.Errorf("no overlay specified") + } + ret.path = strings.Join(parts[3:], "/") + if ret.path == "" { + return ret, fmt.Errorf("no path specified") + } + if len(req.URL.Query()["render"]) > 0 { + ret.node = req.URL.Query()["render"][0] + } + if _, remoteport, err := net.SplitHostPort(req.RemoteAddr); err != nil { + return ret, fmt.Errorf("could not obtain remote port from HTTP request: %w", err) + } else if ret.remoteport, err = strconv.Atoi(remoteport); err != nil { + return ret, fmt.Errorf("couldn't obtain remote port from HTTP request: %w", err) + } + return ret, nil +} diff --git a/internal/pkg/warewulfd/overlay_test.go b/internal/pkg/warewulfd/overlay_test.go new file mode 100644 index 00000000..0163ff56 --- /dev/null +++ b/internal/pkg/warewulfd/overlay_test.go @@ -0,0 +1,110 @@ +package warewulfd + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/warewulf/warewulf/internal/pkg/testenv" +) + +var overlaySendTests = map[string]struct { + url string + body string + status int +}{ + "get file": { + url: "/overlay-file/pub/non-template", + body: "Non-template: {{.Id}}", + status: 200, + }, + "getting a missing file returns 404": { + url: "/overlay-file/pub/does-not-exist", + body: "", + status: 404, + }, + "get raw template": { + url: "/overlay-file/pub/template.ww", + body: "Template: {{.Id}}", + status: 200, + }, + "get rendered template": { + url: "/overlay-file/pub/template.ww?render=n1", + body: "Template: n1", + status: 200, + }, + "get rendered template without explicit suffix": { + url: "/overlay-file/pub/template?render=n1", + body: "Template: n1", + status: 200, + }, + "explicit suffix required when no node specified": { + url: "/overlay-file/pub/template", + body: "", + status: 404, + }, + "getting a template with a missing node returns 404": { + url: "/overlay-file/pub/test.template.ww?render=n2", + body: "", + status: 404, + }, + "don't render non-template files": { + url: "/overlay-file/pub/non-template?render=n1", + body: "Non-template: {{.Id}}", + status: 200, + }, + "getting a missing template returns 404": { + url: "/overlay-file/pub/does-not-exist.ww?render=n1", + body: "", + status: 404, + }, + "get a file from a subdir": { + url: "/overlay-file/pub/subdir/non-template", + body: "Non-template (subdir): {{.Id}}", + status: 200, + }, + "render a template from a subdir": { + url: "/overlay-file/pub/subdir/template.ww?render=n1", + body: "Template (subdir): n1", + status: 200, + }, +} + +func Test_OverlaySend(t *testing.T) { + env := testenv.New(t) + env.WriteFile("etc/warewulf/warewulf.conf", ` +warewulf: + secure: false +`) + env.WriteFile("etc/warewulf/nodes.conf", ` +nodeprofiles: + default: {} +nodes: + n1: {} +`) + _ = env.Configure() + env.WriteFile("var/lib/warewulf/overlays/pub/rootfs/non-template", "Non-template: {{.Id}}") + env.WriteFile("var/lib/warewulf/overlays/pub/rootfs/template.ww", "Template: {{.Id}}") + env.WriteFile("var/lib/warewulf/overlays/pub/rootfs/subdir/non-template", "Non-template (subdir): {{.Id}}") + env.WriteFile("var/lib/warewulf/overlays/pub/rootfs/subdir/template.ww", "Template (subdir): {{.Id}}") + + for description, tt := range overlaySendTests { + t.Run(description, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, tt.url, nil) + w := httptest.NewRecorder() + OverlaySend(w, req) + res := w.Result() + defer res.Body.Close() + + data, readErr := io.ReadAll(res.Body) + assert.NoError(t, readErr) + if tt.body != "" { + assert.Equal(t, tt.body, string(data)) + } + assert.Equal(t, tt.status, res.StatusCode) + }) + } +} diff --git a/internal/pkg/warewulfd/warewulfd.go b/internal/pkg/warewulfd/warewulfd.go index 8b0709b4..152611f7 100644 --- a/internal/pkg/warewulfd/warewulfd.go +++ b/internal/pkg/warewulfd/warewulfd.go @@ -73,6 +73,7 @@ func RunServer() error { wwHandler.HandleFunc("/image/", ProvisionSend) wwHandler.HandleFunc("/overlay-system/", ProvisionSend) wwHandler.HandleFunc("/overlay-runtime/", ProvisionSend) + wwHandler.HandleFunc("/overlay-file/", OverlaySend) wwHandler.HandleFunc("/status", StatusSend) conf := warewulfconf.Get()