Merge pull request #1968 from mslacken/FixSiteOverlays

refactored overlay class
This commit is contained in:
Jonathon Anderson
2025-08-27 22:42:39 -06:00
committed by GitHub
35 changed files with 1220 additions and 508 deletions

View File

@@ -7,13 +7,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased ## Unreleased
### Added ### Added
- Build support for EL10, EL10 depends on `dnsmasq` and no longer `dhcpd-server` (EOL). #1974 - Build support for EL10, EL10 depends on `dnsmasq` and no longer `dhcpd-server` (EOL). #1974
- `make rpm` added for local development rpm builds. #1974 - `make rpm` added for local development rpm builds. #1974
### Changed ### Changed
- `make dist` now uses `mktemp` instead of `.dist`. #1974 - `make dist` now uses `mktemp` instead of `.dist`. #1974
- Fixed dnsmasq template file to use basename for ipxe files. #1974 - Fixed dnsmasq template file to use basename for ipxe files. #1974
- For EL10 default to dnsmasq for dhcpd and tftp. #1974 - For EL10 default to dnsmasq for dhcpd and tftp. #1974
- Refactored overlay class. #1968
## v4.6.3, 2025-08-01 ## v4.6.3, 2025-08-01

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) { func OverlayFiles(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
ret, _ := overlay.OverlayGetFiles(args[0]) myOverlay, _ := overlay.Get(args[0])
ret, _ := myOverlay.GetFiles()
return ret, cobra.ShellCompDirectiveNoFileComp 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 ( import (
"fmt" "fmt"
"os"
"strconv" "strconv"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/pkg/overlay" "github.com/warewulf/warewulf/internal/pkg/overlay"
"github.com/warewulf/warewulf/internal/pkg/util"
) )
func CobraRunE(cmd *cobra.Command, args []string) error { func CobraRunE(cmd *cobra.Command, args []string) error {
overlayName := args[0] myOverlay, err := overlay.Get(args[0])
fileName := args[1] if err != nil {
return err
}
path := args[1]
permissionMode, err := strconv.ParseUint(args[2], 8, 32) permissionMode, err := strconv.ParseUint(args[2], 8, 32)
if err != nil { if err != nil {
return fmt.Errorf("could not convert requested mode: %s", err) return fmt.Errorf("could not convert requested mode: %s", err)
} }
overlay_ := overlay.GetOverlay(overlayName) return myOverlay.Chmod(path, permissionMode)
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
} }

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

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) { 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,25 @@
package delete package delete
import ( import (
"fmt"
"os"
"path"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/pkg/overlay" "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 { func CobraRunE(cmd *cobra.Command, args []string) error {
var fileName string
overlayName := args[0]
fileName := ""
if len(args) == 2 { if len(args) == 2 {
fileName = args[1] fileName = args[1]
} }
overlay_ := overlay.GetOverlay(overlayName) myOverlay, err := overlay.Get(args[0])
if overlay_.IsDistributionOverlay() { if err != nil {
return fmt.Errorf("distribution overlay can't deleted") return err
}
if !overlay_.Exists() {
return fmt.Errorf("overlay does not exist: %s", overlayName)
} }
if fileName == "" { if fileName == "" {
if Force { return myOverlay.Delete(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 { } else {
removePath := overlay_.File(fileName) return myOverlay.DeleteFile(fileName, Force, Parents)
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
} }

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

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" "os"
"path" "path"
"path/filepath" "path/filepath"
"runtime"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/pkg/overlay" "github.com/warewulf/warewulf/internal/pkg/overlay"
"github.com/warewulf/warewulf/internal/pkg/util" "github.com/warewulf/warewulf/internal/pkg/util"
"github.com/warewulf/warewulf/internal/pkg/wwlog" "github.com/warewulf/warewulf/internal/pkg/wwlog"
@@ -17,7 +15,6 @@ import (
func CobraRunE(cmd *cobra.Command, args []string) (err error) { func CobraRunE(cmd *cobra.Command, args []string) (err error) {
var dest string var dest string
overlayName := args[0]
source := args[1] source := args[1]
if len(args) == 3 { if len(args) == 3 {
@@ -25,25 +22,23 @@ func CobraRunE(cmd *cobra.Command, args []string) (err error) {
} else { } else {
dest = source dest = source
} }
overlay_, err := overlay.Get(args[0])
wwlog.Verbose("Copying '%s' into overlay '%s:%s'", source, overlayName, dest) if err != nil {
overlay_ := overlay.GetOverlay(overlayName) return err
}
if !overlay_.IsSiteOverlay() { if !overlay_.IsSiteOverlay() {
overlay_, err = overlay_.CloneSiteOverlay() overlay_, err = overlay_.CloneToSite()
if err != nil { if err != nil {
return err return err
} }
} }
if !overlay_.Exists() {
return fmt.Errorf("overlay does not exist: %s", overlayName)
}
if util.IsDir(overlay_.File(dest)) { if util.IsDir(overlay_.File(dest)) {
dest = path.Join(dest, path.Base(source)) dest = path.Join(dest, path.Base(source))
} }
if !OverwriteFile && util.IsFile(overlay_.File(dest)) { 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 { 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) 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 return nil
} }

View File

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

View File

@@ -24,15 +24,13 @@ var (
} }
}, },
} }
OverwriteFile bool OverwriteFile bool
NoOverlayUpdate bool CreateDirs bool
CreateDirs bool Workers int
Workers int
) )
func init() { func init() {
baseCmd.PersistentFlags().BoolVarP(&OverwriteFile, "overwrite", "o", false, "Overwrite file if exists") 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().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)") 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 ( import (
"os" "os"
"strconv"
"syscall" "syscall"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -11,62 +12,80 @@ import (
"github.com/warewulf/warewulf/internal/pkg/wwlog" "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 { if len(args) > 0 {
overlays = args 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())
}
} else { } 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.Get(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") assert.Contains(t, buf.String(), "testoverlay")
}) })
t.Run("overlay list all", func(t *testing.T) { t.Run("overlay list all", func(t *testing.T) {
baseCmd.SetArgs([]string{"-a"})
baseCmd := GetCommand() baseCmd := GetCommand()
baseCmd.SetArgs([]string{"-a"})
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
baseCmd.SetOut(buf) baseCmd.SetOut(buf)
baseCmd.SetErr(buf) baseCmd.SetErr(buf)
@@ -40,8 +40,8 @@ func Test_Overlay_List(t *testing.T) {
assert.Contains(t, buf.String(), "email.ww") assert.Contains(t, buf.String(), "email.ww")
}) })
t.Run("overlay list long", func(t *testing.T) { t.Run("overlay list long", func(t *testing.T) {
baseCmd.SetArgs([]string{"--long"})
baseCmd := GetCommand() baseCmd := GetCommand()
baseCmd.SetArgs([]string{"--long"})
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
baseCmd.SetOut(buf) baseCmd.SetOut(buf)
baseCmd.SetErr(buf) baseCmd.SetErr(buf)
@@ -50,4 +50,16 @@ func Test_Overlay_List(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, buf.String(), "email.ww") 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" "github.com/warewulf/warewulf/internal/app/wwctl/completions"
) )
var ( // Holds the variables which are needed in CobraRunE
baseCmd = &cobra.Command{ 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, DisableFlagsInUseLine: true,
Use: "list [OPTIONS] OVERLAY_NAME", Use: "list [OPTIONS] OVERLAY_NAME",
Short: "List Warewulf Overlays and files", 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.", 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"}, Aliases: []string{"ls"},
ValidArgsFunction: completions.Overlays, ValidArgsFunction: completions.Overlays,
Args: cobra.ArbitraryArgs, Args: cobra.ArbitraryArgs,
} }
ListContents bool baseCmd.PersistentFlags().BoolVarP(&vars.ListContents, "all", "a", false, "List the contents of overlays")
ListLong bool 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 return baseCmd
} }

View File

@@ -1,36 +1,15 @@
package mkdir package mkdir
import ( import (
"fmt"
"os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/pkg/overlay" "github.com/warewulf/warewulf/internal/pkg/overlay"
"github.com/warewulf/warewulf/internal/pkg/wwlog"
) )
func CobraRunE(cmd *cobra.Command, args []string) (err error) { func CobraRunE(cmd *cobra.Command, args []string) (err error) {
overlayName := args[0]
dirName := args[1]
overlay_ := overlay.GetOverlay(overlayName) myOverlay, err := overlay.Get(args[0])
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))
if err != nil { if err != nil {
return fmt.Errorf("could not create directory: %s", overlayDir) return err
} }
return myOverlay.Mkdir(args[1], PermMode)
return nil
} }

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] overlayName := args[0]
fileName := args[1] fileName := args[1]
overlay_ := overlay.GetOverlay(overlayName) overlay_, err := overlay.Get(overlayName)
if !overlay_.Exists() { if err != nil {
return fmt.Errorf("overlay does not exist: %s", overlayName) return err
} }
overlayFile := overlay_.File(fileName) overlayFile := overlay_.File(fileName)

View File

@@ -14,7 +14,10 @@ import (
Creates '/etc/hosts' from the host template. Creates '/etc/hosts' from the host template.
*/ */
func Hostfile() (err error) { func Hostfile() (err error) {
overlay_ := overlay.GetOverlay("host") overlay_, err := overlay.Get("host")
if err != nil {
return err
}
hostTemplate := path.Join(overlay_.Rootfs(), "/etc/hosts.ww") hostTemplate := path.Join(overlay_.Rootfs(), "/etc/hosts.ww")
if !(util.IsFile(hostTemplate)) { if !(util.IsFile(hostTemplate)) {
return fmt.Errorf("'the overlay template '/etc/hosts.ww' does not exists in 'host' overlay") return fmt.Errorf("'the overlay template '/etc/hosts.ww' does not exists in 'host' overlay")

View File

@@ -14,60 +14,56 @@ import (
"github.com/warewulf/warewulf/internal/pkg/wwlog" "github.com/warewulf/warewulf/internal/pkg/wwlog"
) )
// GetOverlay returns the filesystem path of an overlay identified by its name, // Get returns the filesystem path of an overlay identified by its name,
// along with a boolean indicating whether the returned overlayPath corresponds func Get(name string) (overlay Overlay, err error) {
// to a site-specific overlay. overlay = getSiteOverlay(name)
func GetOverlay(name string) (overlay Overlay) {
overlay = GetSiteOverlay(name)
if overlay.Exists() { if overlay.Exists() {
return overlay return overlay, nil
} }
overlay = GetDistributionOverlay(name) overlay = getDistributionOverlay(name)
if overlay.Exists() { if overlay.Exists() {
return overlay return overlay, nil
} }
return GetSiteOverlay(name) return "", ErrDoesNotExist
}
// GetDistributionOverlay returns the filesystem path of a distribution overlay
// identified by the given name.
func GetDistributionOverlay(name string) (overlay Overlay) {
return getOverlay(config.Get().Paths.DistributionOverlaydir(), name)
}
// GetSiteOverlay returns the filesystem path of a site-specific overlay
// identified by the given name.
func GetSiteOverlay(name string) (overlay Overlay) {
return getOverlay(config.Get().Paths.SiteOverlaydir(), name)
}
// getOverlay constructs an overlay based on the given overlay directory and
// overlay name. The overlay does not necessarily exist.
func getOverlay(overlaydir, name string) (overlay Overlay) {
return Overlay(path.Join(overlaydir, name))
} }
// Create creates a new overlay directory for the given overlay // Create creates a new overlay directory for the given overlay
// //
// Returns an error if the overlay already exists or if directory creation fails. // Returns an error if the overlay already exists or if directory creation fails.
func (overlay Overlay) Create() error { func Create(name string) (overlay Overlay, err error) {
if util.IsDir(overlay.Path()) { overlay = getSiteOverlay(name)
return fmt.Errorf("overlay already exists: %s", overlay) if overlay.Exists() {
return overlay, fmt.Errorf("overlay already exists: %s", name)
} }
return os.MkdirAll(overlay.Rootfs(), 0o755) wwlog.Verbose("created site overlay under: %s", overlay.Path())
return overlay, os.MkdirAll(path.Join(overlay.Path(), "rootfs"), 0o755)
} }
// Creates a site overlay from an existing distribution overlay. // GetDistributionOverlay returns a distribution overlay identified by the given
// name.
func getDistributionOverlay(name string) Overlay {
return Overlay(path.Join(config.Get().Paths.DistributionOverlaydir(), name))
}
// getSiteOverlay returns a site-specific overlay identified by the given name.
func getSiteOverlay(name string) (overlay Overlay) {
return Overlay(path.Join(config.Get().Paths.SiteOverlaydir(), name))
}
// CloneToSite creates a site overlay from an existing distribution overlay.
// //
// If the distribution overlay doesn't exist, return an error. // If the distribution overlay doesn't exist, return an error.
func (overlay Overlay) CloneSiteOverlay() (siteOverlay Overlay, err error) { func (overlay Overlay) CloneToSite() (siteOverlay Overlay, err error) {
siteOverlay = GetSiteOverlay(overlay.Name()) wwlog.Verbose("Cloning to site overlay: %s", overlay.Name())
if !util.IsDir(overlay.Path()) { siteOverlay = getSiteOverlay(overlay.Name())
if siteOverlay.Exists() {
return siteOverlay, nil
}
if !overlay.Exists() {
return siteOverlay, fmt.Errorf("source overlay does not exist: %s", overlay.Name()) return siteOverlay, fmt.Errorf("source overlay does not exist: %s", overlay.Name())
} }
if siteOverlay.Exists() {
return siteOverlay, fmt.Errorf("site overlay already exists: %s", siteOverlay.Name())
}
if !util.IsDir(filepath.Dir(siteOverlay.Path())) { if !util.IsDir(filepath.Dir(siteOverlay.Path())) {
if err := os.MkdirAll(filepath.Dir(siteOverlay.Path()), 0o755); err != nil { if err := os.MkdirAll(filepath.Dir(siteOverlay.Path()), 0o755); err != nil {
return siteOverlay, err return siteOverlay, err
@@ -77,7 +73,7 @@ func (overlay Overlay) CloneSiteOverlay() (siteOverlay Overlay, err error) {
return siteOverlay, err return siteOverlay, err
} }
// OverlayImage returns the full path to an overlay image based on the // Image returns the full path to an overlay image based on the
// context and the overlays contained in it. // context and the overlays contained in it.
// //
// If a context is provided, the image file name is based on that // If a context is provided, the image file name is based on that
@@ -88,7 +84,7 @@ func (overlay Overlay) CloneSiteOverlay() (siteOverlay Overlay, err error) {
// //
// If the context is empty and no overlays are specified, the empty // If the context is empty and no overlays are specified, the empty
// string is returned. // string is returned.
func OverlayImage(nodeName string, context string, overlayNames []string) string { func Image(nodeName string, context string, overlayNames []string) string {
var name string var name string
if context != "" { if context != "" {
if len(overlayNames) > 0 { if len(overlayNames) > 0 {
@@ -106,8 +102,8 @@ func OverlayImage(nodeName string, context string, overlayNames []string) string
return path.Join(config.Get().Paths.OverlayProvisiondir(), nodeName, name) return path.Join(config.Get().Paths.OverlayProvisiondir(), nodeName, name)
} }
func ClearOverlayImage(nodeName string, context string, overlayNames []string) error { func RemoveImage(nodeName string, context string, overlayNames []string) error {
imagePath := OverlayImage(nodeName, context, overlayNames) imagePath := Image(nodeName, context, overlayNames)
if util.IsFile(imagePath) { if util.IsFile(imagePath) {
if err := os.Remove(imagePath); err != nil { if err := os.Remove(imagePath); err != nil {
return fmt.Errorf("failed to remove overlay image: %w", err) return fmt.Errorf("failed to remove overlay image: %w", err)

View File

@@ -30,7 +30,7 @@ func Test_OverlayImage(t *testing.T) {
conf.Paths.WWProvisiondir = "p" conf.Paths.WWProvisiondir = "p"
for _, tt := range overlayImageTests { for _, tt := range overlayImageTests {
t.Run(tt.description, func(t *testing.T) { t.Run(tt.description, func(t *testing.T) {
out := OverlayImage(tt.node, tt.context, tt.overlays) out := Image(tt.node, tt.context, tt.overlays)
if tt.image != out { if tt.image != out {
t.Errorf("got %q, want %q", out, tt.image) t.Errorf("got %q, want %q", out, tt.image)
} }

View File

@@ -100,7 +100,13 @@ func (overlay Overlay) Exists() bool {
// Returns: // Returns:
// - true if the overlay is a site overlay; false otherwise. // - true if the overlay is a site overlay; false otherwise.
func (overlay Overlay) IsSiteOverlay() bool { func (overlay Overlay) IsSiteOverlay() bool {
return path.Dir(overlay.Path()) == config.Get().Paths.SiteOverlaydir() siteDir := filepath.Clean(config.Get().Paths.SiteOverlaydir())
overlayPath := filepath.Clean(overlay.Path())
if rel, err := filepath.Rel(siteDir, overlayPath); err != nil {
return false
} else {
return !strings.HasPrefix(rel, "..")
}
} }
// IsDistributionOverlay determines whether the overlay is a distribution overlay. // IsDistributionOverlay determines whether the overlay is a distribution overlay.
@@ -111,21 +117,26 @@ func (overlay Overlay) IsSiteOverlay() bool {
// Returns: // Returns:
// - true if the overlay is a distribution overlay; false otherwise. // - true if the overlay is a distribution overlay; false otherwise.
func (overlay Overlay) IsDistributionOverlay() bool { func (overlay Overlay) IsDistributionOverlay() bool {
return path.Dir(overlay.Path()) == config.Get().Paths.DistributionOverlaydir() siteDir := filepath.Clean(config.Get().Paths.DistributionOverlaydir())
overlayPath := filepath.Clean(overlay.Path())
if rel, err := filepath.Rel(siteDir, overlayPath); err != nil {
return false
} else {
return !strings.HasPrefix(rel, "..")
}
} }
func (overlay Overlay) AddFile(filePath string, content []byte, parents bool, force bool) error { func (overlay Overlay) AddFile(filePath string, content []byte, parents bool, force bool) error {
wwlog.Info("Creating file %s in overlay %s, force: %v", filePath, overlay.Name(), force) wwlog.Info("Creating file %s in overlay %s, force: %v", filePath, overlay.Name(), force)
if overlay.IsDistributionOverlay() { if !overlay.IsSiteOverlay() {
siteOverlay, err := overlay.CloneSiteOverlay() siteOverlay, err := overlay.CloneToSite()
if err != nil { if err != nil {
return fmt.Errorf("failed to clone distribution overlay '%s' to site overlay: %w", overlay.Name(), err) return fmt.Errorf("failed to clone distribution overlay '%s' to site overlay: %w", overlay.Name(), err)
} }
// replace the overlay with newly created siteOverlay // replace the overlay with newly created siteOverlay
overlay = siteOverlay overlay = siteOverlay
} }
fullPath := overlay.File(filePath) fullPath := overlay.File(filePath)
// create necessary parent directories // create necessary parent directories
if parents { if parents {
@@ -145,69 +156,122 @@ func (overlay Overlay) AddFile(filePath string, content []byte, parents bool, fo
return os.WriteFile(fullPath, content, 0o644) return os.WriteFile(fullPath, content, 0o644)
} }
// DeleteFile deletes a file or the entire overlay directory. func (overlay Overlay) Delete(force bool) (err error) {
// If the file belongs to a distribution overlay, it will be cloned to a site overlay wwlog.Info("Deleting overlay %s, force: %v", overlay.Name(), force)
// before deletion. if overlay.IsDistributionOverlay() {
func (overlay Overlay) DeleteFile(filePath string, force, cleanup bool) error { return fmt.Errorf("cannot delete a distribution overlay: %s", overlay.Name())
wwlog.Info("Deleting file %s from overlay %s, force: %v, cleanup: %v", filePath, overlay.Name(), force, cleanup) }
if filePath == "" { if force {
if force { err := os.RemoveAll(overlay.Path())
err := os.RemoveAll(overlay.Path()) if err != nil {
if err != nil { return fmt.Errorf("failed to delete overlay forcely: %w", err)
return fmt.Errorf("failed to delete overlay forcely: %w", err)
}
} else {
err := os.Remove(overlay.Path())
if err != nil {
return fmt.Errorf("failed to delete overlay: %w", err)
}
} }
} else { } else {
if overlay.IsDistributionOverlay() { // remove rootfs at first
siteOverlay, err := overlay.CloneSiteOverlay() if err = os.Remove(overlay.Rootfs()); err != nil {
if err != nil { return fmt.Errorf("failed to delete overlay: %w", err)
return fmt.Errorf("failed to clone distribution overlay '%s' to site overlay: %w", overlay.Name(), err)
}
// replace the overlay with newly created siteOverlay
overlay = siteOverlay
} }
if overlay.Exists() {
fullPath := overlay.File(filePath) if err = os.Remove(overlay.Path()); err != nil {
if !util.IsFile(fullPath) { return fmt.Errorf("failed to delete overlay: %w", err)
return fmt.Errorf("file %s does not exist in overlay %s", filePath, overlay.Name())
}
if force {
if err := os.RemoveAll(fullPath); err != nil {
return fmt.Errorf("failed to delete file %s from overlay %s: %w", filePath, overlay.Name(), err)
}
} else {
if err := os.Remove(fullPath); err != nil {
return fmt.Errorf("failed to delete file %s from overlay %s: %w", filePath, overlay.Name(), err)
}
}
if cleanup {
// cleanup the empty parents
i := path.Dir(fullPath)
for i != overlay.Rootfs() {
wwlog.Debug("Evaluating directory to remove: %s", i)
err := os.Remove(i)
if err != nil {
// if the directory is not empty, we stop here
if !os.IsNotExist(err) {
wwlog.Debug("Could not remove directory %s: %v", i, err)
}
break
}
wwlog.Debug("Removed empty directory: %s", i)
i = path.Dir(i)
} }
} }
} }
return nil return nil
} }
// DeleteFile deletes a file or the entire overlay directory.
// before deletion.
func (overlay Overlay) DeleteFile(filePath string, force, cleanup bool) (err error) {
wwlog.Info("Deleting file %s from overlay %s, force: %v, cleanup: %v", filePath, overlay.Name(), force, cleanup)
// first check if file exists
if !util.IsFile(overlay.File(filePath)) {
return fmt.Errorf("file %s does not exist in overlay %s", filePath, overlay.Name())
}
if overlay.IsDistributionOverlay() {
siteOverlay, err := overlay.CloneToSite()
if err != nil {
return fmt.Errorf("failed to clone distribution overlay '%s' to site overlay: %w", overlay.Name(), err)
}
// replace the overlay with newly created siteOverlay
overlay = siteOverlay
}
fullPath := overlay.File(filePath)
if force {
if err := os.RemoveAll(fullPath); err != nil {
return fmt.Errorf("failed to delete file %s from overlay %s: %w", filePath, overlay.Name(), err)
}
} else {
if err := os.Remove(fullPath); err != nil {
return fmt.Errorf("failed to delete file %s from overlay %s: %w", filePath, overlay.Name(), err)
}
}
if cleanup {
// cleanup the empty parents
i := path.Dir(fullPath)
for i != overlay.Rootfs() {
wwlog.Debug("Evaluating directory to remove: %s", i)
err := os.Remove(i)
if err != nil {
// if the directory is not empty, we stop here
if !os.IsNotExist(err) {
wwlog.Debug("Could not remove directory %s: %v", i, err)
}
break
}
wwlog.Debug("Removed empty directory: %s", i)
i = path.Dir(i)
}
}
return nil
}
// chmod for the given path in the overlay
func (overlay Overlay) Chmod(path string, mode uint64) (err error) {
if !overlay.IsSiteOverlay() {
overlay, err = overlay.CloneToSite()
if err != nil {
return err
}
}
fullPath := overlay.File(path)
if !(util.IsFile(fullPath) || util.IsDir(fullPath)) {
return fmt.Errorf("file does not exist within overlay: %s:%s", overlay.Name(), fullPath)
}
return os.Chmod(fullPath, os.FileMode(mode))
}
// chown file or dir in overlay
func (overlay Overlay) Chown(path string, uid, gid int) (err error) {
if !overlay.IsSiteOverlay() {
overlay, err = overlay.CloneToSite()
if err != nil {
return err
}
}
fullPath := overlay.File(path)
if !(util.IsFile(fullPath) || util.IsDir(fullPath)) {
return fmt.Errorf("file does not exist within overlay: %s:%s", overlay.Name(), fullPath)
}
return os.Chown(fullPath, uid, gid)
}
func (overlay Overlay) Mkdir(path string, mode int32) (err error) {
if !overlay.IsSiteOverlay() {
overlay, err = overlay.CloneToSite()
if err != nil {
return err
}
}
fullPath := overlay.File(path)
if util.IsFile(fullPath) || util.IsDir(fullPath) {
wwlog.Warn("path already exists, overwriting permissions: %s:%s", overlay.Name(), fullPath)
}
return os.MkdirAll(fullPath, os.FileMode(mode))
}
func BuildAllOverlays(nodes []node.Node, allNodes []node.Node, workerCount int) error { func BuildAllOverlays(nodes []node.Node, allNodes []node.Node, workerCount int) error {
nodeChan := make(chan node.Node, len(nodes)) nodeChan := make(chan node.Node, len(nodes))
errChan := make(chan error, len(nodes)*2) errChan := make(chan error, len(nodes)*2)
@@ -297,13 +361,16 @@ func BuildHostOverlay() error {
hostname, _ := os.Hostname() hostname, _ := os.Hostname()
hostData := node.NewNode(hostname) hostData := node.NewNode(hostname)
wwlog.Info("Building overlay for %s: host", hostname) wwlog.Info("Building overlay for %s: host", hostname)
hostdir := GetOverlay("host").Rootfs() hostdir, err := Get("host")
stats, err := os.Stat(hostdir) if err != nil {
return err
}
stats, err := os.Stat(hostdir.Rootfs())
if err != nil { if err != nil {
return fmt.Errorf("could not build host overlay: %w ", err) return fmt.Errorf("could not build host overlay: %w ", err)
} }
if !(stats.Mode() == os.FileMode(0o750|os.ModeDir) || stats.Mode() == os.FileMode(0o700|os.ModeDir)) { if !(stats.Mode() == os.FileMode(0o750|os.ModeDir) || stats.Mode() == os.FileMode(0o700|os.ModeDir)) {
wwlog.SecWarn("Permissions of host overlay dir %s are %s (750 is considered as secure)", hostdir, stats.Mode()) wwlog.SecWarn("Permissions of host overlay dir %s are %s (750 is considered as secure)", hostdir.Rootfs(), stats.Mode())
} }
registry, err := node.New() registry, err := node.New()
if err != nil { if err != nil {
@@ -360,7 +427,7 @@ func BuildOverlay(nodeConf node.Node, allNodes []node.Node, context string, over
} else { } else {
name = fmt.Sprintf("%s overlay/%v", nodeConf.Id(), overlayNames) name = fmt.Sprintf("%s overlay/%v", nodeConf.Id(), overlayNames)
} }
overlayImage := OverlayImage(nodeConf.Id(), context, overlayNames) overlayImage := Image(nodeConf.Id(), context, overlayNames)
overlayImageDir := path.Dir(overlayImage) overlayImageDir := path.Dir(overlayImage)
err := os.MkdirAll(overlayImageDir, 0o750) err := os.MkdirAll(overlayImageDir, 0o750)
@@ -424,19 +491,19 @@ func BuildOverlayIndir(nodeData node.Node, allNodes []node.Node, overlayNames []
wwlog.Verbose("Processing node/overlays: %s/%s", nodeData.Id(), strings.Join(overlayNames, ",")) wwlog.Verbose("Processing node/overlays: %s/%s", nodeData.Id(), strings.Join(overlayNames, ","))
for _, overlayName := range overlayNames { for _, overlayName := range overlayNames {
wwlog.Verbose("Building overlay %s for node %s in %s", overlayName, nodeData.Id(), outputDir) wwlog.Verbose("Building overlay %s for node %s in %s", overlayName, nodeData.Id(), outputDir)
overlayRootfs := GetOverlay(overlayName).Rootfs() overlayRootfs, err := Get(overlayName)
if !util.IsDir(overlayRootfs) { if err != nil {
return fmt.Errorf("overlay %s: %w", overlayName, ErrDoesNotExist) return err
} }
wwlog.Debug("Walking the overlay structure: %s", overlayRootfs) wwlog.Debug("Walking the overlay structure: %s", overlayRootfs.Rootfs())
err := filepath.Walk(overlayRootfs, func(walkPath string, info os.FileInfo, err error) error { err = filepath.Walk(overlayRootfs.Rootfs(), func(walkPath string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return fmt.Errorf("error for %s: %w", walkPath, err) return fmt.Errorf("error for %s: %w", walkPath, err)
} }
wwlog.Debug("Found overlay file: %s", walkPath) wwlog.Debug("Found overlay file: %s", walkPath)
relPath, relErr := filepath.Rel(overlayRootfs, walkPath) relPath, relErr := filepath.Rel(overlayRootfs.Rootfs(), walkPath)
if relErr != nil { if relErr != nil {
wwlog.Warn("Error computing relative path for %s: %v", walkPath, relErr) wwlog.Warn("Error computing relative path for %s: %v", walkPath, relErr)
return relErr return relErr
@@ -669,15 +736,10 @@ func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
} }
// Get all the files as a string slice for a given overlay // Get all the files as a string slice for a given overlay
func OverlayGetFiles(name string) (files []string, err error) { func (overlay Overlay) GetFiles() (files []string, err error) {
baseDir := GetOverlay(name).Rootfs() err = filepath.Walk(overlay.Rootfs(), func(path string, info fs.FileInfo, err error) error {
if !util.IsDir(baseDir) {
err = fmt.Errorf("overlay %s doesn't exist", name)
return
}
err = filepath.Walk(baseDir, func(path string, info fs.FileInfo, err error) error {
if util.IsFile(path) { if util.IsFile(path) {
files = append(files, strings.TrimPrefix(path, baseDir)) files = append(files, strings.TrimPrefix(path, overlay.Rootfs()))
} }
return nil return nil
}) })

View File

@@ -139,19 +139,22 @@ func Test_OverlayMethods(t *testing.T) {
for name, tt := range tests { for name, tt := range tests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
overlay := GetOverlay(tt.name) overlay, err := Get(tt.name)
assert.Equal(t, tt.name, overlay.Name()) if tt.exists {
assert.Equal(t, env.GetPath(tt.path), overlay.Path())
assert.Equal(t, env.GetPath(tt.rootfs), overlay.Rootfs())
assert.Equal(t, env.GetPath(tt.file), overlay.File("testfile"))
if tt.content != "" {
buffer, err := os.ReadFile(overlay.File("testfile"))
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.content, string(buffer)) assert.Equal(t, tt.name, overlay.Name())
assert.Equal(t, env.GetPath(tt.path), overlay.Path())
assert.Equal(t, env.GetPath(tt.rootfs), overlay.Rootfs())
assert.Equal(t, env.GetPath(tt.file), overlay.File("testfile"))
if tt.content != "" {
buffer, err := os.ReadFile(overlay.File("testfile"))
assert.NoError(t, err)
assert.Equal(t, tt.content, string(buffer))
}
assert.Equal(t, tt.exists, overlay.Exists())
assert.Equal(t, tt.isSite, overlay.IsSiteOverlay())
assert.Equal(t, tt.isDist, overlay.IsDistributionOverlay())
} }
assert.Equal(t, tt.exists, overlay.Exists())
assert.Equal(t, tt.isSite, overlay.IsSiteOverlay())
assert.Equal(t, tt.isDist, overlay.IsDistributionOverlay())
}) })
} }
} }
@@ -719,8 +722,8 @@ func Test_CreateOverlayFile(t *testing.T) {
content []byte content []byte
force bool force bool
}{ }{
{"create file", "test", "newfile.ww", []byte("new file"), false}, {"create file", "test", "test.ww", []byte("new file"), false},
{"overwrite existing file", "test", "existingfile.ww", []byte("overwrite file"), true}, {"overwrite existing file", "test", "test.ww", []byte("overwrite file"), true},
} }
conf := warewulfconf.Get() conf := warewulfconf.Get()
@@ -728,11 +731,15 @@ func Test_CreateOverlayFile(t *testing.T) {
assert.NoError(t, overlayDirErr) assert.NoError(t, overlayDirErr)
defer os.RemoveAll(overlayDir) defer os.RemoveAll(overlayDir)
conf.Paths.WWOverlaydir = overlayDir conf.Paths.WWOverlaydir = overlayDir
conf.Paths.Datadir = "/dev/null"
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
newOverlay := GetSiteOverlay(tt.overlayName) newOverlay, err := Get(tt.overlayName)
err := newOverlay.AddFile(tt.filePath, tt.content, true, tt.force) if err != nil {
newOverlay, err = Create(tt.overlayName)
}
assert.NoError(t, err)
err = newOverlay.AddFile(tt.filePath, tt.content, true, tt.force)
assert.NoError(t, err) assert.NoError(t, err)
newFile := newOverlay.File(tt.filePath) newFile := newOverlay.File(tt.filePath)

View File

@@ -70,13 +70,13 @@ func getNodeOverlayInfo() usecase.Interactor {
Overlays: node_.RuntimeOverlay, Overlays: node_.RuntimeOverlay,
}, },
} }
sysImagePath := overlay.OverlayImage(input.ID, "system", node_.SystemOverlay) sysImagePath := overlay.Image(input.ID, "system", node_.SystemOverlay)
if sysImageStat, err := os.Stat(sysImagePath); err == nil { if sysImageStat, err := os.Stat(sysImagePath); err == nil {
mtime := sysImageStat.ModTime() mtime := sysImageStat.ModTime()
out.SystemOverlay.MTime = &mtime out.SystemOverlay.MTime = &mtime
} }
runtimeImagePath := overlay.OverlayImage(input.ID, "runtime", node_.RuntimeOverlay) runtimeImagePath := overlay.Image(input.ID, "runtime", node_.RuntimeOverlay)
if runtimeImageStat, err := os.Stat(runtimeImagePath); err == nil { if runtimeImageStat, err := os.Stat(runtimeImagePath); err == nil {
mtime := runtimeImageStat.ModTime() mtime := runtimeImageStat.ModTime()
out.RuntimeOverlay.MTime = &mtime out.RuntimeOverlay.MTime = &mtime
@@ -194,12 +194,12 @@ func addNode() usecase.Interactor {
return status.Wrap(fmt.Errorf("image '%s' does not exist", input.Node.ImageName), status.InvalidArgument) return status.Wrap(fmt.Errorf("image '%s' does not exist", input.Node.ImageName), status.InvalidArgument)
} }
for _, overlay_ := range input.Node.SystemOverlay { for _, overlay_ := range input.Node.SystemOverlay {
if !overlay.GetOverlay(overlay_).Exists() { if _, err = overlay.Get(overlay_); err != nil {
return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument)
} }
} }
for _, overlay_ := range input.Node.RuntimeOverlay { for _, overlay_ := range input.Node.RuntimeOverlay {
if !overlay.GetOverlay(overlay_).Exists() { if _, err = overlay.Get(overlay_); err != nil {
return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument)
} }
} }
@@ -269,12 +269,12 @@ func updateNode() usecase.Interactor {
return status.Wrap(fmt.Errorf("image '%s' does not exist", input.Node.ImageName), status.InvalidArgument) return status.Wrap(fmt.Errorf("image '%s' does not exist", input.Node.ImageName), status.InvalidArgument)
} }
for _, overlay_ := range input.Node.SystemOverlay { for _, overlay_ := range input.Node.SystemOverlay {
if !overlay.GetOverlay(overlay_).Exists() { if _, err = overlay.Get(overlay_); err != nil {
return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument)
} }
} }
for _, overlay_ := range input.Node.RuntimeOverlay { for _, overlay_ := range input.Node.RuntimeOverlay {
if !overlay.GetOverlay(overlay_).Exists() { if _, err = overlay.Get(overlay_); err != nil {
return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument)
} }
} }

View File

@@ -24,10 +24,13 @@ type OverlayResponse struct {
func NewOverlayResponse(name string) *OverlayResponse { func NewOverlayResponse(name string) *OverlayResponse {
o := new(OverlayResponse) o := new(OverlayResponse)
o.Files = []string{} o.Files = []string{}
if files, err := overlay.OverlayGetFiles(name); err == nil { myOverlay, err := overlay.Get(name)
o.Files = files if err == nil {
if files, err := myOverlay.GetFiles(); err == nil {
o.Files = files
}
o.Site = myOverlay.IsSiteOverlay()
} }
o.Site = overlay.GetOverlay(name).IsSiteOverlay()
return o return o
} }
@@ -55,7 +58,7 @@ func getOverlayByName() usecase.Interactor {
u := usecase.NewInteractor(func(ctx context.Context, input getOverlayByNameInput, output *OverlayResponse) error { u := usecase.NewInteractor(func(ctx context.Context, input getOverlayByNameInput, output *OverlayResponse) error {
wwlog.Debug("api.getOverlayByName(Name:%v)", input.Name) wwlog.Debug("api.getOverlayByName(Name:%v)", input.Name)
if !overlay.GetOverlay(input.Name).Exists() { if _, err := overlay.Get(input.Name); err != nil {
return status.Wrap(fmt.Errorf("overlay not found: %v", input.Name), status.NotFound) return status.Wrap(fmt.Errorf("overlay not found: %v", input.Name), status.NotFound)
} else { } else {
*output = *NewOverlayResponse(input.Name) *output = *NewOverlayResponse(input.Name)
@@ -79,11 +82,19 @@ type OverlayFile struct {
} }
func (of *OverlayFile) FullPath() string { func (of *OverlayFile) FullPath() string {
return path.Join(overlay.GetOverlay(of.Overlay).Rootfs(), of.Path) if myOverlay, err := overlay.Get(of.Overlay); err != nil {
return ""
} else {
return myOverlay.File(of.Path)
}
} }
func (of *OverlayFile) Exists() bool { func (of *OverlayFile) Exists() bool {
return overlay.GetOverlay(of.Overlay).Exists() && util.IsFile(of.FullPath()) if myOverlay, err := overlay.Get(of.Overlay); err != nil {
return false
} else {
return myOverlay.Exists() && util.IsFile(of.FullPath())
}
} }
func (of *OverlayFile) readContents() (string, error) { func (of *OverlayFile) readContents() (string, error) {
@@ -202,9 +213,12 @@ func createOverlay() usecase.Interactor {
u := usecase.NewInteractor(func(ctx context.Context, input createOverlayInput, output *OverlayResponse) error { u := usecase.NewInteractor(func(ctx context.Context, input createOverlayInput, output *OverlayResponse) error {
wwlog.Debug("api.createOverlay(Name:%v)", input.Name) wwlog.Debug("api.createOverlay(Name:%v)", input.Name)
newOverlay := overlay.GetSiteOverlay(input.Name) newOverlay, err := overlay.Get(input.Name)
if err := newOverlay.Create(); err != nil { if err != nil {
return err newOverlay, err = overlay.Create(input.Name)
if err != nil {
return err
}
} }
*output = *NewOverlayResponse(newOverlay.Name()) *output = *NewOverlayResponse(newOverlay.Name())
return nil return nil
@@ -218,17 +232,21 @@ func createOverlay() usecase.Interactor {
func deleteOverlay() usecase.Interactor { func deleteOverlay() usecase.Interactor {
type deleteOverlayInput struct { type deleteOverlayInput struct {
Name string `path:"name" required:"true" description:"Name of overlay to delete"` Name string `path:"name" required:"true" description:"Name of overlay to delete"`
Force bool `query:"force" default:"false" description:"Whether to delete a non-empty overlay, default:'false'"` Force bool `query:"force" default:"false" description:"Whether to delete a non-empty or in-use overlay, default:'false'"`
} }
u := usecase.NewInteractor(func(ctx context.Context, input deleteOverlayInput, output *OverlayResponse) error { u := usecase.NewInteractor(func(ctx context.Context, input deleteOverlayInput, output *OverlayResponse) error {
wwlog.Debug("api.deleteOverlay(Name:%v, Force:%v)", input.Name, input.Force) wwlog.Debug("api.deleteOverlay(Name:%v, Force:%v)", input.Name, input.Force)
overlay_ := overlay.GetSiteOverlay(input.Name) overlay_, err := overlay.Get(input.Name)
if input.Force { if err != nil {
if err := os.RemoveAll(overlay_.Path()); err != nil { return err
return err }
}
} else { if overlay_.IsDistributionOverlay() {
return status.Wrap(fmt.Errorf("distribution overlay: %s", overlay_.Name()), status.InvalidArgument)
}
if !input.Force {
if registry, err := node.New(); err != nil { if registry, err := node.New(); err != nil {
return err return err
} else { } else {
@@ -240,15 +258,12 @@ func deleteOverlay() usecase.Interactor {
status.InvalidArgument) status.InvalidArgument)
} }
} }
if err := os.Remove(overlay_.Rootfs()); err != nil { }
return status.Wrap(fmt.Errorf("failed to remove overlay rootfs: %v", err), status.Code(409)) if err := overlay_.Delete(input.Force); err != nil {
} else { return status.Wrap(fmt.Errorf("failed to remove overlay: %v", err), status.Code(409))
if err := os.Remove(overlay_.Path()); err != nil {
return status.Wrap(fmt.Errorf("failed to remove overlay: %v", err), status.Code(409))
}
}
} }
*output = *NewOverlayResponse(input.Name) *output = *NewOverlayResponse(input.Name)
output.Site = overlay_.IsSiteOverlay()
return nil return nil
}) })
u.SetTitle("Delete an overlay") u.SetTitle("Delete an overlay")
@@ -274,10 +289,12 @@ func deleteOverlayFile() usecase.Interactor {
if relPath, err := url.QueryUnescape(input.Path); err != nil { if relPath, err := url.QueryUnescape(input.Path); err != nil {
return fmt.Errorf("failed to decode path: %v: %w", input.Path, err) return fmt.Errorf("failed to decode path: %v: %w", input.Path, err)
} else { } else {
overlay_ := overlay.GetOverlay(input.Name) if overlay_, err := overlay.Get(input.Name); err != nil {
err := overlay_.DeleteFile(relPath, input.Force, input.Cleanup) return err
if err != nil { } else {
return fmt.Errorf("unable to delete overlay file %v: %v: %w", input.Name, relPath, err) if err := overlay_.DeleteFile(relPath, input.Force, input.Cleanup); err != nil {
return fmt.Errorf("unable to delete overlay file %v: %v: %w", input.Name, relPath, err)
}
} }
} }
*output = *NewOverlayResponse(input.Name) *output = *NewOverlayResponse(input.Name)
@@ -305,7 +322,12 @@ func addOverlayFile() usecase.Interactor {
if relPath, err := url.QueryUnescape(input.Path); err != nil { if relPath, err := url.QueryUnescape(input.Path); err != nil {
return fmt.Errorf("failed to decode path: %v: %w", input.Path, err) return fmt.Errorf("failed to decode path: %v: %w", input.Path, err)
} else { } else {
overlay_ := overlay.GetOverlay(input.Name) overlay_, err := overlay.Get(input.Name)
if err != nil {
if _, err := overlay.Create(input.Name); err != nil {
return err
}
}
if err := overlay_.AddFile(relPath, []byte(input.Content), true, !(input.IfNoneMatch == "*")); err != nil { if err := overlay_.AddFile(relPath, []byte(input.Content), true, !(input.IfNoneMatch == "*")); err != nil {
return fmt.Errorf("unable to add overlay file %v: %v: %w", input.Name, relPath, err) return fmt.Errorf("unable to add overlay file %v: %v: %w", input.Name, relPath, err)
} }

View File

@@ -108,7 +108,7 @@ var overlayTests = map[string]struct {
response: `{"files":null, "site":true}`, response: `{"files":null, "site":true}`,
}, },
"delete overlay": { "force delete site overlay": {
initFiles: map[string]string{ initFiles: map[string]string{
"/var/lib/warewulf/overlays/test/": "", "/var/lib/warewulf/overlays/test/": "",
}, },
@@ -117,6 +117,16 @@ var overlayTests = map[string]struct {
}, },
response: `{"files":[], "site":true}`, response: `{"files":[], "site":true}`,
}, },
"force delete distribution overlay": {
initFiles: map[string]string{
"/usr/share/warewulf/overlays/test/email.ww": sampleTemplate,
},
request: func(serverURL string) (*http.Request, error) {
return http.NewRequest(http.MethodDelete, serverURL+"/api/overlays/test?force=true", nil)
},
status: 400,
},
} }
func TestOverlayAPI(t *testing.T) { func TestOverlayAPI(t *testing.T) {
@@ -160,8 +170,10 @@ func TestOverlayAPI(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, resp.Body.Close()) assert.NoError(t, resp.Body.Close())
ja := jsonassert.New(t) if tt.response != "" {
ja.Assertf(string(body), tt.response) //nolint:govet // tt.response is used as a format string with special tokens ja := jsonassert.New(t)
ja.Assertf(string(body), tt.response) //nolint:govet // tt.response is used as a format string with special tokens
}
for _, fileName := range tt.resultFiles { for _, fileName := range tt.resultFiles {
assert.DirExists(t, env.GetPath(fileName)) assert.DirExists(t, env.GetPath(fileName))

View File

@@ -81,12 +81,12 @@ func addProfile() usecase.Interactor {
return status.Wrap(fmt.Errorf("image '%s' does not exist", input.Profile.ImageName), status.InvalidArgument) return status.Wrap(fmt.Errorf("image '%s' does not exist", input.Profile.ImageName), status.InvalidArgument)
} }
for _, overlay_ := range input.Profile.SystemOverlay { for _, overlay_ := range input.Profile.SystemOverlay {
if !overlay.GetOverlay(overlay_).Exists() { if _, err = overlay.Get(overlay_); err != nil {
return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument)
} }
} }
for _, overlay_ := range input.Profile.RuntimeOverlay { for _, overlay_ := range input.Profile.RuntimeOverlay {
if !overlay.GetOverlay(overlay_).Exists() { if _, err = overlay.Get(overlay_); err != nil {
return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument)
} }
} }
@@ -126,12 +126,12 @@ func updateProfile() usecase.Interactor {
return status.Wrap(fmt.Errorf("image '%s' does not exist", input.Profile.ImageName), status.InvalidArgument) return status.Wrap(fmt.Errorf("image '%s' does not exist", input.Profile.ImageName), status.InvalidArgument)
} }
for _, overlay_ := range input.Profile.SystemOverlay { for _, overlay_ := range input.Profile.SystemOverlay {
if !overlay.GetOverlay(overlay_).Exists() { if _, err = overlay.Get(overlay_); err != nil {
return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument)
} }
} }
for _, overlay_ := range input.Profile.RuntimeOverlay { for _, overlay_ := range input.Profile.RuntimeOverlay {
if !overlay.GetOverlay(overlay_).Exists() { if _, err = overlay.Get(overlay_); err != nil {
return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument)
} }
} }

View File

@@ -99,10 +99,10 @@ func GetNodeOrSetDiscoverable(hwaddr string, autobuildOverlays bool) (node.Node,
return nodeFound, fmt.Errorf("%s (failed to reload configuration) %w", hwaddr, err) return nodeFound, fmt.Errorf("%s (failed to reload configuration) %w", hwaddr, err)
} }
if autobuildOverlays { if autobuildOverlays {
if err := overlay.ClearOverlayImage(nodeFound.Id(), "system", []string{}); err != nil { if err := overlay.RemoveImage(nodeFound.Id(), "system", []string{}); err != nil {
wwlog.Warn("Failed to clear system overlay image: %s: %s", nodeFound.Id(), err) wwlog.Warn("Failed to clear system overlay image: %s: %s", nodeFound.Id(), err)
} }
if err := overlay.ClearOverlayImage(nodeFound.Id(), "runtime", []string{}); err != nil { if err := overlay.RemoveImage(nodeFound.Id(), "runtime", []string{}); err != nil {
wwlog.Warn("Failed to clear runtime overlay image: %s: %s", nodeFound.Id(), err) wwlog.Warn("Failed to clear runtime overlay image: %s: %s", nodeFound.Id(), err)
} }
} }

View File

@@ -25,8 +25,8 @@ func OverlaySend(w http.ResponseWriter, req *http.Request) {
return return
} }
o := overlay.GetOverlay(rinfo.overlay) myOverlay, err := overlay.Get(rinfo.overlay)
if !o.Exists() { if err != nil {
message := "overlay not found: %s" message := "overlay not found: %s"
wwlog.Error(message, rinfo.overlay) wwlog.Error(message, rinfo.overlay)
http.Error(w, fmt.Sprintf(message, rinfo.overlay), http.StatusNoContent) http.Error(w, fmt.Sprintf(message, rinfo.overlay), http.StatusNoContent)
@@ -41,7 +41,7 @@ func OverlaySend(w http.ResponseWriter, req *http.Request) {
return return
} }
overlayFile := o.File(rinfo.path) overlayFile := myOverlay.File(rinfo.path)
if !path.IsAbs(overlayFile) { if !path.IsAbs(overlayFile) {
message := "Path %s isn't absolute" message := "Path %s isn't absolute"
wwlog.Denied(message, overlayFile) wwlog.Denied(message, overlayFile)

View File

@@ -45,15 +45,16 @@ func sendFile(
} }
func getOverlayFile(n node.Node, context string, stage_overlays []string, autobuild bool) (stage_file string, err error) { func getOverlayFile(n node.Node, context string, stage_overlays []string, autobuild bool) (stage_file string, err error) {
stage_file = overlay.OverlayImage(n.Id(), context, stage_overlays) stage_file = overlay.Image(n.Id(), context, stage_overlays)
build := !util.IsFile(stage_file) build := !util.IsFile(stage_file)
wwlog.Verbose("stage file: %s", stage_file) wwlog.Verbose("stage file: %s", stage_file)
if !build && autobuild { if !build && autobuild {
build = util.PathIsNewer(stage_file, config.Get().Paths.NodesConf()) build = util.PathIsNewer(stage_file, config.Get().Paths.NodesConf())
for _, overlayname := range stage_overlays { for _, overlayname := range stage_overlays {
overlayDir := overlay.GetOverlay(overlayname).Rootfs() if myOverlay, err := overlay.Get(overlayname); err == nil {
build = build || util.PathIsNewer(stage_file, overlayDir) build = build || util.PathIsNewer(stage_file, myOverlay.Rootfs())
}
} }
} }