refactored overlay class

overlay.GetOverlay(name) returns now an error if the overlay doesn't
exist. This is the most canonical way to act if there is no overlay.
This commit is contained in:
Christian Goll
2025-08-04 15:18:51 +02:00
committed by Jonathon Anderson
parent c17fe8d512
commit 6f4fd60d8f
32 changed files with 1105 additions and 437 deletions

View File

@@ -89,7 +89,8 @@ func OverlayList(cmd *cobra.Command, args []string, toComplete string) ([]string
}
func OverlayFiles(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
ret, _ := overlay.OverlayGetFiles(args[0])
myOverlay, _ := overlay.GetOverlay(args[0])
ret, _ := myOverlay.GetFiles()
return ret, cobra.ShellCompDirectiveNoFileComp
}

View File

@@ -1,21 +0,0 @@
package chmod
import (
"testing"
)
// TestArgsOverlayChmod is a regression test for 260.
// Two arguments should fail, three should succeed.
func TestArgsOverlayChmod(t *testing.T) {
command := GetCommand()
err := command.Args(command, []string{"overlay_name", "file_name"})
if err == nil {
t.Errorf("two arguments to overlay chmod should fail")
}
err = command.Args(command, []string{"overlay_name", "file_name", "0755"})
if err != nil {
t.Errorf("three arguments to overlay chmod should succeed")
}
}

View File

@@ -2,42 +2,22 @@ package chmod
import (
"fmt"
"os"
"strconv"
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/pkg/overlay"
"github.com/warewulf/warewulf/internal/pkg/util"
)
func CobraRunE(cmd *cobra.Command, args []string) error {
overlayName := args[0]
fileName := args[1]
myOverlay, err := overlay.GetOverlay(args[0])
if err != nil {
return err
}
path := args[1]
permissionMode, err := strconv.ParseUint(args[2], 8, 32)
if err != nil {
return fmt.Errorf("could not convert requested mode: %s", err)
}
overlay_ := overlay.GetOverlay(overlayName)
if !overlay_.IsSiteOverlay() {
overlay_, err = overlay_.CloneSiteOverlay()
if err != nil {
return err
}
}
if !overlay_.Exists() {
return fmt.Errorf("overlay does not exist: %s", overlayName)
}
overlayFile := overlay_.File(fileName)
if !(util.IsFile(overlayFile) || util.IsDir(overlayFile)) {
return fmt.Errorf("file does not exist within overlay: %s:%s", overlayName, fileName)
}
err = os.Chmod(overlayFile, os.FileMode(permissionMode))
if err != nil {
return fmt.Errorf("could not set permission: %s", err)
}
return nil
return myOverlay.Chmod(path, permissionMode)
}

View File

@@ -0,0 +1,95 @@
package chmod
import (
"bytes"
"io/fs"
"os"
"path/filepath"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/warewulf/warewulf/internal/pkg/testenv"
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
)
func Test_OverlayChmod(t *testing.T) {
tests := []struct {
name string
overlayName string
fileName string
perm string
wantErr bool
system bool
startPerm fs.FileMode
}{
{
name: "simple chmod",
overlayName: "test-overlay",
fileName: "test.txt",
perm: "0600",
wantErr: false,
system: false,
startPerm: 0644,
},
{
name: "system overlay chmod",
overlayName: "wwinit", // A known system overlay
fileName: "init.sh",
perm: "0700",
wantErr: false,
system: true,
startPerm: 0755,
},
}
warewulfd.SetNoDaemon()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
env := testenv.New(t)
conf := env.Configure()
// Setup overlay
var overlayDir string
if tt.system {
overlayDir = filepath.Join(conf.Paths.DistributionOverlaydir(), tt.overlayName)
} else {
overlayDir = filepath.Join(conf.Paths.WWOverlaydir, tt.overlayName)
}
err := os.MkdirAll(overlayDir, 0755)
assert.NoError(t, err)
filePath := filepath.Join(overlayDir, tt.fileName)
err = os.WriteFile(filePath, []byte("test"), tt.startPerm)
assert.NoError(t, err)
baseCmd := GetCommand()
baseCmd.SetArgs([]string{tt.overlayName, tt.fileName, tt.perm})
buf := new(bytes.Buffer)
baseCmd.SetOut(buf)
baseCmd.SetErr(buf)
err = baseCmd.Execute()
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
// Check permissions
// for system overlays, the file is copied to a site overlay
if tt.system {
overlayDir = filepath.Join(conf.Paths.WWOverlaydir, tt.overlayName)
filePath = filepath.Join(overlayDir, tt.fileName)
}
stat, err := os.Stat(filePath)
assert.NoError(t, err)
if !tt.wantErr {
mode, err := strconv.ParseUint(tt.perm, 8, 32)
assert.NoError(t, err)
assert.Equal(t, fs.FileMode(mode), stat.Mode().Perm())
} else {
assert.Equal(t, tt.startPerm, stat.Mode().Perm())
}
})
}
}

View File

@@ -2,58 +2,44 @@ package chown
import (
"fmt"
"os"
"strconv"
"github.com/warewulf/warewulf/internal/pkg/overlay"
"github.com/warewulf/warewulf/internal/pkg/util"
"strings"
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/pkg/overlay"
)
func CobraRunE(cmd *cobra.Command, args []string) error {
var uid int
var gid int
fileName := args[1]
chownSpec := args[2]
var uid, gid = -1, -1
var err error
overlayName := args[0]
fileName := args[1]
uid, err = strconv.Atoi(args[2])
if err != nil {
return fmt.Errorf("UID is not an integer: %s", args[2])
}
if len(args) > 3 {
gid, err = strconv.Atoi(args[3])
if err != nil {
return fmt.Errorf("GID is not an integer: %s", args[3])
if strings.Contains(chownSpec, ":") {
parts := strings.SplitN(chownSpec, ":", 2)
if parts[0] != "" {
uid, err = strconv.Atoi(parts[0])
if err != nil {
return fmt.Errorf("UID is not an integer: %s", parts[0])
}
}
if parts[1] != "" {
gid, err = strconv.Atoi(parts[1])
if err != nil {
return fmt.Errorf("GID is not an integer: %s", parts[1])
}
}
} else {
gid = -1
}
overlay_ := overlay.GetOverlay(overlayName)
if !overlay_.Exists() {
return fmt.Errorf("overlay does not exist: %s", overlayName)
}
if !overlay_.IsSiteOverlay() {
overlay_, err = overlay_.CloneSiteOverlay()
uid, err = strconv.Atoi(chownSpec)
if err != nil {
return err
return fmt.Errorf("UID is not an integer: %s", chownSpec)
}
}
overlayFile := overlay_.File(fileName)
if !(util.IsFile(overlayFile) || util.IsDir(overlayFile)) {
return fmt.Errorf("file does not exist within overlay: %s:%s", overlayName, fileName)
}
err = os.Chown(overlayFile, uid, gid)
myOverlay, err := overlay.GetOverlay(args[0])
if err != nil {
return fmt.Errorf("could not set ownership: %s", err)
return err
}
return nil
return myOverlay.Chown(fileName, uid, gid)
}

View File

@@ -0,0 +1,156 @@
package chown
import (
"bytes"
"os"
"path/filepath"
"strconv"
"syscall"
"testing"
"github.com/stretchr/testify/assert"
"github.com/warewulf/warewulf/internal/pkg/testenv"
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
)
func Test_OverlayChown(t *testing.T) {
currentUser := strconv.Itoa(os.Getuid())
currentGroup := strconv.Itoa(os.Getgid())
tests := []struct {
name string
overlayName string
fileName string
chownSpec string
wantErr bool
system bool
checkUser bool
checkGroup bool
}{
{
name: "simple chown user and group",
overlayName: "test-overlay-1",
fileName: "test.txt",
chownSpec: currentUser + ":" + currentGroup,
wantErr: false,
system: false,
checkUser: true,
checkGroup: true,
},
{
name: "simple chown user only",
overlayName: "test-overlay-2",
fileName: "test.txt",
chownSpec: currentUser,
wantErr: false,
system: false,
checkUser: true,
checkGroup: false, // group should not change
},
{
name: "simple chown group only",
overlayName: "test-overlay-3",
fileName: "test.txt",
chownSpec: ":" + currentGroup,
wantErr: false,
system: false,
checkUser: false, // user should not change
checkGroup: true,
},
{
name: "system overlay chown",
overlayName: "wwinit", // A known system overlay
fileName: "init.sh",
chownSpec: currentUser + ":" + currentGroup,
wantErr: false,
system: true,
checkUser: true,
checkGroup: true,
},
{
name: "bad chown spec",
overlayName: "test-overlay-4",
fileName: "test.txt",
chownSpec: "bad:bad",
wantErr: true,
system: false,
checkUser: false,
checkGroup: false,
},
}
warewulfd.SetNoDaemon()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
env := testenv.New(t)
conf := env.Configure()
// Setup overlay
var overlayDir string
if tt.system {
overlayDir = filepath.Join(conf.Paths.DistributionOverlaydir(), tt.overlayName)
} else {
overlayDir = filepath.Join(conf.Paths.WWOverlaydir, tt.overlayName)
}
err := os.MkdirAll(overlayDir, 0755)
assert.NoError(t, err)
filePath := filepath.Join(overlayDir, tt.fileName)
f, err := os.Create(filePath)
assert.NoError(t, err)
f.Close()
// get initial owner
stat, err := os.Stat(filePath)
assert.NoError(t, err)
sysStat, ok := stat.Sys().(*syscall.Stat_t)
assert.True(t, ok)
startUID := int(sysStat.Uid)
startGID := int(sysStat.Gid)
baseCmd := GetCommand()
baseCmd.SetArgs([]string{tt.overlayName, tt.fileName, tt.chownSpec})
buf := new(bytes.Buffer)
baseCmd.SetOut(buf)
baseCmd.SetErr(buf)
err = baseCmd.Execute()
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
// Check ownership
// for system overlays, the file is copied to a site overlay
if tt.system {
overlayDir = filepath.Join(conf.Paths.WWOverlaydir, tt.overlayName)
filePath = filepath.Join(overlayDir, tt.fileName)
}
stat, err = os.Stat(filePath)
assert.NoError(t, err)
sysStat, ok = stat.Sys().(*syscall.Stat_t)
assert.True(t, ok)
endUID := int(sysStat.Uid)
endGID := int(sysStat.Gid)
if !tt.wantErr {
if tt.checkUser {
u, err := strconv.Atoi(currentUser)
assert.NoError(t, err)
assert.Equal(t, u, endUID)
} else {
assert.Equal(t, startUID, endUID)
}
if tt.checkGroup {
g, err := strconv.Atoi(currentGroup)
assert.NoError(t, err)
assert.Equal(t, g, endGID)
} else {
assert.Equal(t, startGID, endGID)
}
} else {
assert.Equal(t, startUID, endUID)
assert.Equal(t, startGID, endGID)
}
})
}
}

View File

@@ -6,5 +6,6 @@ import (
)
func CobraRunE(cmd *cobra.Command, args []string) (err error) {
return overlay.GetSiteOverlay(args[0]).Create()
_, err = overlay.Create(args[0])
return err
}

View File

@@ -0,0 +1,63 @@
package create
import (
"bytes"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/warewulf/warewulf/internal/pkg/testenv"
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
)
func Test_OverlayCreate(t *testing.T) {
tests := []struct {
name string
overlayName string
setup func(t *testing.T, overlayDir string)
wantErr bool
}{
{
name: "simple create",
overlayName: "test-overlay",
setup: func(t *testing.T, overlayDir string) {},
wantErr: false,
},
{
name: "create existing overlay",
overlayName: "test-overlay-exists",
setup: func(t *testing.T, overlayDir string) {
err := os.MkdirAll(overlayDir, 0755)
assert.NoError(t, err)
},
wantErr: true,
},
}
warewulfd.SetNoDaemon()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
env := testenv.New(t)
conf := env.Configure()
overlayDir := filepath.Join(conf.Paths.WWOverlaydir, tt.overlayName)
tt.setup(t, overlayDir)
baseCmd := GetCommand()
baseCmd.SetArgs([]string{tt.overlayName})
buf := new(bytes.Buffer)
baseCmd.SetOut(buf)
baseCmd.SetErr(buf)
err := baseCmd.Execute()
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
_, err := os.Stat(overlayDir)
assert.NoError(t, err, "overlay directory should have been created")
}
})
}
}

View File

@@ -1,81 +1,20 @@
package delete
import (
"fmt"
"os"
"path"
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/pkg/overlay"
"github.com/warewulf/warewulf/internal/pkg/util"
"github.com/warewulf/warewulf/internal/pkg/wwlog"
)
func CobraRunE(cmd *cobra.Command, args []string) error {
var fileName string
overlayName := args[0]
fileName := ""
if len(args) == 2 {
fileName = args[1]
}
overlay_ := overlay.GetOverlay(overlayName)
if overlay_.IsDistributionOverlay() {
return fmt.Errorf("distribution overlay can't deleted")
myOverlay, err := overlay.GetOverlay(args[0])
if err != nil {
return err
}
if !overlay_.Exists() {
return fmt.Errorf("overlay does not exist: %s", overlayName)
}
if fileName == "" {
if Force {
err := os.RemoveAll(overlay_.Path())
if err != nil {
return fmt.Errorf("failed deleting overlay: %w", err)
}
} else {
err := os.Remove(overlay_.Path())
if err != nil {
return fmt.Errorf("failed deleting overlay: %w", err)
}
}
wwlog.Info("Deleted overlay: %s\n", args[0])
} else {
removePath := overlay_.File(fileName)
if !(util.IsDir(removePath) || util.IsFile(removePath)) {
return fmt.Errorf("path to remove doesn't exist in overlay: %s", removePath)
}
if Force {
err := os.RemoveAll(removePath)
if err != nil {
return fmt.Errorf("failed deleting file from overlay: %s:%s", overlayName, removePath)
}
} else {
err := os.Remove(removePath)
if err != nil {
return fmt.Errorf("failed deleting overlay: %s:%s", overlayName, removePath)
}
}
if Parents {
// Cleanup any empty directories left behind...
i := path.Dir(removePath)
for i != overlay_.Rootfs() {
wwlog.Debug("Evaluating directory to remove: %s", i)
err := os.Remove(i)
if err != nil {
break
}
wwlog.Verbose("Removed empty directory: %s", i)
i = path.Dir(i)
}
}
}
return nil
return myOverlay.DeleteFile(fileName, Force, Parents)
}

View File

@@ -0,0 +1,141 @@
package delete
import (
"bytes"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/warewulf/warewulf/internal/pkg/config"
"github.com/warewulf/warewulf/internal/pkg/testenv"
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
)
func Test_OverlayDelete(t *testing.T) {
tests := []struct {
name string
overlayName string
fileName string
setup func(t *testing.T, conf *config.WarewulfYaml, overlayName string, fileName string)
wantErr bool
verify func(t *testing.T, conf *config.WarewulfYaml, overlayName string, fileName string)
}{
{
name: "delete site overlay",
overlayName: "test-overlay",
setup: func(t *testing.T, conf *config.WarewulfYaml, overlayName string, fileName string) {
siteOverlayDir := filepath.Join(conf.Paths.SiteOverlaydir(), overlayName)
err := os.MkdirAll(siteOverlayDir, 0755)
assert.NoError(t, err)
},
wantErr: false,
verify: func(t *testing.T, conf *config.WarewulfYaml, overlayName string, fileName string) {
siteOverlayDir := filepath.Join(conf.Paths.SiteOverlaydir(), overlayName)
_, err := os.Stat(siteOverlayDir)
assert.True(t, os.IsNotExist(err), "site overlay directory should have been deleted")
},
},
{
name: "delete file in site overlay",
overlayName: "test-overlay-file",
fileName: "test.txt",
setup: func(t *testing.T, conf *config.WarewulfYaml, overlayName string, fileName string) {
siteOverlayDir := filepath.Join(conf.Paths.SiteOverlaydir(), overlayName)
err := os.MkdirAll(siteOverlayDir, 0755)
assert.NoError(t, err)
err = os.WriteFile(filepath.Join(siteOverlayDir, fileName), []byte("test"), 0644)
assert.NoError(t, err)
},
wantErr: false,
verify: func(t *testing.T, conf *config.WarewulfYaml, overlayName string, fileName string) {
siteOverlayDir := filepath.Join(conf.Paths.SiteOverlaydir(), overlayName)
_, err := os.Stat(filepath.Join(siteOverlayDir, fileName))
assert.True(t, os.IsNotExist(err), "file in site overlay should have been deleted")
_, err = os.Stat(siteOverlayDir)
assert.NoError(t, err, "site overlay directory should still exist")
},
},
{
name: "delete non-existent overlay",
overlayName: "non-existent-overlay",
setup: func(t *testing.T, conf *config.WarewulfYaml, overlayName string, fileName string) {},
wantErr: true,
verify: func(t *testing.T, conf *config.WarewulfYaml, overlayName string, fileName string) {},
},
{
name: "delete distribution overlay",
overlayName: "dist-overlay",
setup: func(t *testing.T, conf *config.WarewulfYaml, overlayName string, fileName string) {
distOverlayDir := filepath.Join(conf.Paths.DistributionOverlaydir(), overlayName)
err := os.MkdirAll(distOverlayDir, 0755)
assert.NoError(t, err)
},
wantErr: true,
verify: func(t *testing.T, conf *config.WarewulfYaml, overlayName string, fileName string) {
distOverlayDir := filepath.Join(conf.Paths.DistributionOverlaydir(), overlayName)
_, err := os.Stat(distOverlayDir)
assert.NoError(t, err, "distribution overlay directory should not have been deleted")
},
},
{
name: "delete file in distribution overlay",
overlayName: "dist-overlay-file",
fileName: "test.txt",
setup: func(t *testing.T, conf *config.WarewulfYaml, overlayName string, fileName string) {
distOverlayDir := filepath.Join(conf.Paths.DistributionOverlaydir(), overlayName)
err := os.MkdirAll(distOverlayDir, 0755)
assert.NoError(t, err)
err = os.WriteFile(filepath.Join(distOverlayDir, fileName), []byte("test"), 0644)
assert.NoError(t, err)
},
// This will trigger a copy-on-write. A site overlay will be created.
wantErr: false,
verify: func(t *testing.T, conf *config.WarewulfYaml, overlayName string, fileName string) {
distOverlayDir := filepath.Join(conf.Paths.DistributionOverlaydir(), overlayName)
_, err := os.Stat(filepath.Join(distOverlayDir, fileName))
assert.NoError(t, err, "file in distribution overlay should still exist")
siteOverlayDir := filepath.Join(conf.Paths.SiteOverlaydir(), overlayName)
_, err = os.Stat(siteOverlayDir)
assert.NoError(t, err, "site overlay should have been created")
_, err = os.Stat(filepath.Join(siteOverlayDir, fileName))
assert.True(t, os.IsNotExist(err), "file should not exist in the new site overlay")
},
},
}
warewulfd.SetNoDaemon()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
env := testenv.New(t)
defer env.RemoveAll()
conf := env.Configure()
tt.setup(t, conf, tt.overlayName, tt.fileName)
var args []string
if tt.fileName != "" {
args = []string{tt.overlayName, tt.fileName, "--force"}
} else {
args = []string{tt.overlayName, "--force"}
}
baseCmd := GetCommand()
baseCmd.SetArgs(args)
buf := new(bytes.Buffer)
baseCmd.SetOut(buf)
baseCmd.SetErr(buf)
err := baseCmd.Execute()
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
tt.verify(t, conf, tt.overlayName, tt.fileName)
})
}
}

View File

@@ -35,19 +35,19 @@ func CobraRunE(cmd *cobra.Command, args []string) (err error) {
overlayName := args[0]
fileName := args[1]
overlay_ := overlay.GetOverlay(overlayName)
if !overlay_.Exists() {
return fmt.Errorf("overlay does not exist: %s", overlayName)
myOverlay, err := overlay.GetOverlay(overlayName)
if err != nil {
return err
}
overlayFile := overlay_.File(fileName)
overlayFile := myOverlay.File(fileName)
wwlog.Debug("Will edit overlay file: %s", overlayFile)
overlayFileDir := path.Dir(overlayFile)
if !(util.IsDir(overlayFileDir) || CreateDirs) {
return fmt.Errorf("%s does not exist. Use '--parents' option to create automatically", overlayFileDir)
}
tempFile, tempFileErr := os.CreateTemp(overlay_.Path(), "ww-overlay-edit-")
tempFile, tempFileErr := os.CreateTemp(myOverlay.Path(), "ww-overlay-edit-")
if tempFileErr != nil {
return fmt.Errorf("unable to create temporary file for editing: %s", tempFileErr)
}
@@ -103,14 +103,14 @@ func CobraRunE(cmd *cobra.Command, args []string) (err error) {
return nil
}
if !overlay_.IsSiteOverlay() {
overlay_, err = overlay_.CloneSiteOverlay()
if !myOverlay.IsSiteOverlay() {
myOverlay, err = myOverlay.CloneSiteOverlay()
if err != nil {
return err
}
}
// re-generate because overlay_ may have changed
overlayFile = overlay_.File(fileName)
overlayFile = myOverlay.File(fileName)
overlayFileDir = path.Dir(overlayFile)
if CreateDirs {

View File

@@ -0,0 +1,217 @@
package edit
import (
"bytes"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/warewulf/warewulf/internal/pkg/config"
"github.com/warewulf/warewulf/internal/pkg/testenv"
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
)
func Test_OverlayEdit(t *testing.T) {
tests := []struct {
name string
overlayName string
fileName string
parents bool
editorScript string
setup func(t *testing.T, conf *config.WarewulfYaml, overlayName, fileName string) (mtime time.Time)
wantErr bool
verify func(t *testing.T, conf *config.WarewulfYaml, overlayName, fileName string, mtime time.Time)
}{
{
name: "edit existing file",
overlayName: "test-overlay",
fileName: "test.txt",
editorScript: `#!/bin/sh
sed -i 's/hello/world/' "$1"`,
setup: func(t *testing.T, conf *config.WarewulfYaml, overlayName, fileName string) time.Time {
overlayDir := filepath.Join(conf.Paths.SiteOverlaydir(), overlayName)
assert.NoError(t, os.MkdirAll(overlayDir, 0755))
assert.NoError(t, os.WriteFile(filepath.Join(overlayDir, fileName), []byte("hello"), 0644))
return time.Time{}
},
wantErr: false,
verify: func(t *testing.T, conf *config.WarewulfYaml, overlayName, fileName string, mtime time.Time) {
overlayDir := filepath.Join(conf.Paths.SiteOverlaydir(), overlayName)
content, err := os.ReadFile(filepath.Join(overlayDir, fileName))
assert.NoError(t, err)
assert.Equal(t, "world", string(content))
},
},
{
name: "edit non-existing file",
overlayName: "test-overlay",
fileName: "new-file.txt",
editorScript: `#!/bin/sh
echo "new content" > "$1"`,
setup: func(t *testing.T, conf *config.WarewulfYaml, overlayName, fileName string) time.Time {
overlayDir := filepath.Join(conf.Paths.SiteOverlaydir(), overlayName)
assert.NoError(t, os.MkdirAll(overlayDir, 0755))
return time.Time{}
},
wantErr: false,
verify: func(t *testing.T, conf *config.WarewulfYaml, overlayName, fileName string, mtime time.Time) {
overlayDir := filepath.Join(conf.Paths.SiteOverlaydir(), overlayName)
content, err := os.ReadFile(filepath.Join(overlayDir, fileName))
assert.NoError(t, err)
assert.Equal(t, "new content\n", string(content))
},
},
{
name: "edit non-existing .ww file",
overlayName: "test-overlay",
fileName: "template.ww",
editorScript: `#!/bin/sh
sed -i 's/autogenerated by warewulf/edited by test/' "$1"`,
setup: func(t *testing.T, conf *config.WarewulfYaml, overlayName, fileName string) time.Time {
overlayDir := filepath.Join(conf.Paths.SiteOverlaydir(), overlayName)
assert.NoError(t, os.MkdirAll(overlayDir, 0755))
return time.Time{}
},
wantErr: false,
verify: func(t *testing.T, conf *config.WarewulfYaml, overlayName, fileName string, mtime time.Time) {
overlayDir := filepath.Join(conf.Paths.SiteOverlaydir(), overlayName)
content, err := os.ReadFile(filepath.Join(overlayDir, fileName))
assert.NoError(t, err)
assert.Contains(t, string(content), "edited by test")
},
},
{
name: "edit without changes",
overlayName: "test-overlay",
fileName: "no-change.txt",
editorScript: `#!/bin/sh
# Do nothing to the file
exit 0`,
setup: func(t *testing.T, conf *config.WarewulfYaml, overlayName, fileName string) time.Time {
overlayDir := filepath.Join(conf.Paths.SiteOverlaydir(), overlayName)
assert.NoError(t, os.MkdirAll(overlayDir, 0755))
filePath := filepath.Join(overlayDir, fileName)
assert.NoError(t, os.WriteFile(filePath, []byte("original"), 0644))
stat, err := os.Stat(filePath)
assert.NoError(t, err)
return stat.ModTime()
},
wantErr: false,
verify: func(t *testing.T, conf *config.WarewulfYaml, overlayName, fileName string, mtime time.Time) {
overlayDir := filepath.Join(conf.Paths.SiteOverlaydir(), overlayName)
filePath := filepath.Join(overlayDir, fileName)
stat, err := os.Stat(filePath)
assert.NoError(t, err)
assert.True(t, stat.ModTime().Equal(mtime), "ModTime should not have changed")
},
},
{
name: "edit file in dist overlay (CoW)",
overlayName: "dist-overlay",
fileName: "test.txt",
editorScript: `#!/bin/sh
sed -i 's/dist/site/' "$1"`,
setup: func(t *testing.T, conf *config.WarewulfYaml, overlayName, fileName string) time.Time {
overlayDir := filepath.Join(conf.Paths.DistributionOverlaydir(), overlayName)
assert.NoError(t, os.MkdirAll(overlayDir, 0755))
assert.NoError(t, os.WriteFile(filepath.Join(overlayDir, fileName), []byte("dist content"), 0644))
return time.Time{}
},
wantErr: false,
verify: func(t *testing.T, conf *config.WarewulfYaml, overlayName, fileName string, mtime time.Time) {
distOverlayFile := filepath.Join(conf.Paths.DistributionOverlaydir(), overlayName, fileName)
content, err := os.ReadFile(distOverlayFile)
assert.NoError(t, err)
assert.Equal(t, "dist content", string(content))
siteOverlayFile := filepath.Join(conf.Paths.SiteOverlaydir(), overlayName, fileName)
content, err = os.ReadFile(siteOverlayFile)
assert.NoError(t, err)
assert.Equal(t, "site content", string(content))
},
},
{
name: "edit in missing dir no parents",
overlayName: "test-overlay",
fileName: "newdir/test.txt",
editorScript: `#!/bin/sh
echo "content" > "$1"`,
setup: func(t *testing.T, conf *config.WarewulfYaml, overlayName, fileName string) time.Time {
overlayDir := filepath.Join(conf.Paths.SiteOverlaydir(), overlayName)
assert.NoError(t, os.MkdirAll(overlayDir, 0755))
return time.Time{}
},
wantErr: true,
verify: func(t *testing.T, conf *config.WarewulfYaml, overlayName, fileName string, mtime time.Time) {
overlayFile := filepath.Join(conf.Paths.SiteOverlaydir(), overlayName, fileName)
_, err := os.Stat(overlayFile)
assert.True(t, os.IsNotExist(err))
},
},
{
name: "edit in missing dir with parents",
overlayName: "test-overlay",
fileName: "newdir/test.txt",
parents: true,
editorScript: `#!/bin/sh
echo "content" > "$1"`,
setup: func(t *testing.T, conf *config.WarewulfYaml, overlayName, fileName string) time.Time {
overlayDir := filepath.Join(conf.Paths.SiteOverlaydir(), overlayName)
assert.NoError(t, os.MkdirAll(overlayDir, 0755))
return time.Time{}
},
wantErr: false,
verify: func(t *testing.T, conf *config.WarewulfYaml, overlayName, fileName string, mtime time.Time) {
overlayFile := filepath.Join(conf.Paths.SiteOverlaydir(), overlayName, fileName)
content, err := os.ReadFile(overlayFile)
assert.NoError(t, err)
assert.Equal(t, "content\n", string(content))
},
},
}
warewulfd.SetNoDaemon()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
env := testenv.New(t)
defer env.RemoveAll()
conf := env.Configure()
scriptPath := filepath.Join(t.TempDir(), "fake_editor.sh")
err := os.WriteFile(scriptPath, []byte(tt.editorScript), 0755)
assert.NoError(t, err)
t.Setenv("EDITOR", scriptPath)
var mtime time.Time
if tt.setup != nil {
mtime = tt.setup(t, conf, tt.overlayName, tt.fileName)
}
var args []string
if tt.parents {
args = []string{"--parents", tt.overlayName, tt.fileName}
} else {
args = []string{tt.overlayName, tt.fileName}
}
baseCmd := GetCommand()
baseCmd.SetArgs(args)
buf := new(bytes.Buffer)
baseCmd.SetOut(buf)
baseCmd.SetErr(buf)
err = baseCmd.Execute()
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
if tt.verify != nil {
tt.verify(t, conf, tt.overlayName, tt.fileName, mtime)
}
})
}
}

View File

@@ -5,10 +5,8 @@ import (
"os"
"path"
"path/filepath"
"runtime"
"github.com/spf13/cobra"
"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"
@@ -17,7 +15,6 @@ import (
func CobraRunE(cmd *cobra.Command, args []string) (err error) {
var dest string
overlayName := args[0]
source := args[1]
if len(args) == 3 {
@@ -25,25 +22,23 @@ func CobraRunE(cmd *cobra.Command, args []string) (err error) {
} else {
dest = source
}
wwlog.Verbose("Copying '%s' into overlay '%s:%s'", source, overlayName, dest)
overlay_ := overlay.GetOverlay(overlayName)
overlay_, err := overlay.GetOverlay(args[0])
if err != nil {
return err
}
if !overlay_.IsSiteOverlay() {
overlay_, err = overlay_.CloneSiteOverlay()
if err != nil {
return err
}
}
if !overlay_.Exists() {
return fmt.Errorf("overlay does not exist: %s", overlayName)
}
if util.IsDir(overlay_.File(dest)) {
dest = path.Join(dest, path.Base(source))
}
if !OverwriteFile && util.IsFile(overlay_.File(dest)) {
return fmt.Errorf("a file with that name already exists in the overlay: %s", overlayName)
return fmt.Errorf("a file with that name already exists in the overlay")
}
if CreateDirs {
@@ -68,33 +63,5 @@ func CobraRunE(cmd *cobra.Command, args []string) (err error) {
return fmt.Errorf("could not copy file into overlay: %w", err)
}
if !NoOverlayUpdate {
n, err := node.New()
if err != nil {
return fmt.Errorf("could not open node configuration: %s", err)
}
nodes, err := n.FindAllNodes()
if err != nil {
return fmt.Errorf("could not get node list: %s", err)
}
var updateNodes []node.Node
for _, node := range nodes {
if util.InSlice(node.SystemOverlay, overlayName) {
updateNodes = append(updateNodes, node)
} else if util.InSlice(node.RuntimeOverlay, overlayName) {
updateNodes = append(updateNodes, node)
}
}
workers := Workers
if workers <= 0 {
workers = runtime.NumCPU()
}
return overlay.BuildSpecificOverlays(updateNodes, nodes, []string{overlayName}, workers)
}
return nil
}

View File

@@ -77,7 +77,7 @@ nodes: {}
t.FailNow()
}
baseCmd.SetArgs([]string{"-p", "-n", "test", file.Name()})
baseCmd.SetArgs([]string{"-p", "test", file.Name()})
baseCmd.SetOut(nil)
baseCmd.SetErr(nil)
err = baseCmd.Execute()

View File

@@ -24,15 +24,13 @@ var (
}
},
}
OverwriteFile bool
NoOverlayUpdate bool
CreateDirs bool
Workers int
OverwriteFile bool
CreateDirs bool
Workers int
)
func init() {
baseCmd.PersistentFlags().BoolVarP(&OverwriteFile, "overwrite", "o", false, "Overwrite file if exists")
baseCmd.PersistentFlags().BoolVarP(&NoOverlayUpdate, "noupdate", "n", false, "Don't update overlays")
baseCmd.PersistentFlags().BoolVarP(&CreateDirs, "parents", "p", false, "Create any necessary parent directories")
baseCmd.PersistentFlags().IntVar(&Workers, "workers", 0, "The number of parallel workers building overlays (<=0 indicates 1 worker per CPU)")
}

View File

@@ -2,6 +2,7 @@ package list
import (
"os"
"strconv"
"syscall"
"github.com/spf13/cobra"
@@ -11,62 +12,80 @@ import (
"github.com/warewulf/warewulf/internal/pkg/wwlog"
)
func CobraRunE(cmd *cobra.Command, args []string) error {
var overlays []string
/*
RunE needs a function of type func(*cobraCommand,[]string) err, but
in order to avoid global variables which mess up testing a function of
the required type is returned
*/
func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
var overlays []string
if len(args) > 0 {
overlays = args
} else {
overlays = overlay.FindOverlays()
}
t := table.New(cmd.OutOrStdout())
if ListLong {
t.AddHeader("PERM MODE", "UID", "GID", "OVERLAY", "FILE PATH", "SITE")
} else {
t.AddHeader("OVERLAY NAME", "FILES/DIRS", "SITE")
}
for _, name := range overlays {
overlay_ := overlay.GetOverlay(name)
if !overlay_.Exists() {
wwlog.Error("system/%s (path not found:%s)", name, overlay_.Rootfs())
continue
}
files := util.FindFiles(overlay_.Rootfs())
wwlog.Debug("Iterating overlay rootfs: %s", overlay_.Rootfs())
if ListLong {
for file := range files {
s, err := os.Stat(overlay_.File(files[file]))
if err != nil {
wwlog.Warn("%s: %s: %s", name, files[file], err)
continue
}
fileMode := s.Mode()
perms := fileMode & os.ModePerm
sys := s.Sys()
t.AddLine(perms, sys.(*syscall.Stat_t).Uid, sys.(*syscall.Stat_t).Gid, name, files[file], overlay_.IsSiteOverlay())
}
} else if ListContents {
var fileCount int
for file := range files {
t.AddLine(name, files[file], overlay_.IsSiteOverlay())
fileCount++
}
if fileCount == 0 {
t.AddLine(name, 0, overlay_.IsSiteOverlay())
}
if len(args) > 0 {
overlays = args
} else {
t.AddLine(name, len(files), overlay_.IsSiteOverlay())
overlays = overlay.FindOverlays()
}
}
t.Print()
return nil
t := table.New(cmd.OutOrStdout())
locationStr := "SITE"
if vars.ShowPath {
locationStr = "PATH"
}
if vars.ListLong {
t.AddHeader("PERM MODE", "UID", "GID", "OVERLAY", "FILE PATH", locationStr)
} else {
t.AddHeader("OVERLAY NAME", "FILES/DIRS", locationStr)
}
for _, name := range overlays {
overlay_, err := overlay.GetOverlay(name)
if err != nil {
wwlog.Error("%s:%s", name, err)
continue
}
files := util.FindFiles(overlay_.Rootfs())
wwlog.Debug("Iterating overlay rootfs: %s", overlay_.Rootfs())
if vars.ListLong {
for file := range files {
s, err := os.Stat(overlay_.File(files[file]))
if err != nil {
wwlog.Warn("%s: %s: %s", name, files[file], err)
continue
}
fileMode := s.Mode()
perms := fileMode & os.ModePerm
sys := s.Sys()
locLine := strconv.FormatBool(overlay_.IsSiteOverlay())
if vars.ShowPath {
locLine = overlay_.Path()
}
t.AddLine(perms, sys.(*syscall.Stat_t).Uid, sys.(*syscall.Stat_t).Gid, name, files[file], locLine)
}
} else {
locLine := strconv.FormatBool(overlay_.IsSiteOverlay())
if vars.ShowPath {
locLine = overlay_.Path()
}
if vars.ListContents {
var fileCount int
for file := range files {
t.AddLine(name, files[file], locLine)
fileCount++
}
if fileCount == 0 {
t.AddLine(name, 0, locLine)
}
} else {
t.AddLine(name, len(files), locLine)
}
}
}
t.Print()
return nil
}
}

View File

@@ -29,8 +29,8 @@ func Test_Overlay_List(t *testing.T) {
assert.Contains(t, buf.String(), "testoverlay")
})
t.Run("overlay list all", func(t *testing.T) {
baseCmd.SetArgs([]string{"-a"})
baseCmd := GetCommand()
baseCmd.SetArgs([]string{"-a"})
buf := new(bytes.Buffer)
baseCmd.SetOut(buf)
baseCmd.SetErr(buf)
@@ -40,8 +40,8 @@ func Test_Overlay_List(t *testing.T) {
assert.Contains(t, buf.String(), "email.ww")
})
t.Run("overlay list long", func(t *testing.T) {
baseCmd.SetArgs([]string{"--long"})
baseCmd := GetCommand()
baseCmd.SetArgs([]string{"--long"})
buf := new(bytes.Buffer)
baseCmd.SetOut(buf)
baseCmd.SetErr(buf)
@@ -50,4 +50,16 @@ func Test_Overlay_List(t *testing.T) {
assert.NoError(t, err)
assert.Contains(t, buf.String(), "email.ww")
})
t.Run("overlay list path", func(t *testing.T) {
baseCmd := GetCommand()
baseCmd.SetArgs([]string{"--path"})
buf := new(bytes.Buffer)
baseCmd.SetOut(buf)
baseCmd.SetErr(buf)
wwlog.SetLogWriter(buf)
err := baseCmd.Execute()
assert.NoError(t, err)
assert.Contains(t, buf.String(), env.BaseDir)
})
}

View File

@@ -5,28 +5,29 @@ import (
"github.com/warewulf/warewulf/internal/app/wwctl/completions"
)
var (
baseCmd = &cobra.Command{
// Holds the variables which are needed in CobraRunE
type variables struct {
ListContents bool
ListLong bool
ShowPath bool
}
// GetCommand returns the root cobra.Command for the application.
func GetCommand() *cobra.Command {
vars := variables{}
baseCmd := &cobra.Command{
DisableFlagsInUseLine: true,
Use: "list [OPTIONS] OVERLAY_NAME",
Short: "List Warewulf Overlays and files",
Long: "This command displays information about all Warewulf overlays or the specified\nOVERLAY_NAME. It also supports listing overlay content information.",
RunE: CobraRunE,
RunE: CobraRunE(&vars),
Aliases: []string{"ls"},
ValidArgsFunction: completions.Overlays,
Args: cobra.ArbitraryArgs,
}
ListContents bool
ListLong bool
)
baseCmd.PersistentFlags().BoolVarP(&vars.ListContents, "all", "a", false, "List the contents of overlays")
baseCmd.PersistentFlags().BoolVarP(&vars.ListLong, "long", "l", false, "List 'long' of all overlay contents")
baseCmd.PersistentFlags().BoolVarP(&vars.ShowPath, "path", "p", false, "Show the absolute path to the overlay")
func init() {
baseCmd.PersistentFlags().BoolVarP(&ListContents, "all", "a", false, "List the contents of overlays")
baseCmd.PersistentFlags().BoolVarP(&ListLong, "long", "l", false, "List 'long' of all overlay contents")
}
// GetRootCommand returns the root cobra.Command for the application.
func GetCommand() *cobra.Command {
return baseCmd
}

View File

@@ -1,36 +1,15 @@
package mkdir
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/pkg/overlay"
"github.com/warewulf/warewulf/internal/pkg/wwlog"
)
func CobraRunE(cmd *cobra.Command, args []string) (err error) {
overlayName := args[0]
dirName := args[1]
overlay_ := overlay.GetOverlay(overlayName)
if !overlay_.IsSiteOverlay() {
overlay_, err = overlay_.CloneSiteOverlay()
if err != nil {
return err
}
}
if !overlay_.Exists() {
return fmt.Errorf("overlay does not exist: %s", overlayName)
}
overlayDir := overlay_.File(dirName)
wwlog.Debug("Will create directory in overlay: %s:%s", overlayName, dirName)
err = os.MkdirAll(overlayDir, os.FileMode(PermMode))
myOverlay, err := overlay.GetOverlay(args[0])
if err != nil {
return fmt.Errorf("could not create directory: %s", overlayDir)
return err
}
return nil
return myOverlay.Mkdir(args[1], PermMode)
}

View File

@@ -0,0 +1,89 @@
package mkdir
import (
"bytes"
"io/fs"
"os"
"path/filepath"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/warewulf/warewulf/internal/pkg/testenv"
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
)
func Test_OverlayMkdir(t *testing.T) {
tests := []struct {
name string
overlayName string
dirName string
perm string
wantErr bool
system bool
}{
{
name: "simple mkdir",
overlayName: "test-overlay",
dirName: "testdir",
perm: "0755",
wantErr: false,
system: false,
},
{
name: "system overlay mkdir",
overlayName: "wwinit", // A known system overlay
dirName: "init.d",
perm: "0700",
wantErr: false,
system: true,
},
}
warewulfd.SetNoDaemon()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
env := testenv.New(t)
conf := env.Configure()
// Setup overlay
var overlayDir string
if tt.system {
overlayDir = filepath.Join(conf.Paths.DistributionOverlaydir(), tt.overlayName)
err := os.MkdirAll(overlayDir, 0755)
assert.NoError(t, err)
} else {
overlayDir = filepath.Join(conf.Paths.WWOverlaydir, tt.overlayName)
err := os.MkdirAll(overlayDir, 0755)
assert.NoError(t, err)
}
baseCmd := GetCommand()
baseCmd.SetArgs([]string{"-m", tt.perm, tt.overlayName, tt.dirName})
buf := new(bytes.Buffer)
baseCmd.SetOut(buf)
baseCmd.SetErr(buf)
err := baseCmd.Execute()
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
// Check permissions
// for system overlays, the file is copied to a site overlay
if tt.system {
overlayDir = filepath.Join(conf.Paths.WWOverlaydir, tt.overlayName)
}
dirPath := filepath.Join(overlayDir, tt.dirName)
stat, err := os.Stat(dirPath)
assert.NoError(t, err)
if !tt.wantErr {
mode, err := strconv.ParseUint(tt.perm, 8, 32)
assert.NoError(t, err)
assert.Equal(t, fs.FileMode(mode), stat.Mode().Perm())
}
})
}
}

View File

@@ -1,21 +0,0 @@
package mkdir
import (
"testing"
)
// TestArgsOverlayMkdir is a regression test for 248.
// One argument should fail, two should succeed.
func TestArgsOverlayMkdir(t *testing.T) {
command := GetCommand()
err := command.Args(command, []string{"overlay_name"})
if err == nil {
t.Errorf("one argument to overlay mkdir should fail")
}
err = command.Args(command, []string{"overlay_name", "directory"})
if err != nil {
t.Errorf("two arguments to overlay mkdir should succeed")
}
}

View File

@@ -20,9 +20,9 @@ func CobraRunE(cmd *cobra.Command, args []string) error {
overlayName := args[0]
fileName := args[1]
overlay_ := overlay.GetOverlay(overlayName)
if !overlay_.Exists() {
return fmt.Errorf("overlay does not exist: %s", overlayName)
overlay_, err := overlay.GetOverlay(overlayName)
if err != nil {
return err
}
overlayFile := overlay_.File(fileName)