diff --git a/initrd/directory.go b/initrd/directory.go index 8f301a020c..53d6666bb1 100644 --- a/initrd/directory.go +++ b/initrd/directory.go @@ -65,6 +65,12 @@ func (initrd *directory) Build(ctx context.Context) (string, error) { } } + if _, err := os.Stat(initrd.opts.output); err == nil { + if err = os.Remove(initrd.opts.output); err != nil { + return "", err + } + } + f, err := os.OpenFile(initrd.opts.output, os.O_RDWR|os.O_CREATE, 0o644) if err != nil { return "", fmt.Errorf("could not open initramfs file: %w", err) @@ -75,6 +81,11 @@ func (initrd *directory) Build(ctx context.Context) (string, error) { writer := cpio.NewWriter(f) defer writer.Close() + ignoringItems, err := getKraftignoreItems(ctx, initrd) + if err != nil { + return "", err + } + if err := filepath.WalkDir(initrd.path, func(path string, d fs.DirEntry, err error) error { if err != nil { return fmt.Errorf("received error before parsing path: %w", err) @@ -86,6 +97,21 @@ func (initrd *directory) Build(ctx context.Context) (string, error) { } internal = "." + filepath.ToSlash(internal) + if len(ignoringItems) > 0 && path != initrd.path { + switch isExistInKraftignoreFile(internal, d, ignoringItems) { + case SkipDir: + log.G(ctx). + WithField("directory", internal). + Trace("ignoring from archiving") + return filepath.SkipDir + case Exist: + log.G(ctx). + WithField("file", internal). + Trace("ignoring from archiving") + return nil + } + } + info, err := d.Info() if err != nil { return fmt.Errorf("could not get directory entry info: %w", err) diff --git a/initrd/kraftignore.go b/initrd/kraftignore.go new file mode 100644 index 0000000000..a7c8f09b81 --- /dev/null +++ b/initrd/kraftignore.go @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2022, Unikraft GmbH and The KraftKit Authors. +// Licensed under the BSD-3-Clause License (the "License"). +// You may not use this file except in compliance with the License. +package initrd + +import ( + "bufio" + "context" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "kraftkit.sh/log" +) + +// kraftignore filename +const KraftignoreFileName = ".kraftignore" + +type IgnoringFileType string + +const ( + Exist = IgnoringFileType("Exist") + NotExist = IgnoringFileType("NotExist") + SkipDir = IgnoringFileType("SkipDir") +) + +// returns file and directory names specified in .kraftignore +func getKraftignoreItems(ctx context.Context, initrd *directory) ([]string, error) { + if initrd.opts.kraftignorePath == "" { + cwd, err := os.Getwd() + if err != nil { + return []string{}, err + } + initrd.opts.kraftignorePath = filepath.Join(cwd, KraftignoreFileName) + } + + if _, err := os.Stat(initrd.opts.kraftignorePath); errors.Is(err, os.ErrNotExist) { + return []string{}, nil + } else if err != nil { + return []string{}, err + } + + kraftignoreFile, err := os.Open(initrd.opts.kraftignorePath) + if err != nil { + return []string{}, err + } + + defer func() { + kraftIgnoreErr := kraftignoreFile.Close() + if kraftIgnoreErr != nil { + if err != nil { + err = fmt.Errorf("%w: %w", err, kraftIgnoreErr) + } else { + err = kraftIgnoreErr + } + } + }() + + kraftignoreScanner := bufio.NewScanner(kraftignoreFile) + kraftignoreScanner.Split(bufio.ScanLines) + var kraftignoreFileLines, ignoringItems []string + + for kraftignoreScanner.Scan() { + kraftignoreFileLines = append(kraftignoreFileLines, kraftignoreScanner.Text()) + } + + for lineNum, line := range kraftignoreFileLines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + items := findLineItems(line) + for _, item := range items { + if item == "" || item == "#" { + continue + } + + if hasGlobPatterns(item) { + log.G(ctx). + WithField("file", initrd.opts.kraftignorePath). + Warn("contains a glob pattern ", item, + " at line ", lineNum, + " which is not supported by Kraftkit") + continue + } + + if _, err := os.Stat(filepath.Join(initrd.path, item)); os.IsNotExist(err) { + log.G(ctx). + WithField("file", initrd.opts.kraftignorePath). + Warn("contains ", item, + " at line ", lineNum, + " which does not exist in the provided rootfs directory") + continue + } + + ignoringItems = append(ignoringItems, item) + } + } + + return ignoringItems, err +} + +// checks if the path exist in .kraftignore +func isExistInKraftignoreFile(internal string, pathInfo fs.DirEntry, kraftignoreItems []string) IgnoringFileType { + for _, ignoringItem := range kraftignoreItems { + if internal == ignoringItem { + if pathInfo.IsDir() { + return SkipDir + } + return Exist + } + } + return NotExist +} + +// checks if the item contains glob pattern +func hasGlobPatterns(item string) bool { + return strings.ContainsAny(item, "*?![{") +} + +// finds items in a line of .kraftignore +func findLineItems(line string) []string { + items := strings.Split(line, " ") + for index := 0; index < len(items); index++ { + charToFind := "" + if strings.HasPrefix(items[index], `"`) && !strings.HasSuffix(items[index], `"`) { + charToFind = `"` + } else if strings.HasPrefix(items[index], `'`) && !strings.HasSuffix(items[index], `'`) { + charToFind = `'` + } + + if len(charToFind) > 0 { + i := index + 1 + for ; i < len(items) && !strings.HasSuffix(items[i], charToFind); i++ { + items[index] += " " + items[i] + items = append(items[:i], items[i+1:]...) + i-- + } + items[index] += " " + items[i] + items = append(items[:i], items[i+1:]...) + } + items[index] = strings.Trim(items[index], `"`) + items[index] = strings.Trim(items[index], `'`) + items[index] = strings.TrimSpace(items[index]) + items[index] = strings.TrimPrefix(items[index], "../") + if !strings.HasPrefix(items[index], "./") { + if !strings.HasPrefix(items[index], "/") { + items[index] = "/" + items[index] + } + items[index] = "." + items[index] + } + items[index] = strings.TrimSuffix(items[index], string(filepath.Separator)) + } + return items +} diff --git a/initrd/options.go b/initrd/options.go index 65cbf89dff..c27cb5d508 100644 --- a/initrd/options.go +++ b/initrd/options.go @@ -4,10 +4,15 @@ // You may not use this file except in compliance with the License. package initrd +import ( + "path/filepath" +) + type InitrdOptions struct { - output string - cacheDir string - arch string + output string + cacheDir string + arch string + kraftignorePath string } type InitrdOption func(*InitrdOptions) error @@ -41,3 +46,11 @@ func WithArchitecture(arch string) InitrdOption { return nil } } + +// WithKraftignorePath sets the path for .kraftignore. +func WithKraftignorePath(dir string) InitrdOption { + return func(opts *InitrdOptions) error { + opts.kraftignorePath = filepath.Join(dir, KraftignoreFileName) + return nil + } +}