Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions pkg/isoeditor/kargs.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"regexp"

"github.com/openshift/assisted-image-service/pkg/overlay"
Expand Down Expand Up @@ -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 {
Expand Down
138 changes: 138 additions & 0 deletions pkg/isoeditor/kargs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package isoeditor

import (
"errors"
"os"
"path/filepath"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -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))
})
})