diff --git a/internal/pkg/overlay/overlay.go b/internal/pkg/overlay/overlay.go index 5628d004..3c65fbcb 100644 --- a/internal/pkg/overlay/overlay.go +++ b/internal/pkg/overlay/overlay.go @@ -14,7 +14,6 @@ import ( "text/template" "github.com/Masterminds/sprig/v3" - "github.com/pkg/errors" "github.com/warewulf/warewulf/internal/pkg/config" "github.com/warewulf/warewulf/internal/pkg/node" @@ -23,7 +22,7 @@ import ( ) var ( - ErrDoesNotExist = errors.New("overlay does not exist") + ErrDoesNotExist = fmt.Errorf("overlay does not exist") ) // Overlay represents an overlay directory path. @@ -231,155 +230,160 @@ func BuildOverlay(nodeConf node.Node, context string, overlayNames []string) err return err } -/* -Build the given overlays for a node in the given directory. If the given does not -exists it will be created. -*/ +var regFile *regexp.Regexp +var regLink *regexp.Regexp + +func init() { + regFile = regexp.MustCompile(`.*{{\s*/\*\s*file\s*["'](.*)["']\s*\*/\s*}}.*`) + regLink = regexp.MustCompile(`.*{{\s*/\*\s*softlink\s*["'](.*)["']\s*\*/\s*}}.*`) +} + +// Build the given overlays for a node in the given directory. func BuildOverlayIndir(nodeData node.Node, overlayNames []string, outputDir string) error { if len(overlayNames) == 0 { return nil } if !util.IsDir(outputDir) { - return errors.Errorf("output must a be a directory: %s", outputDir) + return fmt.Errorf("output must a be a directory: %s", outputDir) } if !util.ValidString(strings.Join(overlayNames, ""), "^[a-zA-Z0-9-._:]+$") { - return errors.Errorf("overlay names contains illegal characters: %v", overlayNames) + return fmt.Errorf("overlay names contains illegal characters: %v", overlayNames) } // Temporarily set umask to 0000, so directories in the overlay retain permissions defer syscall.Umask(syscall.Umask(0)) - wwlog.Verbose("Processing node/overlay: %s/%s", nodeData.Id(), strings.Join(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) - overlaySourceDir := GetOverlay(overlayName).Rootfs() - wwlog.Debug("Changing directory to OverlayDir: %s", overlaySourceDir) - err := os.Chdir(overlaySourceDir) - if err != nil { - return fmt.Errorf("directory: %s name: %s err: %w", overlaySourceDir, overlayName, ErrDoesNotExist) + overlayRootfs := GetOverlay(overlayName).Rootfs() + if !util.IsDir(overlayRootfs) { + return fmt.Errorf("overlay %s: %w", overlayName, ErrDoesNotExist) } - wwlog.Verbose("Walking the overlay structure: %s", overlaySourceDir) - err = filepath.Walk(".", func(location string, info os.FileInfo, err error) error { + wwlog.Debug("Walking the overlay structure: %s", overlayRootfs) + err := filepath.Walk(overlayRootfs, func(walkPath string, info os.FileInfo, err error) error { if err != nil { - return fmt.Errorf("error for %s: %w", location, err) + return fmt.Errorf("error for %s: %w", walkPath, err) } + wwlog.Debug("Found overlay file: %s", walkPath) - wwlog.Debug("Found overlay file: %s", location) + relPath, relErr := filepath.Rel(overlayRootfs, walkPath) + if relErr != nil { + wwlog.Warn("Error computing relative path for %s: %v", walkPath, relErr) + return relErr + } + outputPath := path.Join(outputDir, relPath) if info.IsDir() { - wwlog.Debug("Found directory: %s", location) + wwlog.Debug("Found directory: %s", walkPath) - err = os.MkdirAll(path.Join(outputDir, location), info.Mode()) - if err != nil { + if err = os.MkdirAll(outputPath, info.Mode()); err != nil { return fmt.Errorf("could not create directory within overlay: %w", err) } - err = util.CopyUIDGID(location, path.Join(outputDir, location)) - if err != nil { + if err = util.CopyUIDGID(walkPath, outputPath); err != nil { return fmt.Errorf("failed setting permissions on overlay directory: %w", err) } - wwlog.Debug("Created directory in overlay: %s", location) + wwlog.Debug("Created directory in overlay: %s", outputPath) - } else if filepath.Ext(location) == ".ww" { + } else if filepath.Ext(walkPath) == ".ww" { + originalOutputPath := outputPath + outputPath := strings.TrimSuffix(outputPath, ".ww") tstruct, err := InitStruct(overlayName, nodeData) if err != nil { return fmt.Errorf("failed to initial data for %s: %w", nodeData.Id(), err) } - tstruct.BuildSource = path.Join(overlaySourceDir, location) - wwlog.Verbose("Evaluating overlay template file: %s", location) - destFile := strings.TrimSuffix(location, ".ww") + tstruct.BuildSource = walkPath + wwlog.Verbose("Evaluating overlay template file: %s", walkPath) - buffer, backupFile, writeFile, err := RenderTemplateFile(location, tstruct) + buffer, backupFile, writeFile, err := RenderTemplateFile(walkPath, tstruct) if err != nil { - return fmt.Errorf("failed to render template %s: %w", location, err) + return fmt.Errorf("failed to render template %s: %w", walkPath, err) } - if writeFile { - destFileName := destFile - var fileBuffer bytes.Buffer - // search for magic file name comment - fileScanner := bufio.NewScanner(bytes.NewReader(buffer.Bytes())) - fileScanner.Split(ScanLines) - regFile := regexp.MustCompile(`.*{{\s*/\*\s*file\s*["'](.*)["']\s*\*/\s*}}.*`) - regLink := regexp.MustCompile(`.*{{\s*/\*\s*softlink\s*["'](.*)["']\s*\*/\s*}}.*`) - foundFileComment := false - for fileScanner.Scan() { - line := fileScanner.Text() - filenameFromTemplate := regFile.FindAllStringSubmatch(line, -1) - softlinkFromTemplate := regLink.FindAllStringSubmatch(line, -1) - if len(softlinkFromTemplate) != 0 { - wwlog.Debug("Creating soft link %s -> %s", destFileName, softlinkFromTemplate[0][1]) - return os.Symlink(softlinkFromTemplate[0][1], path.Join(outputDir, destFileName)) - } else if len(filenameFromTemplate) != 0 { - wwlog.Debug("Writing file %s", filenameFromTemplate[0][1]) - if foundFileComment { - err = CarefulWriteBuffer(path.Join(outputDir, destFileName), - fileBuffer, backupFile, info.Mode()) - if err != nil { - return fmt.Errorf("could not write file from template: %w", err) - } - err = util.CopyUIDGID(location, path.Join(outputDir, destFileName)) - if err != nil { - return fmt.Errorf("failed setting permissions on template output file: %w", err) - } - fileBuffer.Reset() + if !writeFile { + return nil + } + var fileBuffer bytes.Buffer + // search for magic file name comment + fileScanner := bufio.NewScanner(bytes.NewReader(buffer.Bytes())) + fileScanner.Split(ScanLines) + foundFileComment := false + for fileScanner.Scan() { + line := fileScanner.Text() + filenameFromTemplate := regFile.FindAllStringSubmatch(line, -1) + softlinkFromTemplate := regLink.FindAllStringSubmatch(line, -1) + if len(softlinkFromTemplate) != 0 { + wwlog.Debug("Creating soft link %s -> %s", outputPath, softlinkFromTemplate[0][1]) + return os.Symlink(softlinkFromTemplate[0][1], outputPath) + } else if len(filenameFromTemplate) != 0 { + wwlog.Debug("Writing file %s", filenameFromTemplate[0][1]) + if foundFileComment { + err = CarefulWriteBuffer(outputPath, fileBuffer, backupFile, info.Mode()) + if err != nil { + return fmt.Errorf("could not write file from template: %w", err) } - destFileName = path.Join(path.Dir(destFile), filenameFromTemplate[0][1]) - foundFileComment = true - } else { - _, _ = fileBuffer.WriteString(line) + err = util.CopyUIDGID(walkPath, outputPath) + if err != nil { + return fmt.Errorf("failed setting permissions on template output file: %w", err) + } + fileBuffer.Reset() + } + outputPath = path.Join(path.Dir(originalOutputPath), filenameFromTemplate[0][1]) + foundFileComment = true + } else { + if _, err = fileBuffer.WriteString(line); err != nil { + return fmt.Errorf("could not write to template buffer: %w", err) } } - err = CarefulWriteBuffer(path.Join(outputDir, destFileName), fileBuffer, backupFile, info.Mode()) - if err != nil { - return fmt.Errorf("could not write file from template: %w", err) - } - err = util.CopyUIDGID(location, path.Join(outputDir, destFileName)) - if err != nil { - return fmt.Errorf("failed setting permissions on template output file: %w", err) - } - - wwlog.Debug("Wrote template file into overlay: %s", destFile) - - // } else if b, _ := regexp.MatchString(`\.ww[a-zA-Z0-9\-\._]*$`, location); b { - // wwlog.Debug("Ignoring WW template file: %s", location) } + err = CarefulWriteBuffer(outputPath, fileBuffer, backupFile, info.Mode()) + if err != nil { + return fmt.Errorf("could not write file from template: %w", err) + } + err = util.CopyUIDGID(walkPath, outputPath) + if err != nil { + return fmt.Errorf("failed setting permissions on template output file: %w", err) + } + wwlog.Debug("Wrote template file into overlay: %s", outputPath) + } else if info.Mode()&os.ModeSymlink == os.ModeSymlink { - wwlog.Debug("Found symlink %s", location) - destination, err := os.Readlink(location) + wwlog.Debug("Found symlink %s", walkPath) + target, err := os.Readlink(walkPath) if err != nil { - wwlog.ErrorExc(err, "") + return fmt.Errorf("failed reading symlink: %w", err) } - if util.IsFile(path.Join(outputDir, location)) { - if !util.IsFile(path.Join(outputDir, location+".wwbackup")) { - wwlog.Debug("Target exists, creating backup file") - err = os.Rename(path.Join(outputDir, location), path.Join(outputDir, location+".wwbackup")) + if util.IsFile(outputPath) { + backupPath := outputPath + ".wwbackup" + if !util.IsFile(backupPath) { + wwlog.Debug("Output file already exists: moving to backup file") + if err = os.Rename(outputPath, backupPath); err != nil { + return fmt.Errorf("failed renaming to backup file: %w", err) + } } else { - wwlog.Debug("%s exists, keeping the backup file", path.Join(outputDir, location+".wwbackup")) - err = os.Remove(path.Join(outputDir, location)) - } - if err != nil { - wwlog.ErrorExc(err, "") + wwlog.Debug("%s exists, keeping the backup file", backupPath) + if err = os.Remove(outputPath); err != nil { + return fmt.Errorf("failed removing existing file: %w", err) + } } } - err = os.Symlink(destination, path.Join(outputDir, location)) - if err != nil { - wwlog.ErrorExc(err, "") + if err = os.Symlink(target, outputPath); err != nil { + return fmt.Errorf("failed creating symlink: %w", err) } + wwlog.Debug("Created symlink file: %s", outputPath) } else { - err := util.CopyFile(location, path.Join(outputDir, location)) - if err == nil { - wwlog.Debug("Copied file into overlay: %s", location) - } else { + if err := util.CopyFile(walkPath, outputPath); err != nil { return fmt.Errorf("could not copy file into overlay: %w", err) } + wwlog.Debug("Copied overlay file: %s", outputPath) } return nil }) + if err != nil { - return fmt.Errorf("failed to build overlay working directory: %w", err) + return fmt.Errorf("failed to build overlay image directory: %w", err) } } diff --git a/internal/pkg/overlay/overlay_test.go b/internal/pkg/overlay/overlay_test.go index a409041d..52aa8725 100644 --- a/internal/pkg/overlay/overlay_test.go +++ b/internal/pkg/overlay/overlay_test.go @@ -1,6 +1,7 @@ package overlay import ( + "fmt" "io" "os" "path" @@ -11,7 +12,6 @@ import ( warewulfconf "github.com/warewulf/warewulf/internal/pkg/config" "github.com/warewulf/warewulf/internal/pkg/node" "github.com/warewulf/warewulf/internal/pkg/testenv" - "github.com/warewulf/warewulf/internal/pkg/util" ) @@ -110,122 +110,263 @@ func Test_OverlayMethods(t *testing.T) { } } -var buildOverlayTests = []struct { - description string - nodeName string - context string - overlays []string - image string - contents []string - hasFiles bool -}{ - { - description: "if no node, context, or overlays are specified then no overlay image is generated", - nodeName: "", - context: "", - overlays: nil, - image: "", - contents: nil, - }, - { - description: "if only node is specified then no overlay image is generated", - nodeName: "node1", - context: "", - overlays: nil, - image: "", - contents: nil, - }, - { - description: "if only context is specified then context named image is generated", - nodeName: "", - context: "system", - overlays: nil, - image: "__SYSTEM__.img", - contents: nil, - }, - { - description: "if an overlay is specified without a node, then the overlay is built directly in the overlay directory", - nodeName: "", - context: "", - overlays: []string{"o1"}, - image: "o1.img", - contents: []string{"o1.txt"}, - }, - { - description: "if multiple overlays are specified without a node, then the combined overlay is built directly in the overlay directory", - nodeName: "", - context: "", - overlays: []string{"o1", "o2"}, - image: "o1-o2.img", - contents: []string{"o1.txt", "o2.txt"}, - }, - { - description: "if a single node overlay is specified, then the overlay is built in a node overlay directory", - nodeName: "node1", - context: "", - overlays: []string{"o1"}, - image: "node1/o1.img", - contents: []string{"o1.txt"}, - }, - { - description: "if multiple node overlays are specified, then the combined overlay is built in a node overlay directory", - nodeName: "node1", - context: "", - overlays: []string{"o1", "o2"}, - image: "node1/o1-o2.img", - contents: []string{"o1.txt", "o2.txt"}, - }, - { - description: "if no node system overlays are specified, then context pointed overlay is generated", - nodeName: "node1", - context: "system", - overlays: nil, - image: "node1/__SYSTEM__.img", - contents: nil, - }, - { - description: "if no node runtime overlays are specified, then context pointed overlay is generated", - nodeName: "node1", - context: "runtime", - overlays: nil, - image: "node1/__RUNTIME__.img", - contents: nil, - }, - { - description: "if a single node system overlay is specified, then a system overlay image is generated in a node overlay directory", - nodeName: "node1", - context: "system", - overlays: []string{"o1"}, - image: "node1/__SYSTEM__.img", - contents: []string{"o1.txt"}, - }, - { - description: "if a single node runtime overlay is specified, then a runtime overlay image is generated in a node overlay directory", - nodeName: "node1", - context: "runtime", - overlays: []string{"o1"}, - image: "node1/__RUNTIME__.img", - contents: []string{"o1.txt"}, - }, - { - description: "if multiple node system overlays are specified, then a system overlay image is generated with the contents of both overlays", - nodeName: "node1", - context: "system", - overlays: []string{"o1", "o2"}, - image: "node1/__SYSTEM__.img", - contents: []string{"o1.txt", "o2.txt"}, - }, - { - description: "if multiple node runtime overlays are specified, then a runtime overlay image is generated with the contents of both overlays", - nodeName: "node1", - context: "runtime", - overlays: []string{"o1", "o2"}, - image: "node1/__RUNTIME__.img", - contents: []string{"o1.txt", "o2.txt"}, - }, +func Test_BuildOverlayIndir(t *testing.T) { + var tests = map[string]struct { + node node.Node + overlays []string + overlayFiles map[string]string + overlayDirs []string + overlaySymlinks map[string]string + outputDir string + outputFiles map[string]string + outputDirs []string + outputSymlinks map[string]string + }{ + "empty": { + outputDir: "/image", + }, + "empty directory": { + overlays: []string{"o1"}, + overlayDirs: []string{"/var/lib/warewulf/overlays/o1/rootfs/testdir"}, + outputDir: "/image", + outputDirs: []string{"testdir"}, + }, + "single flat file": { + overlays: []string{"o1"}, + overlayFiles: map[string]string{ + "/var/lib/warewulf/overlays/o1/rootfs/testfile": "A test file from o1", + }, + outputDir: "/image", + outputFiles: map[string]string{ + "testfile": "A test file from o1", + }, + }, + "multiple overlays": { + overlays: []string{"o1", "o2"}, + overlayFiles: map[string]string{ + "/var/lib/warewulf/overlays/o1/rootfs/t1.txt": "A test file from o1", + "/var/lib/warewulf/overlays/o2/rootfs/t2.txt": "A test file from o2", + }, + outputDir: "/image", + outputFiles: map[string]string{ + "t1.txt": "A test file from o1", + "t2.txt": "A test file from o2", + }, + }, + "template": { + node: node.Node{Profile: node.Profile{Comment: "A node comment"}}, + overlays: []string{"o1"}, + overlayFiles: map[string]string{ + "/var/lib/warewulf/overlays/o1/rootfs/t1.txt.ww": "{{ .Comment }}", + "/image/t1.txt": "Previous content", + }, + outputDir: "/image", + outputFiles: map[string]string{ + "t1.txt": "A node comment", + "t1.txt.wwbackup": "Previous content", + }, + }, + "multifile": { + overlays: []string{"o1"}, + overlayFiles: map[string]string{ + "/var/lib/warewulf/overlays/o1/rootfs/file.txt.ww": ` +{{- file "t1.txt" }} +T1 +{{ file "t2.txt" }} +T2 +{{ file "t3.txt" }} +T3 +`, + }, + outputDir: "/image", + outputFiles: map[string]string{ + "t1.txt": "T1\n", + "t2.txt": "T2\n", + "t3.txt": "T3\n", + }, + }, + "symlink": { + overlays: []string{"o1"}, + overlaySymlinks: map[string]string{ + "/var/lib/warewulf/overlays/o1/rootfs/test-link": "/test-target", + }, + outputDir: "/image", + outputSymlinks: map[string]string{ + "test-link": "/test-target", + }, + }, + "symlink from template": { + overlays: []string{"o1"}, + overlayFiles: map[string]string{ + "/var/lib/warewulf/overlays/o1/rootfs/test-link.ww": ` +{{- softlink "/test-target" }} +`, + }, + outputDir: "/image", + outputSymlinks: map[string]string{ + "test-link": "/test-target", + }, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + env := testenv.New(t) + defer env.RemoveAll(t) + + for fileName, content := range tt.overlayFiles { + env.WriteFile(t, fileName, content) + } + for _, dirName := range tt.overlayDirs { + env.MkdirAll(t, dirName) + } + for linkName, target := range tt.overlaySymlinks { + env.Symlink(t, target, linkName) + } + + env.MkdirAll(t, tt.outputDir) + + assert.NoError(t, BuildOverlayIndir(tt.node, tt.overlays, env.GetPath(tt.outputDir))) + dirFiles := tt.outputDirs + for outputFile, _ := range tt.outputFiles { + dirFiles = append(dirFiles, outputFile) + } + for outputFile, _ := range tt.outputSymlinks { + dirFiles = append(dirFiles, outputFile) + } + sort.Strings(dirFiles) + assert.Equal(t, dirFiles, env.ReadDir(t, tt.outputDir)) + for fileName, content := range tt.outputFiles { + assert.Equal(t, content, env.ReadFile(t, path.Join(tt.outputDir, fileName))) + } + for _, dirName := range tt.outputDirs { + assert.True(t, util.IsDir(env.GetPath(path.Join(tt.outputDir, dirName))), fmt.Sprintf("%s is not a directory", dirName)) + } + for linkName, expTarget := range tt.outputSymlinks { + target, err := os.Readlink(env.GetPath(path.Join(tt.outputDir, linkName))) + assert.NoError(t, err) + assert.Equal(t, expTarget, target) + } + }) + } } func Test_BuildOverlay(t *testing.T) { + var tests = []struct { + description string + nodeName string + context string + overlays []string + image string + contents []string + hasFiles bool + }{ + { + description: "if no node, context, or overlays are specified then no overlay image is generated", + nodeName: "", + context: "", + overlays: nil, + image: "", + contents: nil, + }, + { + description: "if only node is specified then no overlay image is generated", + nodeName: "node1", + context: "", + overlays: nil, + image: "", + contents: nil, + }, + { + description: "if only context is specified then context named image is generated", + nodeName: "", + context: "system", + overlays: nil, + image: "__SYSTEM__.img", + contents: nil, + }, + { + description: "if an overlay is specified without a node, then the overlay is built directly in the overlay directory", + nodeName: "", + context: "", + overlays: []string{"o1"}, + image: "o1.img", + contents: []string{"o1.txt"}, + }, + { + description: "if multiple overlays are specified without a node, then the combined overlay is built directly in the overlay directory", + nodeName: "", + context: "", + overlays: []string{"o1", "o2"}, + image: "o1-o2.img", + contents: []string{"o1.txt", "o2.txt"}, + }, + { + description: "if a single node overlay is specified, then the overlay is built in a node overlay directory", + nodeName: "node1", + context: "", + overlays: []string{"o1"}, + image: "node1/o1.img", + contents: []string{"o1.txt"}, + }, + { + description: "if multiple node overlays are specified, then the combined overlay is built in a node overlay directory", + nodeName: "node1", + context: "", + overlays: []string{"o1", "o2"}, + image: "node1/o1-o2.img", + contents: []string{"o1.txt", "o2.txt"}, + }, + { + description: "if no node system overlays are specified, then context pointed overlay is generated", + nodeName: "node1", + context: "system", + overlays: nil, + image: "node1/__SYSTEM__.img", + contents: nil, + }, + { + description: "if no node runtime overlays are specified, then context pointed overlay is generated", + nodeName: "node1", + context: "runtime", + overlays: nil, + image: "node1/__RUNTIME__.img", + contents: nil, + }, + { + description: "if a single node system overlay is specified, then a system overlay image is generated in a node overlay directory", + nodeName: "node1", + context: "system", + overlays: []string{"o1"}, + image: "node1/__SYSTEM__.img", + contents: []string{"o1.txt"}, + }, + { + description: "if a single node runtime overlay is specified, then a runtime overlay image is generated in a node overlay directory", + nodeName: "node1", + context: "runtime", + overlays: []string{"o1"}, + image: "node1/__RUNTIME__.img", + contents: []string{"o1.txt"}, + }, + { + description: "if multiple node system overlays are specified, then a system overlay image is generated with the contents of both overlays", + nodeName: "node1", + context: "system", + overlays: []string{"o1", "o2"}, + image: "node1/__SYSTEM__.img", + contents: []string{"o1.txt", "o2.txt"}, + }, + { + description: "if multiple node runtime overlays are specified, then a runtime overlay image is generated with the contents of both overlays", + nodeName: "node1", + context: "runtime", + overlays: []string{"o1", "o2"}, + image: "node1/__RUNTIME__.img", + contents: []string{"o1.txt", "o2.txt"}, + }, + } + conf := warewulfconf.Get() overlayDir, overlayDirErr := os.MkdirTemp(os.TempDir(), "ww-test-overlay-*") assert.NoError(t, overlayDirErr) @@ -242,7 +383,7 @@ func Test_BuildOverlay(t *testing.T) { assert.NoError(t, err) } - for _, tt := range buildOverlayTests { + for _, tt := range tests { nodeInfo := node.NewNode(tt.nodeName) t.Run(tt.description, func(t *testing.T) { provisionDir, provisionDirErr := os.MkdirTemp(os.TempDir(), "ww-test-provision-*") @@ -269,83 +410,79 @@ func Test_BuildOverlay(t *testing.T) { } } -// Although these tests specify system and runtime overlays for the -// nodes, these overlays define the overlays that are defined in the -// configuration. BuildAllOverlays doesn't receive these as arguments, -// but builds all the overlays that are configured on the given node. -var buildAllOverlaysTests = []struct { - description string - nodes []string - systemOverlays [][]string - runtimeOverlays [][]string - createdOverlays []string -}{ - { - description: "empty input creates no overlays", - nodes: nil, - systemOverlays: nil, - runtimeOverlays: nil, - createdOverlays: nil, - }, - { - description: "a node with no overlays creates default system/runtime overlays", - nodes: []string{"node1"}, - systemOverlays: [][]string{{"o1"}}, - runtimeOverlays: [][]string{{"o1"}}, - createdOverlays: []string{"node1/__SYSTEM__.img.gz", "node1/__RUNTIME__.img.gz"}, - }, - { - description: "multiple nodes with no overlays creates default system/runtime overlays", - nodes: []string{"node1", "node2"}, - systemOverlays: [][]string{{"o1"}, {"o2"}}, - runtimeOverlays: [][]string{{"o1"}, {"o2"}}, - createdOverlays: []string{"node1/__SYSTEM__.img.gz", "node1/__RUNTIME__.img.gz", "node2/__SYSTEM__.img.gz", "node2/__RUNTIME__.img.gz"}, - }, - { - description: "a system overlay for a node generates a system overlay for that node", - nodes: []string{"node1"}, - systemOverlays: [][]string{{"o1"}}, - runtimeOverlays: nil, - createdOverlays: []string{"node1/__SYSTEM__.img.gz"}, - }, - { - description: "two nodes with different system overlays generates a system overlay for each node", - nodes: []string{"node1", "node2"}, - systemOverlays: [][]string{{"o1"}, {"o1", "o2"}}, - runtimeOverlays: nil, - createdOverlays: []string{"node1/__SYSTEM__.img.gz", "node2/__SYSTEM__.img.gz"}, - }, - { - description: "two nodes with a single runtime overlay generates a runtime overlay for the first node", - nodes: []string{"node1"}, - systemOverlays: nil, - runtimeOverlays: [][]string{{"o1"}}, - createdOverlays: []string{"node1/__RUNTIME__.img.gz"}, - }, - { - description: "two nodes with different runtime overlays generates a system overlay for each node", - nodes: []string{"node1", "node2"}, - systemOverlays: nil, - runtimeOverlays: [][]string{{"o1"}, {"o1", "o2"}}, - createdOverlays: []string{"node1/__RUNTIME__.img.gz", "node2/__RUNTIME__.img.gz"}, - }, - { - description: "a node with both a runtime and system overlay generates an image for each", - nodes: []string{"node1"}, - systemOverlays: [][]string{{"o1"}}, - runtimeOverlays: [][]string{{"o2"}}, - createdOverlays: []string{"node1/__RUNTIME__.img.gz", "node1/__SYSTEM__.img.gz"}, - }, - { - description: "two nodes with both runtime and system overlays generates each image for each node", - nodes: []string{"node1", "node2"}, - systemOverlays: [][]string{{"o1"}, {"o1", "o2"}}, - runtimeOverlays: [][]string{{"o2"}, {"o2"}}, - createdOverlays: []string{"node1/__RUNTIME__.img.gz", "node1/__SYSTEM__.img.gz", "node2/__RUNTIME__.img.gz", "node2/__SYSTEM__.img.gz"}, - }, -} - func Test_BuildAllOverlays(t *testing.T) { + var tests = []struct { + description string + nodes []string + systemOverlays [][]string + runtimeOverlays [][]string + createdOverlays []string + }{ + { + description: "empty input creates no overlays", + nodes: nil, + systemOverlays: nil, + runtimeOverlays: nil, + createdOverlays: nil, + }, + { + description: "a node with no overlays creates default system/runtime overlays", + nodes: []string{"node1"}, + systemOverlays: [][]string{{"o1"}}, + runtimeOverlays: [][]string{{"o1"}}, + createdOverlays: []string{"node1/__SYSTEM__.img.gz", "node1/__RUNTIME__.img.gz"}, + }, + { + description: "multiple nodes with no overlays creates default system/runtime overlays", + nodes: []string{"node1", "node2"}, + systemOverlays: [][]string{{"o1"}, {"o2"}}, + runtimeOverlays: [][]string{{"o1"}, {"o2"}}, + createdOverlays: []string{"node1/__SYSTEM__.img.gz", "node1/__RUNTIME__.img.gz", "node2/__SYSTEM__.img.gz", "node2/__RUNTIME__.img.gz"}, + }, + { + description: "a system overlay for a node generates a system overlay for that node", + nodes: []string{"node1"}, + systemOverlays: [][]string{{"o1"}}, + runtimeOverlays: nil, + createdOverlays: []string{"node1/__SYSTEM__.img.gz"}, + }, + { + description: "two nodes with different system overlays generates a system overlay for each node", + nodes: []string{"node1", "node2"}, + systemOverlays: [][]string{{"o1"}, {"o1", "o2"}}, + runtimeOverlays: nil, + createdOverlays: []string{"node1/__SYSTEM__.img.gz", "node2/__SYSTEM__.img.gz"}, + }, + { + description: "two nodes with a single runtime overlay generates a runtime overlay for the first node", + nodes: []string{"node1"}, + systemOverlays: nil, + runtimeOverlays: [][]string{{"o1"}}, + createdOverlays: []string{"node1/__RUNTIME__.img.gz"}, + }, + { + description: "two nodes with different runtime overlays generates a system overlay for each node", + nodes: []string{"node1", "node2"}, + systemOverlays: nil, + runtimeOverlays: [][]string{{"o1"}, {"o1", "o2"}}, + createdOverlays: []string{"node1/__RUNTIME__.img.gz", "node2/__RUNTIME__.img.gz"}, + }, + { + description: "a node with both a runtime and system overlay generates an image for each", + nodes: []string{"node1"}, + systemOverlays: [][]string{{"o1"}}, + runtimeOverlays: [][]string{{"o2"}}, + createdOverlays: []string{"node1/__RUNTIME__.img.gz", "node1/__SYSTEM__.img.gz"}, + }, + { + description: "two nodes with both runtime and system overlays generates each image for each node", + nodes: []string{"node1", "node2"}, + systemOverlays: [][]string{{"o1"}, {"o1", "o2"}}, + runtimeOverlays: [][]string{{"o2"}, {"o2"}}, + createdOverlays: []string{"node1/__RUNTIME__.img.gz", "node1/__SYSTEM__.img.gz", "node2/__RUNTIME__.img.gz", "node2/__SYSTEM__.img.gz"}, + }, + } + conf := warewulfconf.Get() overlayDir, overlayDirErr := os.MkdirTemp(os.TempDir(), "ww-test-overlay-*") assert.NoError(t, overlayDirErr) @@ -354,7 +491,7 @@ func Test_BuildAllOverlays(t *testing.T) { assert.NoError(t, os.Mkdir(path.Join(overlayDir, "o1"), 0700)) assert.NoError(t, os.Mkdir(path.Join(overlayDir, "o2"), 0700)) - for _, tt := range buildAllOverlaysTests { + for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { provisionDir, provisionDirErr := os.MkdirTemp(os.TempDir(), "ww-test-provision-*") assert.NoError(t, provisionDirErr) @@ -385,65 +522,65 @@ func Test_BuildAllOverlays(t *testing.T) { } } -var buildSpecificOverlaysTests = []struct { - description string - nodes []string - overlays []string - images []string - succeed bool -}{ - { - description: "building no overlays for no nodes generates no error and no images", - nodes: nil, - overlays: nil, - images: nil, - succeed: true, - }, - { - description: "building no overlays for a node generates no error and no images", - nodes: []string{"node1"}, - overlays: nil, - images: nil, - succeed: true, - }, - { - description: "building no overlays for two nodes generates no error and no images", - nodes: []string{"node1", "node2"}, - overlays: nil, - images: nil, - succeed: true, - }, - { - description: "building an overlay for a node generates an overlay image in that node's overlay directory", - nodes: []string{"node1"}, - overlays: []string{"o1"}, - images: []string{"node1/o1.img"}, - succeed: true, - }, - { - description: "building an overlay for two nodes generates an overlay image in each node's overlay directory", - nodes: []string{"node1", "node2"}, - overlays: []string{"o1"}, - images: []string{"node1/o1.img", "node2/o1.img"}, - succeed: true, - }, - { - description: "building multiple overlays for a node generates an overlay image for each overlay in the node's overlay directory", - nodes: []string{"node1"}, - overlays: []string{"o1", "o2"}, - images: []string{"node1/o1.img", "node1/o2.img"}, - succeed: true, - }, - { - description: "building multiple overlays for two nodes generates an overlay image for each overlay in each node's overlay directory", - nodes: []string{"node1", "node2"}, - overlays: []string{"o1", "o2"}, - images: []string{"node1/o1.img", "node1/o2.img", "node2/o1.img", "node2/o2.img"}, - succeed: true, - }, -} - func Test_BuildSpecificOverlays(t *testing.T) { + var tests = []struct { + description string + nodes []string + overlays []string + images []string + succeed bool + }{ + { + description: "building no overlays for no nodes generates no error and no images", + nodes: nil, + overlays: nil, + images: nil, + succeed: true, + }, + { + description: "building no overlays for a node generates no error and no images", + nodes: []string{"node1"}, + overlays: nil, + images: nil, + succeed: true, + }, + { + description: "building no overlays for two nodes generates no error and no images", + nodes: []string{"node1", "node2"}, + overlays: nil, + images: nil, + succeed: true, + }, + { + description: "building an overlay for a node generates an overlay image in that node's overlay directory", + nodes: []string{"node1"}, + overlays: []string{"o1"}, + images: []string{"node1/o1.img"}, + succeed: true, + }, + { + description: "building an overlay for two nodes generates an overlay image in each node's overlay directory", + nodes: []string{"node1", "node2"}, + overlays: []string{"o1"}, + images: []string{"node1/o1.img", "node2/o1.img"}, + succeed: true, + }, + { + description: "building multiple overlays for a node generates an overlay image for each overlay in the node's overlay directory", + nodes: []string{"node1"}, + overlays: []string{"o1", "o2"}, + images: []string{"node1/o1.img", "node1/o2.img"}, + succeed: true, + }, + { + description: "building multiple overlays for two nodes generates an overlay image for each overlay in each node's overlay directory", + nodes: []string{"node1", "node2"}, + overlays: []string{"o1", "o2"}, + images: []string{"node1/o1.img", "node1/o2.img", "node2/o1.img", "node2/o2.img"}, + succeed: true, + }, + } + conf := warewulfconf.Get() overlayDir, overlayDirErr := os.MkdirTemp(os.TempDir(), "ww-test-overlay-*") assert.NoError(t, overlayDirErr) @@ -452,7 +589,7 @@ func Test_BuildSpecificOverlays(t *testing.T) { assert.NoError(t, os.Mkdir(path.Join(overlayDir, "o1"), 0700)) assert.NoError(t, os.Mkdir(path.Join(overlayDir, "o2"), 0700)) - for _, tt := range buildSpecificOverlaysTests { + for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { provisionDir, provisionDirErr := os.MkdirTemp(os.TempDir(), "ww-test-provision-*") assert.NoError(t, provisionDirErr) diff --git a/internal/pkg/testenv/testenv.go b/internal/pkg/testenv/testenv.go index 115688e9..30405d24 100644 --- a/internal/pkg/testenv/testenv.go +++ b/internal/pkg/testenv/testenv.go @@ -140,12 +140,22 @@ func (env *TestEnv) ImportFile(t *testing.T, fileName string, inputFileName stri env.WriteFile(t, fileName, string(buffer)) } -// CreateFile creates an empty file to fileName, creating any necessary intermediate directories +// CreateFile creates an empty file at fileName, creating any necessary intermediate directories // relative to the test environment. func (env *TestEnv) CreateFile(t *testing.T, fileName string) { env.WriteFile(t, fileName, "") } +// Symlink creates a symlink at fileName to target, creating any necessary intermediate directories +// relative to the test environment. +func (env *TestEnv) Symlink(t *testing.T, target string, fileName string) { + dirName := filepath.Dir(fileName) + env.MkdirAll(t, dirName) + + err := os.Symlink(target, env.GetPath(fileName)) + assert.NoError(t, err) +} + // ReadFile returns the content of fileName as converted to a // string. // @@ -156,6 +166,20 @@ func (env *TestEnv) ReadFile(t *testing.T, fileName string) string { return string(buffer) } +// ReadDir returns the content of dirName as converted to a +// slice of strings. +// +// Asserts no errors occur. +func (env *TestEnv) ReadDir(t *testing.T, dirName string) []string { + entries, err := os.ReadDir(env.GetPath(dirName)) + assert.NoError(t, err) + var entryStrs []string + for _, entry := range entries { + entryStrs = append(entryStrs, entry.Name()) + } + return entryStrs +} + // RemoveAll deletes the temporary directory, and all its contents, // for the test environment. // diff --git a/internal/pkg/warewulfd/util.go b/internal/pkg/warewulfd/util.go index a40c1781..973cc6a6 100644 --- a/internal/pkg/warewulfd/util.go +++ b/internal/pkg/warewulfd/util.go @@ -45,9 +45,7 @@ func sendFile( } 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) - err = nil build := !util.IsFile(stage_file) wwlog.Verbose("stage file: %s", stage_file) if !build && autobuild {