Support for creating and updating overlay file in wwapi

Signed-off-by: jason yang <jasonyangshadow@gmail.com>
This commit is contained in:
jason yang
2025-05-01 06:29:40 +00:00
committed by Jonathon Anderson
parent 7b2c0901ed
commit 62d1aa654d
8 changed files with 231 additions and 37 deletions

View File

@@ -114,6 +114,25 @@ func (overlay Overlay) IsDistributionOverlay() bool {
return path.Dir(overlay.Path()) == config.Get().Paths.DistributionOverlaydir()
}
func (overlay Overlay) CreateOverlayFile(filePath string, content []byte, force bool) error {
wwlog.Info("Creating file %s in overlay %s, force: %v", filePath, overlay.Name(), force)
fullPath := overlay.File(filePath)
// create necessary parent directories
if err := os.MkdirAll(path.Dir(fullPath), 0o755); err != nil {
return fmt.Errorf("failed to create parent directories for %s: %w", fullPath, err)
}
// if the file already exists and force is false, return an error
if util.IsFile(fullPath) {
if force {
return os.WriteFile(fullPath, content, 0o644)
}
return fmt.Errorf("file %s already exists in overlay %s", filePath, overlay.Name())
}
return os.WriteFile(fullPath, content, 0o644)
}
// DeleteFile deletes a file or the entire overlay directory.
// If the file belongs to a distribution overlay, it will be cloned to a site overlay
// before deletion.

View File

@@ -20,7 +20,7 @@ import (
)
func Test_FindOverlays(t *testing.T) {
var tests = map[string]struct {
tests := map[string]struct {
distOverlays []string
siteOverlays []string
overlayList []string
@@ -75,7 +75,7 @@ func Test_OverlayMethods(t *testing.T) {
env.WriteFile(path.Join(sitedir, "both/rootfs/testfile"), "the site version")
env.WriteFile(path.Join(distdir, "both/rootfs/testfile"), "the distribution version")
var tests = map[string]struct {
tests := map[string]struct {
name string
path string
rootfs string
@@ -157,7 +157,7 @@ func Test_OverlayMethods(t *testing.T) {
}
func Test_BuildOverlayIndir(t *testing.T) {
var tests = map[string]struct {
tests := map[string]struct {
node node.Node
overlays []string
overlayFiles map[string]string
@@ -336,7 +336,7 @@ Tags:map[]
}
func Test_BuildOverlay(t *testing.T) {
var tests = []struct {
tests := []struct {
description string
nodeName string
context string
@@ -377,7 +377,7 @@ func Test_BuildOverlay(t *testing.T) {
overlays: []string{"o1"},
image: "o1.img",
contents: []string{"o1.txt"},
perms: []int{0644},
perms: []int{0o644},
},
{
description: "if multiple overlays are specified without a node, then the combined overlay is built directly in the overlay directory",
@@ -386,7 +386,7 @@ func Test_BuildOverlay(t *testing.T) {
overlays: []string{"o1", "o2"},
image: "o1-o2.img",
contents: []string{"o1.txt", "o2.txt"},
perms: []int{0644, 0644},
perms: []int{0o644, 0o644},
},
{
description: "if a single node overlay is specified, then the overlay is built in a node overlay directory",
@@ -395,7 +395,7 @@ func Test_BuildOverlay(t *testing.T) {
overlays: []string{"o1"},
image: "node1/o1.img",
contents: []string{"o1.txt"},
perms: []int{0644},
perms: []int{0o644},
},
{
description: "if multiple node overlays are specified, then the combined overlay is built in a node overlay directory",
@@ -404,7 +404,7 @@ func Test_BuildOverlay(t *testing.T) {
overlays: []string{"o1", "o2"},
image: "node1/o1-o2.img",
contents: []string{"o1.txt", "o2.txt"},
perms: []int{0644, 0644},
perms: []int{0o644, 0o644},
},
{
description: "if no node system overlays are specified, then context pointed overlay is generated",
@@ -429,7 +429,7 @@ func Test_BuildOverlay(t *testing.T) {
overlays: []string{"o1"},
image: "node1/__SYSTEM__.img",
contents: []string{"o1.txt"},
perms: []int{0644},
perms: []int{0o644},
},
{
description: "if a single node runtime overlay is specified, then a runtime overlay image is generated in a node overlay directory",
@@ -438,7 +438,7 @@ func Test_BuildOverlay(t *testing.T) {
overlays: []string{"o1"},
image: "node1/__RUNTIME__.img",
contents: []string{"o1.txt"},
perms: []int{0644},
perms: []int{0o644},
},
{
description: "if multiple node system overlays are specified, then a system overlay image is generated with the contents of both overlays",
@@ -447,7 +447,7 @@ func Test_BuildOverlay(t *testing.T) {
overlays: []string{"o1", "o2"},
image: "node1/__SYSTEM__.img",
contents: []string{"o1.txt", "o2.txt"},
perms: []int{0644, 0644},
perms: []int{0o644, 0o644},
},
{
description: "if multiple node runtime overlays are specified, then a runtime overlay image is generated with the contents of both overlays",
@@ -456,7 +456,7 @@ func Test_BuildOverlay(t *testing.T) {
overlays: []string{"o1", "o2"},
image: "node1/__RUNTIME__.img",
contents: []string{"o1.txt", "o2.txt"},
perms: []int{0644, 0644},
perms: []int{0o644, 0o644},
},
{
description: "validating altered permissions are retained",
@@ -465,7 +465,7 @@ func Test_BuildOverlay(t *testing.T) {
overlays: []string{"o3"},
image: "node1/__RUNTIME__.img",
contents: []string{"subdir", "subdir/o3.txt"},
perms: []int{0700, 0600},
perms: []int{0o700, 0o600},
},
}
@@ -473,12 +473,12 @@ func Test_BuildOverlay(t *testing.T) {
defer env.RemoveAll()
env.CreateFile("var/lib/warewulf/overlays/o1/rootfs/o1.txt")
env.Chmod("var/lib/warewulf/overlays/o1/rootfs/o1.txt", 0644)
env.Chmod("var/lib/warewulf/overlays/o1/rootfs/o1.txt", 0o644)
env.CreateFile("var/lib/warewulf/overlays/o2/rootfs/o2.txt")
env.Chmod("var/lib/warewulf/overlays/o2/rootfs/o2.txt", 0644)
env.Chmod("var/lib/warewulf/overlays/o2/rootfs/o2.txt", 0o644)
env.CreateFile("var/lib/warewulf/overlays/o3/rootfs/subdir/o3.txt.ww")
env.Chmod("var/lib/warewulf/overlays/o3/rootfs/subdir", 0700)
env.Chmod("var/lib/warewulf/overlays/o3/rootfs/subdir/o3.txt.ww", 0600)
env.Chmod("var/lib/warewulf/overlays/o3/rootfs/subdir", 0o700)
env.Chmod("var/lib/warewulf/overlays/o3/rootfs/subdir/o3.txt.ww", 0o600)
for _, tt := range tests {
nodeInfo := node.NewNode(tt.nodeName)
@@ -508,7 +508,7 @@ func Test_BuildOverlay(t *testing.T) {
}
func Test_BuildAllOverlays(t *testing.T) {
var tests = []struct {
tests := []struct {
description string
nodes []string
systemOverlays [][]string
@@ -585,8 +585,8 @@ func Test_BuildAllOverlays(t *testing.T) {
assert.NoError(t, overlayDirErr)
defer os.RemoveAll(overlayDir)
conf.Paths.WWOverlaydir = overlayDir
assert.NoError(t, os.Mkdir(path.Join(overlayDir, "o1"), 0700))
assert.NoError(t, os.Mkdir(path.Join(overlayDir, "o2"), 0700))
assert.NoError(t, os.Mkdir(path.Join(overlayDir, "o1"), 0o700))
assert.NoError(t, os.Mkdir(path.Join(overlayDir, "o2"), 0o700))
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
@@ -620,7 +620,7 @@ func Test_BuildAllOverlays(t *testing.T) {
}
func Test_BuildSpecificOverlays(t *testing.T) {
var tests = []struct {
tests := []struct {
description string
nodes []string
overlays []string
@@ -683,8 +683,8 @@ func Test_BuildSpecificOverlays(t *testing.T) {
assert.NoError(t, overlayDirErr)
defer os.RemoveAll(overlayDir)
conf.Paths.WWOverlaydir = overlayDir
assert.NoError(t, os.Mkdir(path.Join(overlayDir, "o1"), 0700))
assert.NoError(t, os.Mkdir(path.Join(overlayDir, "o2"), 0700))
assert.NoError(t, os.Mkdir(path.Join(overlayDir, "o1"), 0o700))
assert.NoError(t, os.Mkdir(path.Join(overlayDir, "o2"), 0o700))
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
@@ -711,6 +711,39 @@ func Test_BuildSpecificOverlays(t *testing.T) {
}
}
func Test_CreateOverlayFile(t *testing.T) {
tests := []struct {
name string
overlayName string
filePath string
content []byte
force bool
}{
{"create file", "test", "newfile.ww", []byte("new file"), false},
{"overwrite existing file", "test", "existingfile.ww", []byte("overwrite file"), true},
}
conf := warewulfconf.Get()
overlayDir, overlayDirErr := os.MkdirTemp(os.TempDir(), "ww-test-overlay-*")
assert.NoError(t, overlayDirErr)
defer os.RemoveAll(overlayDir)
conf.Paths.WWOverlaydir = overlayDir
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
newOverlay := GetSiteOverlay(tt.overlayName)
err := newOverlay.CreateOverlayFile(tt.filePath, tt.content, tt.force)
assert.NoError(t, err)
newFile := newOverlay.File(tt.filePath)
assert.FileExists(t, newFile)
readContent, err := os.ReadFile(newFile)
assert.NoError(t, err)
assert.Equal(t, tt.content, readContent)
})
}
}
func dirIsEmpty(t *testing.T, name string) bool {
f, err := os.Open(name)
if err != nil {

View File

@@ -36,7 +36,6 @@ func FirstError(errs ...error) (err error) {
}
func DirModTime(path string) (time.Time, error) {
var lastTime time.Time
err := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err != nil {
@@ -186,7 +185,8 @@ func FindFilterFiles(
path string,
includePattern []string,
ignorePattern []string,
ignore_xdev bool) (ofiles []string, err error) {
ignore_xdev bool,
) (ofiles []string, err error) {
wwlog.Debug("Finding files: %s include: %s ignore: %s", path, includePattern, ignorePattern)
// Preprocess patterns to remove leading (and trailing) /, as we are handling relative paths
@@ -347,7 +347,7 @@ Appending the lines to the given file
*/
func AppendLines(fileName string, lines []string) error {
wwlog.Verbose("appending %v lines to %s", len(lines), fileName)
file, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY, 0644)
file, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
return fmt.Errorf("can't open file: %s: %w", fileName, err)
}
@@ -362,6 +362,21 @@ func AppendLines(fileName string, lines []string) error {
return nil
}
func OverwriteFile(fileName string, content []byte) error {
wwlog.Verbose("overwrite file %s", fileName)
file, err := os.OpenFile(fileName, os.O_RDWR|os.O_TRUNC, 0o644)
if err != nil {
return fmt.Errorf("failed to open file: %s, err: %w", fileName, err)
}
defer file.Close()
_, err = file.Write(content)
if err != nil {
return fmt.Errorf("while writing file: %s, err: %w", fileName, err)
}
return file.Sync()
}
/*
******************************************************************************
@@ -372,14 +387,15 @@ func CpioCreate(
ifiles []string,
ofile string,
format string,
cpio_args ...string) (err error) {
cpio_args ...string,
) (err error) {
args := []string{
"--quiet",
"--create",
"--directory", rootdir,
"--format", format,
"--file=" + ofile}
"--file=" + ofile,
}
args = append(args, cpio_args...)
@@ -413,13 +429,12 @@ func CpioCreate(
Compress a file using gzip or pigz
*/
func FileGz(
file string) (err error) {
file string,
) (err error) {
file_gz := file + ".gz"
if IsFile(file_gz) {
err := os.Remove(file_gz)
if err != nil {
return fmt.Errorf("could not remove existing file: %s: %w", file_gz, err)
}
@@ -509,9 +524,9 @@ func BuildFsImage(
ignore []string,
ignore_xdev bool,
format string,
cpio_args ...string) (err error) {
err = os.MkdirAll(path.Dir(imagePath), 0755)
cpio_args ...string,
) (err error) {
err = os.MkdirAll(path.Dir(imagePath), 0o755)
if err != nil {
return fmt.Errorf("failed to create image directory for %s: %s: %w", name, imagePath, err)
}

View File

@@ -9,7 +9,7 @@ import (
)
func Test_FindFiles(t *testing.T) {
var tests = map[string]struct {
tests := map[string]struct {
createFiles []string
findFiles []string
}{
@@ -43,7 +43,7 @@ func Test_FindFiles(t *testing.T) {
}
func Test_FindFilterFiles(t *testing.T) {
var tests = map[string]struct {
tests := map[string]struct {
createFiles []string
include []string
exclude []string
@@ -92,3 +92,19 @@ func Test_FindFilterFiles(t *testing.T) {
})
}
}
func Test_Overwrite(t *testing.T) {
t.Run("overwrite a file", func(t *testing.T) {
env := testenv.New(t)
defer env.RemoveAll()
env.CreateFile("file")
env.WriteFile("file", "hello world")
assert.Equal(t, "hello world", env.ReadFile("file"))
err := OverwriteFile(env.GetPath("file"), []byte("hello warewulf"))
assert.NoError(t, err)
assert.Equal(t, "hello warewulf", env.ReadFile("file"))
})
}

View File

@@ -71,8 +71,10 @@ func Handler(auth *config.Authentication, allowedNets []net.IPNet) *web.Service
r.Method(http.MethodGet, "/{name}", nethttp.NewHandler(getOverlayByName()))
r.Method(http.MethodGet, "/{name}/file", nethttp.NewHandler(getOverlayFile()))
r.Method(http.MethodPut, "/{name}", nethttp.NewHandler(createOverlay()))
r.Method(http.MethodPut, "/{name}/file", nethttp.NewHandler(createOverlayFile()))
r.Method(http.MethodDelete, "/{name}", nethttp.NewHandler(deleteOverlay()))
r.Method(http.MethodDelete, "/{name}/file", nethttp.NewHandler(deleteOverlayFile()))
r.Method(http.MethodPut, "/{name}/file", nethttp.NewHandler(updateOverlayFile()))
})
})

View File

@@ -215,6 +215,39 @@ func createOverlay() usecase.Interactor {
return u
}
func createOverlayFile() usecase.Interactor {
type createOverlayFileInput struct {
Name string `path:"name" required:"true" description:"Name of overlay to create a file in"`
Path string `query:"path" required:"true" description:"Path to file to create in an overlay"`
Force bool `query:"force" default:"false" description:"Whether to forcefully create an overlay file, default:'false'"`
Content string `json:"content" required:"true" description:"Content of the file to create"`
}
u := usecase.NewInteractor(func(ctx context.Context, input createOverlayFileInput, output *OverlayResponse) error {
wwlog.Debug("api.createOverlayFile(Name:%v, Path:%v, Force: %v)", input.Name, input.Path, input.Force)
if input.Path == "" {
return status.Wrap(fmt.Errorf("must specify a path"), status.InvalidArgument)
}
if input.Content == "" {
return status.Wrap(fmt.Errorf("content should not be empty"), status.InvalidArgument)
}
if relPath, err := url.QueryUnescape(input.Path); err != nil {
return fmt.Errorf("failed to decode path: %v: %w", input.Path, err)
} else {
newOverlay := overlay.GetOverlay(input.Name)
if err := newOverlay.CreateOverlayFile(relPath, []byte(input.Content), input.Force); err != nil {
return fmt.Errorf("unable to create overlay file %v: %v: %w", input.Name, relPath, err)
}
*output = *NewOverlayResponse(input.Name)
return nil
}
})
u.SetTitle("Create a file in an overlay")
u.SetDescription("Create a file in an overlay from the overlay name and file path.")
u.SetTags("Overlay")
return u
}
func deleteOverlay() usecase.Interactor {
type deleteOverlayInput struct {
Name string `path:"name" required:"true" description:"Name of overlay to delete"`
@@ -288,3 +321,37 @@ func deleteOverlayFile() usecase.Interactor {
u.SetTags("Overlay")
return u
}
func updateOverlayFile() usecase.Interactor {
type updateOverlayInput struct {
Name string `path:"name" required:"true" description:"Name of overlay to update"`
Path string `query:"path" required:"true" description:"Path to file to get from an overlay"`
Content string `json:"content" required:"true" description:"New content"`
}
u := usecase.NewInteractor(func(ctx context.Context, input updateOverlayInput, output *OverlayResponse) error {
wwlog.Debug("api.updateOverlay(Name: %s)", input.Name)
if input.Path == "" {
return status.Wrap(fmt.Errorf("must specify a path"), status.InvalidArgument)
}
if input.Content == "" {
return status.Wrap(fmt.Errorf("new content should not be empty"), status.InvalidArgument)
}
*output = *NewOverlayResponse(input.Name)
if relPath, err := url.QueryUnescape(input.Path); err != nil {
return status.Wrap(fmt.Errorf("failed to decode path"), status.InvalidArgument)
} else {
overlay_ := overlay.GetOverlay(input.Name)
file := overlay_.File(relPath)
if err := util.OverwriteFile(file, []byte(input.Content)); err != nil {
return fmt.Errorf("failed to overwrite file: %s, err: %w", file, err)
}
}
return nil
})
u.SetTitle("Update an overlay")
u.SetDescription("Update an overlay")
u.SetTags("Overlay")
return u
}

View File

@@ -1,6 +1,7 @@
package api
import (
"bytes"
"io"
"net"
"net/http"
@@ -89,6 +90,46 @@ func TestOverlayAPI(t *testing.T) {
}`)
})
t.Run("update overlay file", func(t *testing.T) {
req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/overlays/testoverlay/file?path=email.ww", bytes.NewReader([]byte("{\"content\":\"hello world\"}")))
assert.NoError(t, err)
// set request
resp, err := http.DefaultTransport.RoundTrip(req)
assert.NoError(t, err)
// validate the resp
body, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.NoError(t, resp.Body.Close())
assert.JSONEq(t, `{"files":["/email.ww"], "site":false}`, string(body))
// get again
req, err = http.NewRequest(http.MethodGet, srv.URL+"/api/overlays/testoverlay/file?path=email.ww", nil)
assert.NoError(t, err)
// send request
resp, err = http.DefaultTransport.RoundTrip(req)
assert.NoError(t, err)
// validate the resp
body, err = io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.NoError(t, resp.Body.Close())
ja := jsonassert.New(t)
ja.Assert(string(body), `
{
"overlay": "testoverlay",
"path": "email.ww",
"contents": "hello world",
"perms": "<<PRESENCE>>",
"uid": "<<PRESENCE>>",
"gid": "<<PRESENCE>>"
}`)
})
t.Run("create an overlay", func(t *testing.T) {
req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/overlays/test", nil)
assert.NoError(t, err)