diff --git a/CHANGELOG.md b/CHANGELOG.md index f3d5f126..47ade193 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased ### Added + - Build support for EL10, EL10 depends on `dnsmasq` and no longer `dhcpd-server` (EOL). #1974 - `make rpm` added for local development rpm builds. #1974 ### Changed + - `make dist` now uses `mktemp` instead of `.dist`. #1974 - Fixed dnsmasq template file to use basename for ipxe files. #1974 - For EL10 default to dnsmasq for dhcpd and tftp. #1974 +- Refactored overlay class. #1968 ## v4.6.3, 2025-08-01 diff --git a/internal/app/wwctl/completions/completions.go b/internal/app/wwctl/completions/completions.go index a6b3847f..fb424378 100644 --- a/internal/app/wwctl/completions/completions.go +++ b/internal/app/wwctl/completions/completions.go @@ -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 } diff --git a/internal/app/wwctl/overlay/chmod/chmod_test.go b/internal/app/wwctl/overlay/chmod/chmod_test.go deleted file mode 100644 index 717f7ebd..00000000 --- a/internal/app/wwctl/overlay/chmod/chmod_test.go +++ /dev/null @@ -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") - } -} diff --git a/internal/app/wwctl/overlay/chmod/main.go b/internal/app/wwctl/overlay/chmod/main.go index 3a7f28d6..26260025 100644 --- a/internal/app/wwctl/overlay/chmod/main.go +++ b/internal/app/wwctl/overlay/chmod/main.go @@ -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) } diff --git a/internal/app/wwctl/overlay/chmod/main_test.go b/internal/app/wwctl/overlay/chmod/main_test.go new file mode 100644 index 00000000..25eda675 --- /dev/null +++ b/internal/app/wwctl/overlay/chmod/main_test.go @@ -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()) + } + }) + } +} diff --git a/internal/app/wwctl/overlay/chown/main.go b/internal/app/wwctl/overlay/chown/main.go index 2afa2068..ec71a5b5 100644 --- a/internal/app/wwctl/overlay/chown/main.go +++ b/internal/app/wwctl/overlay/chown/main.go @@ -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) } diff --git a/internal/app/wwctl/overlay/chown/main_test.go b/internal/app/wwctl/overlay/chown/main_test.go new file mode 100644 index 00000000..39545613 --- /dev/null +++ b/internal/app/wwctl/overlay/chown/main_test.go @@ -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) + } + }) + } +} diff --git a/internal/app/wwctl/overlay/create/main.go b/internal/app/wwctl/overlay/create/main.go index 201c02f2..45bbd52e 100644 --- a/internal/app/wwctl/overlay/create/main.go +++ b/internal/app/wwctl/overlay/create/main.go @@ -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 } diff --git a/internal/app/wwctl/overlay/create/main_test.go b/internal/app/wwctl/overlay/create/main_test.go new file mode 100644 index 00000000..71ec69fe --- /dev/null +++ b/internal/app/wwctl/overlay/create/main_test.go @@ -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") + } + }) + } +} diff --git a/internal/app/wwctl/overlay/delete/main.go b/internal/app/wwctl/overlay/delete/main.go index ee439c08..ffa5d669 100644 --- a/internal/app/wwctl/overlay/delete/main.go +++ b/internal/app/wwctl/overlay/delete/main.go @@ -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) } diff --git a/internal/app/wwctl/overlay/delete/main_test.go b/internal/app/wwctl/overlay/delete/main_test.go new file mode 100644 index 00000000..9dac9836 --- /dev/null +++ b/internal/app/wwctl/overlay/delete/main_test.go @@ -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) + }) + } +} diff --git a/internal/app/wwctl/overlay/edit/main.go b/internal/app/wwctl/overlay/edit/main.go index 9af87847..72ecbfbf 100644 --- a/internal/app/wwctl/overlay/edit/main.go +++ b/internal/app/wwctl/overlay/edit/main.go @@ -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 { diff --git a/internal/app/wwctl/overlay/edit/main_test.go b/internal/app/wwctl/overlay/edit/main_test.go new file mode 100644 index 00000000..8bf8bbb9 --- /dev/null +++ b/internal/app/wwctl/overlay/edit/main_test.go @@ -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) + } + }) + } +} diff --git a/internal/app/wwctl/overlay/imprt/main.go b/internal/app/wwctl/overlay/imprt/main.go index ef948fe7..309cb0b1 100644 --- a/internal/app/wwctl/overlay/imprt/main.go +++ b/internal/app/wwctl/overlay/imprt/main.go @@ -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 } diff --git a/internal/app/wwctl/overlay/imprt/main_test.go b/internal/app/wwctl/overlay/imprt/main_test.go index dbbfcc94..0918caf5 100644 --- a/internal/app/wwctl/overlay/imprt/main_test.go +++ b/internal/app/wwctl/overlay/imprt/main_test.go @@ -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() diff --git a/internal/app/wwctl/overlay/imprt/root.go b/internal/app/wwctl/overlay/imprt/root.go index c3958d86..ded3dd5a 100644 --- a/internal/app/wwctl/overlay/imprt/root.go +++ b/internal/app/wwctl/overlay/imprt/root.go @@ -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)") } diff --git a/internal/app/wwctl/overlay/list/main.go b/internal/app/wwctl/overlay/list/main.go index 93f16677..2a325b88 100644 --- a/internal/app/wwctl/overlay/list/main.go +++ b/internal/app/wwctl/overlay/list/main.go @@ -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 + } } diff --git a/internal/app/wwctl/overlay/list/main_test.go b/internal/app/wwctl/overlay/list/main_test.go index 3f6a1956..43d1cf7b 100644 --- a/internal/app/wwctl/overlay/list/main_test.go +++ b/internal/app/wwctl/overlay/list/main_test.go @@ -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) + }) } diff --git a/internal/app/wwctl/overlay/list/root.go b/internal/app/wwctl/overlay/list/root.go index d5031e1d..b99fd30c 100644 --- a/internal/app/wwctl/overlay/list/root.go +++ b/internal/app/wwctl/overlay/list/root.go @@ -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 } diff --git a/internal/app/wwctl/overlay/mkdir/main.go b/internal/app/wwctl/overlay/mkdir/main.go index 9314fabb..eaff489d 100644 --- a/internal/app/wwctl/overlay/mkdir/main.go +++ b/internal/app/wwctl/overlay/mkdir/main.go @@ -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) } diff --git a/internal/app/wwctl/overlay/mkdir/main_test.go b/internal/app/wwctl/overlay/mkdir/main_test.go new file mode 100644 index 00000000..20d5c910 --- /dev/null +++ b/internal/app/wwctl/overlay/mkdir/main_test.go @@ -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()) + } + }) + } +} diff --git a/internal/app/wwctl/overlay/mkdir/mkdir_test.go b/internal/app/wwctl/overlay/mkdir/mkdir_test.go deleted file mode 100644 index 6d3c9573..00000000 --- a/internal/app/wwctl/overlay/mkdir/mkdir_test.go +++ /dev/null @@ -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") - } -} diff --git a/internal/app/wwctl/overlay/show/main.go b/internal/app/wwctl/overlay/show/main.go index 8796e422..1ce99c3e 100644 --- a/internal/app/wwctl/overlay/show/main.go +++ b/internal/app/wwctl/overlay/show/main.go @@ -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) diff --git a/internal/pkg/configure/hostfile.go b/internal/pkg/configure/hostfile.go index e8bf1759..3c9e15d4 100644 --- a/internal/pkg/configure/hostfile.go +++ b/internal/pkg/configure/hostfile.go @@ -14,7 +14,10 @@ import ( Creates '/etc/hosts' from the host template. */ func Hostfile() (err error) { - overlay_ := overlay.GetOverlay("host") + overlay_, err := overlay.GetOverlay("host") + if err != nil { + return err + } hostTemplate := path.Join(overlay_.Rootfs(), "/etc/hosts.ww") if !(util.IsFile(hostTemplate)) { return fmt.Errorf("'the overlay template '/etc/hosts.ww' does not exists in 'host' overlay") diff --git a/internal/pkg/overlay/config.go b/internal/pkg/overlay/config.go index 7896f8db..027225ab 100644 --- a/internal/pkg/overlay/config.go +++ b/internal/pkg/overlay/config.go @@ -15,58 +15,53 @@ import ( ) // GetOverlay returns the filesystem path of an overlay identified by its name, -// along with a boolean indicating whether the returned overlayPath corresponds -// to a site-specific overlay. -func GetOverlay(name string) (overlay Overlay) { - overlay = GetSiteOverlay(name) +func GetOverlay(name string) (overlay Overlay, err error) { + overlay = getSiteOverlayName(name) if overlay.Exists() { - return overlay + return overlay, nil } - overlay = GetDistributionOverlay(name) + overlay = getDistributionOverlay(name) if overlay.Exists() { - return overlay + return overlay, nil } - return GetSiteOverlay(name) -} - -// 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)) + return "", ErrDoesNotExist } // Create creates a new overlay directory for the given overlay // // 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) { + overlay = getSiteOverlayName(name) if util.IsDir(overlay.Path()) { - return fmt.Errorf("overlay already exists: %s", overlay) + 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) +} + +// GetDistributionOverlay returns the filesystem path of a distribution overlay +// identified by the given name. +func getDistributionOverlay(name string) Overlay { + return Overlay(path.Join(config.Get().Paths.DistributionOverlaydir(), name)) +} + +// GetSiteOverlay returns the filesystem path of a site-specific overlay +// identified by the given name. +func getSiteOverlayName(name string) (overlay Overlay) { + return Overlay(path.Join(config.Get().Paths.SiteOverlaydir(), name)) } // Creates a site overlay from an existing distribution overlay. // // If the distribution overlay doesn't exist, return an error. func (overlay Overlay) CloneSiteOverlay() (siteOverlay Overlay, err error) { - siteOverlay = GetSiteOverlay(overlay.Name()) + wwlog.Verbose("Creating site overlay: %s", overlay.Name()) + siteOverlay = getSiteOverlayName(overlay.Name()) if !util.IsDir(overlay.Path()) { 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()) + return siteOverlay, nil } if !util.IsDir(filepath.Dir(siteOverlay.Path())) { if err := os.MkdirAll(filepath.Dir(siteOverlay.Path()), 0o755); err != nil { diff --git a/internal/pkg/overlay/overlay.go b/internal/pkg/overlay/overlay.go index 0365c47b..548d5bc6 100644 --- a/internal/pkg/overlay/overlay.go +++ b/internal/pkg/overlay/overlay.go @@ -100,7 +100,7 @@ func (overlay Overlay) Exists() bool { // Returns: // - true if the overlay is a site overlay; false otherwise. func (overlay Overlay) IsSiteOverlay() bool { - return path.Dir(overlay.Path()) == config.Get().Paths.SiteOverlaydir() + return strings.Contains(overlay.Path(), config.Get().Paths.SiteOverlaydir()) } // IsDistributionOverlay determines whether the overlay is a distribution overlay. @@ -111,7 +111,7 @@ func (overlay Overlay) IsSiteOverlay() bool { // Returns: // - true if the overlay is a distribution overlay; false otherwise. func (overlay Overlay) IsDistributionOverlay() bool { - return path.Dir(overlay.Path()) == config.Get().Paths.DistributionOverlaydir() + return strings.Contains(overlay.Path(), config.Get().Paths.DistributionOverlaydir()) } func (overlay Overlay) AddFile(filePath string, content []byte, parents bool, force bool) error { @@ -125,7 +125,9 @@ func (overlay Overlay) AddFile(filePath string, content []byte, parents bool, fo // replace the overlay with newly created siteOverlay overlay = siteOverlay } - + if !overlay.IsSiteOverlay() { + return fmt.Errorf("cloning of site overlay failed") + } fullPath := overlay.File(filePath) // create necessary parent directories if parents { @@ -146,23 +148,34 @@ func (overlay Overlay) AddFile(filePath string, content []byte, parents bool, fo } // DeleteFile deletes a file or the entire overlay directory. -// If the file belongs to a distribution overlay, it will be cloned to a site overlay // before deletion. -func (overlay Overlay) DeleteFile(filePath string, force, cleanup bool) error { +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) if filePath == "" { + if overlay.IsDistributionOverlay() { + return fmt.Errorf("cannot delete a distribution overlay: %s", overlay.Name()) + } if force { err := os.RemoveAll(overlay.Path()) if err != nil { return fmt.Errorf("failed to delete overlay forcely: %w", err) } } else { - err := os.Remove(overlay.Path()) - if err != nil { + // remove rootfs at first + if err = os.Remove(overlay.Rootfs()); err != nil { return fmt.Errorf("failed to delete overlay: %w", err) } + if overlay.Exists() { + if err = os.Remove(overlay.Path()); err != nil { + return fmt.Errorf("failed to delete overlay: %w", err) + } + } } } else { + // 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.CloneSiteOverlay() if err != nil { @@ -171,12 +184,7 @@ func (overlay Overlay) DeleteFile(filePath string, force, cleanup bool) error { // replace the overlay with newly created siteOverlay overlay = siteOverlay } - fullPath := overlay.File(filePath) - if !util.IsFile(fullPath) { - 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) @@ -208,6 +216,51 @@ func (overlay Overlay) DeleteFile(filePath string, force, cleanup bool) error { return nil } +// chmod for the given oppath in the overlay +func (overlay Overlay) Chmod(path string, mode uint64) (err error) { + if !overlay.IsSiteOverlay() { + overlay, err = overlay.CloneSiteOverlay() + 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.CloneSiteOverlay() + 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.CloneSiteOverlay() + 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 { nodeChan := make(chan node.Node, len(nodes)) errChan := make(chan error, len(nodes)*2) @@ -297,13 +350,16 @@ func BuildHostOverlay() error { hostname, _ := os.Hostname() hostData := node.NewNode(hostname) wwlog.Info("Building overlay for %s: host", hostname) - hostdir := GetOverlay("host").Rootfs() - stats, err := os.Stat(hostdir) + hostdir, err := GetOverlay("host") + if err != nil { + return err + } + stats, err := os.Stat(hostdir.Rootfs()) if err != nil { 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)) { - 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() if err != nil { @@ -424,19 +480,19 @@ func BuildOverlayIndir(nodeData node.Node, allNodes []node.Node, overlayNames [] wwlog.Verbose("Processing node/overlays: %s/%s", nodeData.Id(), strings.Join(overlayNames, ",")) for _, overlayName := range overlayNames { wwlog.Verbose("Building overlay %s for node %s in %s", overlayName, nodeData.Id(), outputDir) - overlayRootfs := GetOverlay(overlayName).Rootfs() - if !util.IsDir(overlayRootfs) { - return fmt.Errorf("overlay %s: %w", overlayName, ErrDoesNotExist) + overlayRootfs, err := GetOverlay(overlayName) + if err != nil { + return err } - wwlog.Debug("Walking the overlay structure: %s", overlayRootfs) - err := filepath.Walk(overlayRootfs, func(walkPath string, info os.FileInfo, err error) error { + wwlog.Debug("Walking the overlay structure: %s", overlayRootfs.Rootfs()) + err = filepath.Walk(overlayRootfs.Rootfs(), func(walkPath string, info os.FileInfo, err error) error { if err != nil { return fmt.Errorf("error for %s: %w", walkPath, err) } wwlog.Debug("Found overlay file: %s", walkPath) - relPath, relErr := filepath.Rel(overlayRootfs, walkPath) + relPath, relErr := filepath.Rel(overlayRootfs.Rootfs(), walkPath) if relErr != nil { wwlog.Warn("Error computing relative path for %s: %v", walkPath, relErr) return relErr @@ -669,15 +725,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 -func OverlayGetFiles(name string) (files []string, err error) { - baseDir := GetOverlay(name).Rootfs() - 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 { +func (overlay Overlay) GetFiles() (files []string, err error) { + err = filepath.Walk(overlay.Rootfs(), func(path string, info fs.FileInfo, err error) error { if util.IsFile(path) { - files = append(files, strings.TrimPrefix(path, baseDir)) + files = append(files, strings.TrimPrefix(path, overlay.Rootfs())) } return nil }) diff --git a/internal/pkg/overlay/overlay_test.go b/internal/pkg/overlay/overlay_test.go index 67c7b6f8..ccc34fbf 100644 --- a/internal/pkg/overlay/overlay_test.go +++ b/internal/pkg/overlay/overlay_test.go @@ -139,19 +139,22 @@ func Test_OverlayMethods(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { - overlay := GetOverlay(tt.name) - 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")) + overlay, err := GetOverlay(tt.name) + if tt.exists { 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 force bool }{ - {"create file", "test", "newfile.ww", []byte("new file"), false}, - {"overwrite existing file", "test", "existingfile.ww", []byte("overwrite file"), true}, + {"create file", "test", "test.ww", []byte("new file"), false}, + {"overwrite existing file", "test", "test.ww", []byte("overwrite file"), true}, } conf := warewulfconf.Get() @@ -728,11 +731,15 @@ func Test_CreateOverlayFile(t *testing.T) { assert.NoError(t, overlayDirErr) defer os.RemoveAll(overlayDir) conf.Paths.WWOverlaydir = overlayDir - + conf.Paths.Datadir = "/dev/null" for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - newOverlay := GetSiteOverlay(tt.overlayName) - err := newOverlay.AddFile(tt.filePath, tt.content, true, tt.force) + newOverlay, err := GetOverlay(tt.overlayName) + 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) newFile := newOverlay.File(tt.filePath) diff --git a/internal/pkg/warewulfd/api/node.go b/internal/pkg/warewulfd/api/node.go index b0b14d52..26e58439 100644 --- a/internal/pkg/warewulfd/api/node.go +++ b/internal/pkg/warewulfd/api/node.go @@ -194,12 +194,12 @@ func addNode() usecase.Interactor { return status.Wrap(fmt.Errorf("image '%s' does not exist", input.Node.ImageName), status.InvalidArgument) } for _, overlay_ := range input.Node.SystemOverlay { - if !overlay.GetOverlay(overlay_).Exists() { + if _, err = overlay.GetOverlay(overlay_); err != nil { return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) } } for _, overlay_ := range input.Node.RuntimeOverlay { - if !overlay.GetOverlay(overlay_).Exists() { + if _, err = overlay.GetOverlay(overlay_); err != nil { 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) } for _, overlay_ := range input.Node.SystemOverlay { - if !overlay.GetOverlay(overlay_).Exists() { + if _, err = overlay.GetOverlay(overlay_); err != nil { return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) } } for _, overlay_ := range input.Node.RuntimeOverlay { - if !overlay.GetOverlay(overlay_).Exists() { + if _, err = overlay.GetOverlay(overlay_); err != nil { return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) } } diff --git a/internal/pkg/warewulfd/api/overlay.go b/internal/pkg/warewulfd/api/overlay.go index ee8126fb..9140c6ac 100644 --- a/internal/pkg/warewulfd/api/overlay.go +++ b/internal/pkg/warewulfd/api/overlay.go @@ -24,10 +24,13 @@ type OverlayResponse struct { func NewOverlayResponse(name string) *OverlayResponse { o := new(OverlayResponse) o.Files = []string{} - if files, err := overlay.OverlayGetFiles(name); err == nil { - o.Files = files + myOverlay, err := overlay.GetOverlay(name) + 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 } @@ -55,7 +58,7 @@ func getOverlayByName() usecase.Interactor { u := usecase.NewInteractor(func(ctx context.Context, input getOverlayByNameInput, output *OverlayResponse) error { wwlog.Debug("api.getOverlayByName(Name:%v)", input.Name) - if !overlay.GetOverlay(input.Name).Exists() { + if _, err := overlay.GetOverlay(input.Name); err != nil { return status.Wrap(fmt.Errorf("overlay not found: %v", input.Name), status.NotFound) } else { *output = *NewOverlayResponse(input.Name) @@ -79,11 +82,13 @@ type OverlayFile struct { } func (of *OverlayFile) FullPath() string { - return path.Join(overlay.GetOverlay(of.Overlay).Rootfs(), of.Path) + myOverlay, _ := overlay.GetOverlay(of.Overlay) + return myOverlay.File(of.Path) } func (of *OverlayFile) Exists() bool { - return overlay.GetOverlay(of.Overlay).Exists() && util.IsFile(of.FullPath()) + myOverlay, _ := overlay.GetOverlay(of.Overlay) + return myOverlay.Exists() && util.IsFile(of.FullPath()) } func (of *OverlayFile) readContents() (string, error) { @@ -202,9 +207,12 @@ func createOverlay() usecase.Interactor { u := usecase.NewInteractor(func(ctx context.Context, input createOverlayInput, output *OverlayResponse) error { wwlog.Debug("api.createOverlay(Name:%v)", input.Name) - newOverlay := overlay.GetSiteOverlay(input.Name) - if err := newOverlay.Create(); err != nil { - return err + newOverlay, err := overlay.GetOverlay(input.Name) + if err != nil { + newOverlay, err = overlay.Create(input.Name) + if err != nil { + return err + } } *output = *NewOverlayResponse(newOverlay.Name()) return nil @@ -223,12 +231,9 @@ func deleteOverlay() usecase.Interactor { u := usecase.NewInteractor(func(ctx context.Context, input deleteOverlayInput, output *OverlayResponse) error { wwlog.Debug("api.deleteOverlay(Name:%v, Force:%v)", input.Name, input.Force) - overlay_ := overlay.GetSiteOverlay(input.Name) - if input.Force { - if err := os.RemoveAll(overlay_.Path()); err != nil { - return err - } - } else { + overlay_, _ := overlay.GetOverlay(input.Name) + isSiteOverlay := overlay_.IsSiteOverlay() + if !input.Force { if registry, err := node.New(); err != nil { return err } else { @@ -240,15 +245,12 @@ func deleteOverlay() usecase.Interactor { 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)) - } else { - if err := os.Remove(overlay_.Path()); err != nil { - return status.Wrap(fmt.Errorf("failed to remove overlay: %v", err), status.Code(409)) - } - } + } + if err := os.RemoveAll(overlay_.Path()); err != nil { + return status.Wrap(fmt.Errorf("failed to remove overlay: %v", err), status.Code(409)) } *output = *NewOverlayResponse(input.Name) + output.Site = isSiteOverlay return nil }) u.SetTitle("Delete an overlay") @@ -274,7 +276,7 @@ func deleteOverlayFile() usecase.Interactor { if relPath, err := url.QueryUnescape(input.Path); err != nil { return fmt.Errorf("failed to decode path: %v: %w", input.Path, err) } else { - overlay_ := overlay.GetOverlay(input.Name) + overlay_, _ := overlay.GetOverlay(input.Name) err := overlay_.DeleteFile(relPath, input.Force, input.Cleanup) if err != nil { return fmt.Errorf("unable to delete overlay file %v: %v: %w", input.Name, relPath, err) @@ -305,7 +307,12 @@ func addOverlayFile() usecase.Interactor { if relPath, err := url.QueryUnescape(input.Path); err != nil { return fmt.Errorf("failed to decode path: %v: %w", input.Path, err) } else { - overlay_ := overlay.GetOverlay(input.Name) + overlay_, err := overlay.GetOverlay(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 { return fmt.Errorf("unable to add overlay file %v: %v: %w", input.Name, relPath, err) } diff --git a/internal/pkg/warewulfd/api/profile.go b/internal/pkg/warewulfd/api/profile.go index 7e8f125a..9b835f9c 100644 --- a/internal/pkg/warewulfd/api/profile.go +++ b/internal/pkg/warewulfd/api/profile.go @@ -81,12 +81,12 @@ func addProfile() usecase.Interactor { return status.Wrap(fmt.Errorf("image '%s' does not exist", input.Profile.ImageName), status.InvalidArgument) } for _, overlay_ := range input.Profile.SystemOverlay { - if !overlay.GetOverlay(overlay_).Exists() { + if _, err = overlay.GetOverlay(overlay_); err != nil { return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) } } for _, overlay_ := range input.Profile.RuntimeOverlay { - if !overlay.GetOverlay(overlay_).Exists() { + if _, err = overlay.GetOverlay(overlay_); err != nil { 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) } for _, overlay_ := range input.Profile.SystemOverlay { - if !overlay.GetOverlay(overlay_).Exists() { + if _, err = overlay.GetOverlay(overlay_); err != nil { return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) } } for _, overlay_ := range input.Profile.RuntimeOverlay { - if !overlay.GetOverlay(overlay_).Exists() { + if _, err = overlay.GetOverlay(overlay_); err != nil { return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) } } diff --git a/internal/pkg/warewulfd/overlay.go b/internal/pkg/warewulfd/overlay.go index 1b299b9b..9b95f202 100644 --- a/internal/pkg/warewulfd/overlay.go +++ b/internal/pkg/warewulfd/overlay.go @@ -25,8 +25,8 @@ func OverlaySend(w http.ResponseWriter, req *http.Request) { return } - o := overlay.GetOverlay(rinfo.overlay) - if !o.Exists() { + myOverlay, err := overlay.GetOverlay(rinfo.overlay) + if err != nil { message := "overlay not found: %s" wwlog.Error(message, rinfo.overlay) http.Error(w, fmt.Sprintf(message, rinfo.overlay), http.StatusNoContent) @@ -41,7 +41,7 @@ func OverlaySend(w http.ResponseWriter, req *http.Request) { return } - overlayFile := o.File(rinfo.path) + overlayFile := myOverlay.File(rinfo.path) if !path.IsAbs(overlayFile) { message := "Path %s isn't absolute" wwlog.Denied(message, overlayFile) diff --git a/internal/pkg/warewulfd/util.go b/internal/pkg/warewulfd/util.go index 919ee05e..0e8b944c 100644 --- a/internal/pkg/warewulfd/util.go +++ b/internal/pkg/warewulfd/util.go @@ -52,8 +52,8 @@ func getOverlayFile(n node.Node, context string, stage_overlays []string, autobu build = util.PathIsNewer(stage_file, config.Get().Paths.NodesConf()) for _, overlayname := range stage_overlays { - overlayDir := overlay.GetOverlay(overlayname).Rootfs() - build = build || util.PathIsNewer(stage_file, overlayDir) + myOverlay, _ := overlay.GetOverlay(overlayname) + build = build || util.PathIsNewer(stage_file, myOverlay.Rootfs()) } }