Added warewulfd:/overlay-file/{overlay}/{path...}?render={nodeID}

Co-authored-by: Jonathon Anderson <janderson@ciq.com>
Signed-off-by: Christian Goll <cgoll@suse.com>
Signed-off-by: Jonathon Anderson <janderson@ciq.com>
This commit is contained in:
Christian Goll
2025-01-28 13:52:42 -07:00
committed by Jonathon Anderson
parent 0d81f2e521
commit 755411d3cb
5 changed files with 276 additions and 3 deletions

View File

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

View File

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

View File

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

View File

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