diff --git a/pkg/isoeditor/kargs.go b/pkg/isoeditor/kargs.go index e8ca1bcf..911f69f1 100644 --- a/pkg/isoeditor/kargs.go +++ b/pkg/isoeditor/kargs.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "os" + "path/filepath" "regexp" "github.com/openshift/assisted-image-service/pkg/overlay" @@ -48,6 +49,112 @@ func KargsFiles(isoPath string) ([]string, error) { return kargsFiles(isoPath, ReadFileFromISO) } +// EmbedKargsIntoBootImage appends custom kernel arguments into a staging ISO image that +// already contains an ignition config, using offsets and size limits defined in `coreos/kargs.json` +// that are extracted from the original base ISO. +// +// This function is only invoked when both the ignition config and kernel arguments must be embedded +// into the same boot image. +func EmbedKargsIntoBootImage(baseIsoPath string, stagingIsoPath string, customKargs string) error { + + // Read the kargs.json file content from the ISO + kargsData, err := ReadFileFromISO(baseIsoPath, kargsConfigFilePath) + if err != nil { + return fmt.Errorf("failed to read kargs config from %s: %w", kargsConfigFilePath, err) + } + + // Loading the kargs config JSON file + var kargsConfig struct { + Default string `json:"default"` + Files []struct { + Path string `json:"path"` + Offset int64 `json:"offset"` + End string `json:"end"` + Pad string `json:"pad"` + } `json:"files"` + Size int `json:"size"` + } + if err := json.Unmarshal(kargsData, &kargsConfig); err != nil { + return fmt.Errorf("failed to parse %s: %w", kargsConfigFilePath, err) + } + + // Make sure kargs config files are present + if len(kargsConfig.Files) == 0 { + return fmt.Errorf("no kargs file entries found in %s", kargsConfigFilePath) + } + + // Fetch kargs files from the ISO + files, err := KargsFiles(baseIsoPath) + if err != nil { + return err + } + + // Embed kargs config into each file + for _, filePath := range files { + // Check if file exists + absFilePath := filepath.Join(stagingIsoPath, filePath) + fileExists, err := fileExists(absFilePath) + if err != nil { + return err + } + if !fileExists { + return fmt.Errorf("file %s does not exist", absFilePath) + } + + // Finding offset for the target filePath + var kargsOffset int64 + for _, file := range kargsConfig.Files { + if file.Path == filePath { + kargsOffset = file.Offset + break + } + } + + // Calculate the customKargsOffset + existingKargs := []byte(kargsConfig.Default) + appendKargsOffset := kargsOffset + int64(len(existingKargs)) + + // Now open the file for read/write and patch at offset + f, err := os.OpenFile(absFilePath, os.O_RDWR, 0) + if err != nil { + return fmt.Errorf("failed to open target file %s: %w", absFilePath, err) + } + defer f.Close() + + // Seek to the kargs offset in the filePath + _, err = f.Seek(appendKargsOffset, io.SeekStart) + if err != nil { + return fmt.Errorf("failed to seek to kargs offset %d in %s: %w", appendKargsOffset, absFilePath, err) + } + + // Determine available kargs field size if possible + var maxLen int64 + if kargsConfig.Size > 0 { + maxLen = int64(kargsConfig.Size) + } else { + // Try to get remaining bytes until next file or EOF (best-effort) + // If we can't determine a safe max, at least ensure we don't write beyond file size. + fi, statErr := f.Stat() + if statErr == nil { + maxLen = fi.Size() - appendKargsOffset + } + } + + // Ensure to not overflow the kargs field size + kargsLength := len(existingKargs) + len(customKargs) + if maxLen > 0 && int64(kargsLength) > maxLen { + return fmt.Errorf("kargs length %d exceeds available field size %d", kargsLength, maxLen) + } + + // Write the kargs bytes + if _, err = f.Write([]byte(customKargs)); err != nil { + return fmt.Errorf("failed writing kargs into %s: %w", absFilePath, err) + } + } + + return nil +} + func kargsFileData(isoPath string, file string, appendKargs []byte) (FileData, error) { baseISO, err := os.Open(isoPath) if err != nil { diff --git a/pkg/isoeditor/kargs_test.go b/pkg/isoeditor/kargs_test.go index 5ea43b26..c1fda9cf 100644 --- a/pkg/isoeditor/kargs_test.go +++ b/pkg/isoeditor/kargs_test.go @@ -2,6 +2,8 @@ package isoeditor import ( "errors" + "os" + "path/filepath" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -153,3 +155,139 @@ menuentry 'Fedora CoreOS (Live)' --class fedora --class gnu-linux --class gnu -- }) }) }) + +// Tests for EmbedKargsIntoBootImage +var _ = Describe("EmbedKargsIntoBootImage", func() { + var ( + baseDir string // acts as baseIsoPath (where /coreos/kargs.json is read from) + stagingDir string // acts as stagingIsoPath (where files are written) + ) + + writeBaseKargsJSON := func(json string) { + p := filepath.Join(baseDir, "coreos", "kargs.json") + Expect(os.MkdirAll(filepath.Dir(p), 0755)).To(Succeed()) + Expect(os.WriteFile(p, []byte(json), 0644)).To(Succeed()) + } + + // helper to create a target file inside staging dir with a given size (filled with zeros) + createStagingFile := func(rel string, size int) string { + full := filepath.Join(stagingDir, rel) + Expect(os.MkdirAll(filepath.Dir(full), 0755)).To(Succeed()) + buf := make([]byte, size) + Expect(os.WriteFile(full, buf, 0644)).To(Succeed()) + return full + } + + BeforeEach(func() { + var err error + baseDir, err = os.MkdirTemp("", "iso-base") + Expect(err).ToNot(HaveOccurred()) + stagingDir, err = os.MkdirTemp("", "iso-staging") + Expect(err).ToNot(HaveOccurred()) + }) + + AfterEach(func() { + os.RemoveAll(baseDir) + os.RemoveAll(stagingDir) + }) + + It("fails when /coreos/kargs.json cannot be read from base ISO path", func() { + // Do NOT create base coreos/kargs.json + err := EmbedKargsIntoBootImage(baseDir, stagingDir, "any") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to read kargs config")) + }) + + It("fails when /coreos/kargs.json is malformed", func() { + writeBaseKargsJSON(`{ not valid json }`) + err := EmbedKargsIntoBootImage(baseDir, stagingDir, "newKargs") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to parse")) + }) + + It("fails when no kargs file entries are present", func() { + writeBaseKargsJSON(`{"default":"abc","files":[],"size":10}`) + err := EmbedKargsIntoBootImage(baseDir, stagingDir, "extra") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("no kargs file entries")) + }) + + It("fails when a listed staging file does not exist", func() { + writeBaseKargsJSON(`{ + "default": "abc", + "files": [{"path":"cdboot.img","offset":10}], + "size": 100 + }`) + // Don't create cdboot.img in staging + err := EmbedKargsIntoBootImage(baseDir, stagingDir, "zzz") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("does not exist")) + }) + + It("fails when kargs length exceeds configured Size", func() { + // default=3 chars, custom=9 chars -> total 12 > size 10 + writeBaseKargsJSON(`{ + "default": "abc", + "files": [{"path":"cdboot.img","offset":0}], + "size": 10 + }`) + _ = createStagingFile("cdboot.img", 32) + err := EmbedKargsIntoBootImage(baseDir, stagingDir, "toolonggg") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("exceeds available field size")) + }) + + It("fails when size is not provided but available space (by file length) is insufficient", func() { + // Size=0 means use file size heuristic: + // file size 8, offset=4, default len=3 -> append offset = 7, remaining = 1 + // total needed default+custom = 3 + 3 = 6 > 1 -> error + writeBaseKargsJSON(`{ + "default": "abc", + "files": [{"path":"cdboot.img","offset":4}], + "size": 0 + }`) + _ = createStagingFile("cdboot.img", 8) + err := EmbedKargsIntoBootImage(baseDir, stagingDir, "xyz") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("exceeds available field size")) + }) + + It("fails when the staging path exists but is a directory (open for write fails)", func() { + writeBaseKargsJSON(`{ + "default": "abc", + "files": [{"path":"cdboot.img","offset":5}], + "size": 100 + }`) + // Create a directory named cdboot.img + Expect(os.MkdirAll(filepath.Join(stagingDir, "cdboot.img"), 0755)).To(Succeed()) + err := EmbedKargsIntoBootImage(baseDir, stagingDir, "ok") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to open target file")) + }) + + It("successfully embeds kargs into TWO different files with different offsets", func() { + // default is "abc" (len=3) + writeBaseKargsJSON(`{ + "default": "abc", + "files": [ + {"path":"cdboot.img","offset":10}, + {"path":"coreos/kargs.json","offset":20} + ], + "size": 1024 + }`) + cdboot := createStagingFile("cdboot.img", 256) + kargsBin := createStagingFile("coreos/kargs.json", 256) + + custom := "dual-file=ok" + Expect(EmbedKargsIntoBootImage(baseDir, stagingDir, custom)).To(Succeed()) + + // Verify writes at offset + len(default) + cd, err := os.ReadFile(cdboot) + Expect(err).ToNot(HaveOccurred()) + Expect(string(cd[10+3 : 10+3+len(custom)])).To(Equal(custom)) + + kb, err := os.ReadFile(kargsBin) + Expect(err).ToNot(HaveOccurred()) + Expect(string(kb[20+3 : 20+3+len(custom)])).To(Equal(custom)) + }) +})