Merge pull request #1659 from anderbubble/warewulfd-overlays-and-http
Added warewulfd:/overlay-file/{overlay}/{path...}?render={nodeID}
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
157
internal/pkg/warewulfd/overlay.go
Normal file
157
internal/pkg/warewulfd/overlay.go
Normal 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
|
||||
}
|
||||
110
internal/pkg/warewulfd/overlay_test.go
Normal file
110
internal/pkg/warewulfd/overlay_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user