diff --git a/pkg/isoeditor/rhcos.go b/pkg/isoeditor/rhcos.go index adf330c3..d645f409 100644 --- a/pkg/isoeditor/rhcos.go +++ b/pkg/isoeditor/rhcos.go @@ -1,10 +1,12 @@ package isoeditor import ( + "encoding/json" "fmt" "os" "path/filepath" "regexp" + "strings" "github.com/openshift/assisted-image-service/internal/common" log "github.com/sirupsen/logrus" @@ -19,6 +21,36 @@ const ( RootfsImagePath = "images/pxeboot/rootfs.img" ) +// transformKernelArgs applies the standard kernel argument transformations: +// 1. Remove coreos.liveiso parameter +// 2. Add coreos.live.rootfs_url parameter at the specified insertion point +func transformKernelArgs(content string, insertionPattern string, rootFSURL string, fileEntry *kargsFileEntry) (string, error) { + // Validate rootfs URL + if strings.Contains(rootFSURL, "$") { + return "", fmt.Errorf("invalid rootfs URL: contains invalid character '$'") + } + if strings.Contains(rootFSURL, "\\") { + return "", fmt.Errorf("invalid rootfs URL: contains invalid character '\\'") + } + + var err error + + // Remove the coreos.liveiso parameter + content, err = editString(content, `\b(?Pcoreos\.liveiso=\S+ ?)`, "", fileEntry) + if err != nil { + return "", err + } + + // Add the rootfs_url parameter at the specified insertion point + replacement := " coreos.live.rootfs_url=\"" + rootFSURL + "\"" + content, err = editString(content, insertionPattern, replacement, fileEntry) + if err != nil { + return "", err + } + + return content, nil +} + //go:generate mockgen -package=isoeditor -destination=mock_editor.go . Editor type Editor interface { CreateMinimalISOTemplate(fullISOPath, rootFSURL, arch, minimalISOPath, openshiftVersion, nmstatectlPath string) error @@ -52,19 +84,11 @@ func CreateMinimalISO(extractDir, volumeID, rootFSURL, arch, minimalISOPath stri includeNmstateRamDisk = true } - if err := fixGrubConfig(rootFSURL, extractDir, includeNmstateRamDisk); err != nil { - log.WithError(err).Warnf("Failed to edit grub config") + if err := updateKargs(extractDir, rootFSURL, includeNmstateRamDisk, arch); err != nil { + log.WithError(err).Warnf("Failed to update kargs offsets and sizes") return err } - // ignore isolinux.cfg for ppc64le because it doesn't exist - if arch != "ppc64le" { - if err := fixIsolinuxConfig(rootFSURL, extractDir, includeNmstateRamDisk); err != nil { - log.WithError(err).Warnf("Failed to edit isolinux config") - return err - } - } - if err := Create(minimalISOPath, extractDir, volumeID); err != nil { return err } @@ -148,13 +172,16 @@ func embedInitrdPlaceholders(extractDir string) error { return nil } -func fixGrubConfig(rootFSURL, extractDir string, includeNmstateRamDisk bool) error { +// fixGrubConfig modifies grub.cfg and updates kargs config in place +func fixGrubConfig(rootFSURL, extractDir string, includeNmstateRamDisk bool, kargs *kargsConfig) error { availableGrubPaths := []string{"EFI/redhat/grub.cfg", "EFI/fedora/grub.cfg", "boot/grub/grub.cfg", "EFI/centos/grub.cfg"} var foundGrubPath string + var fileEntry *kargsFileEntry for _, pathSection := range availableGrubPaths { path := filepath.Join(extractDir, pathSection) if _, err := os.Stat(path); err == nil { foundGrubPath = path + fileEntry = kargs.FindFileByPath(pathSection) break } } @@ -162,65 +189,230 @@ func fixGrubConfig(rootFSURL, extractDir string, includeNmstateRamDisk bool) err return fmt.Errorf("no grub.cfg found, possible paths are %v", availableGrubPaths) } - // Add the rootfs url - replacement := fmt.Sprintf("$1 $2 'coreos.live.rootfs_url=%s'", rootFSURL) - if err := editFile(foundGrubPath, `(?m)^(\s+linux) (.+| )+$`, replacement); err != nil { + // Read the file content + content, err := os.ReadFile(foundGrubPath) + if err != nil { return err } + contentStr := string(content) - // Remove the coreos.liveiso parameter - if err := editFile(foundGrubPath, ` coreos.liveiso=\S+`, ""); err != nil { + // Typical grub.cfg lines we're modifying: + // + // linux /images/pxeboot/vmlinuz rw coreos.liveiso=rhcos-9.6.20250523-0 ignition.firstboot ignition.platform.id=metal + // initrd /images/pxeboot/initrd.img /images/ignition.img + + // Apply standard kernel argument transformations (remove coreos.liveiso, add rootfs_url) + contentStr, err = transformKernelArgs(contentStr, `(?m)^(\s+linux .+)(?P$)`, rootFSURL, fileEntry) + if err != nil { return err } - // Edit config to add custom ramdisk image to initrd + // Edit config to add custom ramdisk image to initrd - capture the end-of-line position to append our images + var initrdReplacement string if includeNmstateRamDisk { - if err := editFile(foundGrubPath, `(?m)^(\s+initrd) (.+| )+$`, fmt.Sprintf("$1 $2 %s %s", ramDiskImagePath, nmstateDiskImagePath)); err != nil { - return err - } + initrdReplacement = " " + ramDiskImagePath + " " + nmstateDiskImagePath } else { - if err := editFile(foundGrubPath, `(?m)^(\s+initrd) (.+| )+$`, fmt.Sprintf("$1 $2 %s", ramDiskImagePath)); err != nil { - return err - } + initrdReplacement = " " + ramDiskImagePath + } + contentStr, err = editString(contentStr, `(?m)^(\s+initrd .+)(?P$)`, initrdReplacement, fileEntry) + if err != nil { + return err } - return nil + // Write the modified content back to the file + return os.WriteFile(foundGrubPath, []byte(contentStr), 0600) } -func fixIsolinuxConfig(rootFSURL, extractDir string, includeNmstateRamDisk bool) error { - replacement := fmt.Sprintf("$1 $2 coreos.live.rootfs_url=%s", rootFSURL) - if err := editFile(filepath.Join(extractDir, "isolinux/isolinux.cfg"), `(?m)^(\s+append) (.+| )+$`, replacement); err != nil { +// fixIsolinuxConfig modifies isolinux.cfg and updates kargs config in place +func fixIsolinuxConfig(rootFSURL, extractDir string, includeNmstateRamDisk bool, kargs *kargsConfig) error { + relativeIsolinuxPath := strings.TrimPrefix(defaultIsolinuxFilePath, "/") + isolinuxPath := filepath.Join(extractDir, relativeIsolinuxPath) + + // Read the file content + content, err := os.ReadFile(isolinuxPath) + if err != nil { return err } + contentStr := string(content) - if err := editFile(filepath.Join(extractDir, "isolinux/isolinux.cfg"), ` coreos.liveiso=\S+`, ""); err != nil { + // Typical isolinux.cfg line we're modifying: + // + // append initrd=/images/pxeboot/initrd.img,/images/ignition.img rw coreos.liveiso=rhcos-9.6.20250523-0 ignition.firstboot ignition.platform.id=metal + + // Find the kargs file entry for this file + fileEntry := kargs.FindFileByPath(relativeIsolinuxPath) + + // Apply standard kernel argument transformations (remove coreos.liveiso, add rootfs_url) + contentStr, err = transformKernelArgs(contentStr, `(?m)^(\s+append .+)(?P$)`, rootFSURL, fileEntry) + if err != nil { return err } + // Add ramdisk images to initrd specification - capture the position right after the initrd argument to append our images + var initrdReplacement string if includeNmstateRamDisk { - if err := editFile(filepath.Join(extractDir, "isolinux/isolinux.cfg"), `(?m)^(\s+append.*initrd=\S+) (.*)$`, fmt.Sprintf("${1},%s,%s ${2}", ramDiskImagePath, nmstateDiskImagePath)); err != nil { - return err - } + initrdReplacement = "," + ramDiskImagePath + "," + nmstateDiskImagePath } else { - if err := editFile(filepath.Join(extractDir, "isolinux/isolinux.cfg"), `(?m)^(\s+append.*initrd=\S+) (.*)$`, fmt.Sprintf("${1},%s ${2}", ramDiskImagePath)); err != nil { - return err - } + initrdReplacement = "," + ramDiskImagePath + } + contentStr, err = editString(contentStr, `(?m)^(\s+append.*initrd=\S+)(?P)`, initrdReplacement, fileEntry) + if err != nil { + return err + } + + // Write the modified content back to the file + return os.WriteFile(isolinuxPath, []byte(contentStr), 0600) +} + +// editString applies a regex replacement to a string and returns the modified string +// It looks for a named capture group called "replace" and replaces only that content, using precise string manipulation +func editString(content string, reString string, replacement string, fileEntry *kargsFileEntry) (string, error) { + re := regexp.MustCompile(reString) + + // Get the index of the "replace" named capture group + replaceIndex := re.SubexpIndex("replace") + if replaceIndex == -1 { + return "", fmt.Errorf("regex must have a named capture group called 'replace'") + } + + // Find the first match with subgroups + submatchIndexes := re.FindStringSubmatchIndex(content) + if submatchIndexes == nil { + // No match found + return content, nil } + // submatchIndexes contains [fullMatchStart, fullMatchEnd, group1Start, group1End, group2Start, group2End, ...] + if len(submatchIndexes) < (replaceIndex+1)*2 { + return "", fmt.Errorf("regex match does not contain the 'replace' capture group") + } + + replaceStart := submatchIndexes[replaceIndex*2] + replaceEnd := submatchIndexes[replaceIndex*2+1] + + // Replace only the "replace" capturing group + newContent := content[:replaceStart] + replacement + content[replaceEnd:] + + if content == newContent { + return content, nil + } + + if fileEntry == nil || fileEntry.Offset == nil { + return newContent, nil + } + + embedStart := *fileEntry.Offset + fileSizeChange := int64(len(newContent)) - int64(len(content)) + + replaceStartPos := int64(replaceStart) + replaceEndPos := int64(replaceEnd) + + // Add boundary crossing check to ensure no replacements span across the embed area start boundary + if replaceStartPos < embedStart && replaceEndPos > embedStart { + return "", fmt.Errorf("replacement spans across embed area boundary (replace: %d-%d, embed starts: %d)", replaceStartPos, replaceEndPos, embedStart) + } + + if replaceEndPos <= embedStart { + // Change is before embed area - affects offset + *fileEntry.Offset += fileSizeChange + } + + return newContent, nil +} + +type kargsFileEntry struct { + End *string `json:"end,omitempty"` + Offset *int64 `json:"offset,omitempty"` + Pad *string `json:"pad,omitempty"` + Path *string `json:"path,omitempty"` +} + +type kargsConfig struct { + Default string `json:"default"` + Files []kargsFileEntry `json:"files"` + Size int64 `json:"size"` +} + +// FindFileByPath searches for a file entry by its path and returns a pointer to it. +// Returns nil if no file with the specified path is found. +func (k *kargsConfig) FindFileByPath(path string) *kargsFileEntry { + if k == nil { + return nil + } + for i := range k.Files { + if k.Files[i].Path != nil && *k.Files[i].Path == path { + return &k.Files[i] + } + } return nil } -func editFile(fileName string, reString string, replacement string) error { - content, err := os.ReadFile(fileName) +// updateDefaultKargs modifies the default kernel arguments to match bootloader modifications +// and applies the embed area size change directly to config.Size +func updateDefaultKargs(config *kargsConfig, rootFSURL string) error { + originalDefault := config.Default + + // Apply the same transformations we make to bootloader configs + // For default kargs, we append at the end (using $ insertion pattern) + // Pass nil for fileEntry since we don't track offsets for the default string + var err error + config.Default, err = transformKernelArgs(config.Default, `(?P$)`, rootFSURL, nil) if err != nil { return err } - re := regexp.MustCompile(reString) - newContent := re.ReplaceAllString(string(content), replacement) + // Calculate and apply the embed area size change from the default kargs transformation + embedSizeChange := int64(len(config.Default)) - int64(len(originalDefault)) + config.Size += embedSizeChange - if err := os.WriteFile(fileName, []byte(newContent), 0600); err != nil { - return err + return nil +} + +// updateKargs reads kargs.json, applies fixes with embed area awareness, and updates kargs.json +func updateKargs(extractDir, rootFSURL string, includeNmstateRamDisk bool, arch string) error { + kargsPath := filepath.Join(extractDir, "coreos/kargs.json") + + var config *kargsConfig + + if _, err := os.Stat(kargsPath); !os.IsNotExist(err) { + kargsData, err := os.ReadFile(kargsPath) + if err != nil { + return fmt.Errorf("failed to read kargs.json: %v", err) + } + + var kargsStruct kargsConfig + if err := json.Unmarshal(kargsData, &kargsStruct); err != nil { + return fmt.Errorf("failed to unmarshal kargs.json: %v", err) + } + config = &kargsStruct + } + + // Apply bootloader config changes (without tracking size changes) + if err := fixGrubConfig(rootFSURL, extractDir, includeNmstateRamDisk, config); err != nil { + return fmt.Errorf("failed to fix grub config: %v", err) + } + + // ignore isolinux.cfg for ppc64le because it doesn't exist + if arch != "ppc64le" { + if err := fixIsolinuxConfig(rootFSURL, extractDir, includeNmstateRamDisk, config); err != nil { + return fmt.Errorf("failed to fix isolinux config: %v", err) + } + } + + if config != nil { + // Update the default kernel arguments and apply embed area size change + if err := updateDefaultKargs(config, rootFSURL); err != nil { + return fmt.Errorf("failed to update default kargs: %v", err) + } + + updatedData, err := json.MarshalIndent(*config, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal updated kargs.json: %v", err) + } + + if err := os.WriteFile(kargsPath, updatedData, 0600); err != nil { + return fmt.Errorf("failed to write updated kargs.json: %v", err) + } } return nil diff --git a/pkg/isoeditor/rhcos_test.go b/pkg/isoeditor/rhcos_test.go index db74f79e..c418fd26 100644 --- a/pkg/isoeditor/rhcos_test.go +++ b/pkg/isoeditor/rhcos_test.go @@ -1,6 +1,7 @@ package isoeditor import ( + "encoding/json" "fmt" "os" "path/filepath" @@ -95,10 +96,11 @@ var _ = Context("with test files", func() { Describe("Fix Config", func() { Context("with including nmstate disk image", func() { It("fixGrubConfig alters the kernel parameters correctly", func() { - err := fixGrubConfig(testRootFSURL, filesDir, true) + // Pass nil for kargs since we're just testing file changes + err := fixGrubConfig(testRootFSURL, filesDir, true, nil) Expect(err).ToNot(HaveOccurred()) - newLine := " linux /images/pxeboot/vmlinuz random.trust_cpu=on rd.luks.options=discard ignition.firstboot ignition.platform.id=metal 'coreos.live.rootfs_url=%s'" + newLine := " linux /images/pxeboot/vmlinuz random.trust_cpu=on rd.luks.options=discard ignition.firstboot ignition.platform.id=metal coreos.live.rootfs_url=\"%s\"" grubCfg := fmt.Sprintf(newLine, testRootFSURL) validateFileContainsLine(filepath.Join(filesDir, "EFI/redhat/grub.cfg"), grubCfg) @@ -108,10 +110,11 @@ var _ = Context("with test files", func() { }) It("fixIsolinuxConfig alters the kernel parameters correctly", func() { - err := fixIsolinuxConfig(testRootFSURL, filesDir, true) + // Pass nil for kargs since we're just testing file changes + err := fixIsolinuxConfig(testRootFSURL, filesDir, true, nil) Expect(err).ToNot(HaveOccurred()) - newLine := " append initrd=/images/pxeboot/initrd.img,/images/ignition.img,%s,%s random.trust_cpu=on rd.luks.options=discard ignition.firstboot ignition.platform.id=metal coreos.live.rootfs_url=%s" + newLine := " append initrd=/images/pxeboot/initrd.img,/images/ignition.img,%s,%s random.trust_cpu=on rd.luks.options=discard ignition.firstboot ignition.platform.id=metal coreos.live.rootfs_url=\"%s\"" isolinuxCfg := fmt.Sprintf(newLine, ramDiskImagePath, nmstateDiskImagePath, testRootFSURL) validateFileContainsLine(filepath.Join(filesDir, "isolinux/isolinux.cfg"), isolinuxCfg) }) @@ -119,10 +122,11 @@ var _ = Context("with test files", func() { Context("without including nmstate disk image", func() { It("fixGrubConfig alters the kernel parameters correctly", func() { - err := fixGrubConfig(testRootFSURL, filesDir, false) + // Pass nil for kargs since we're just testing file changes + err := fixGrubConfig(testRootFSURL, filesDir, false, nil) Expect(err).ToNot(HaveOccurred()) - newLine := " linux /images/pxeboot/vmlinuz random.trust_cpu=on rd.luks.options=discard ignition.firstboot ignition.platform.id=metal 'coreos.live.rootfs_url=%s'" + newLine := " linux /images/pxeboot/vmlinuz random.trust_cpu=on rd.luks.options=discard ignition.firstboot ignition.platform.id=metal coreos.live.rootfs_url=\"%s\"" grubCfg := fmt.Sprintf(newLine, testRootFSURL) validateFileContainsLine(filepath.Join(filesDir, "EFI/redhat/grub.cfg"), grubCfg) @@ -132,13 +136,208 @@ var _ = Context("with test files", func() { }) It("fixIsolinuxConfig alters the kernel parameters correctly", func() { - err := fixIsolinuxConfig(testRootFSURL, filesDir, false) + // Pass nil for kargs since we're just testing file changes + err := fixIsolinuxConfig(testRootFSURL, filesDir, false, nil) Expect(err).ToNot(HaveOccurred()) - newLine := " append initrd=/images/pxeboot/initrd.img,/images/ignition.img,%s random.trust_cpu=on rd.luks.options=discard ignition.firstboot ignition.platform.id=metal coreos.live.rootfs_url=%s" + newLine := " append initrd=/images/pxeboot/initrd.img,/images/ignition.img,%s random.trust_cpu=on rd.luks.options=discard ignition.firstboot ignition.platform.id=metal coreos.live.rootfs_url=\"%s\"" isolinuxCfg := fmt.Sprintf(newLine, ramDiskImagePath, testRootFSURL) validateFileContainsLine(filepath.Join(filesDir, "isolinux/isolinux.cfg"), isolinuxCfg) }) }) + + Context("URL validation", func() { + It("rejects URLs containing $ character", func() { + invalidURL := "https://example.com/test$invalid/rootfs.img" + err := fixGrubConfig(invalidURL, filesDir, false, nil) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("invalid rootfs URL: contains invalid character '$'")) + + err = fixIsolinuxConfig(invalidURL, filesDir, false, nil) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("invalid rootfs URL: contains invalid character '$'")) + }) + + It("rejects URLs containing \\ character", func() { + invalidURL := "https://example.com/test\\invalid/rootfs.img" + err := fixGrubConfig(invalidURL, filesDir, false, nil) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("invalid rootfs URL: contains invalid character '\\'")) + + err = fixIsolinuxConfig(invalidURL, filesDir, false, nil) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("invalid rootfs URL: contains invalid character '\\'")) + }) + + It("accepts valid URLs", func() { + validURL := "https://example.com/valid/rootfs.img" + err := fixGrubConfig(validURL, filesDir, false, nil) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("editString function", func() { + It("replaces content in named capture group", func() { + content := "line1\nline2\nline3" + newContent, err := editString(content, `(?Pline2)`, "modified", nil) + Expect(err).ToNot(HaveOccurred()) + Expect(newContent).To(Equal("line1\nmodified\nline3")) + }) + + It("appends content when replace group is at end", func() { + content := "some text" + newContent, err := editString(content, `(some text)(?P$)`, " more", nil) + Expect(err).ToNot(HaveOccurred()) + Expect(newContent).To(Equal("some text more")) + }) + + It("returns error if no replace group", func() { + content := "some text" + _, err := editString(content, `some text`, "replacement", nil) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("must have a named capture group called 'replace'")) + }) + + It("returns original content if no match", func() { + content := "some text" + newContent, err := editString(content, `(?Pnomatch)`, "replacement", nil) + Expect(err).ToNot(HaveOccurred()) + Expect(newContent).To(Equal(content)) + }) + }) + + Context("editString offset tracking", func() { + It("updates offset when replacement is before embed area", func() { + content := "prefix some text suffix" + offset := int64(13) // Points to "suffix" + fileEntry := &kargsFileEntry{Offset: &offset} + + // Replace "some" (before offset) with "LONGER" + newContent, err := editString(content, `(?Psome)`, "LONGER", fileEntry) + Expect(err).ToNot(HaveOccurred()) + Expect(newContent).To(Equal("prefix LONGER text suffix")) + // Offset should increase by 2 (6 - 4 = 2) + Expect(*fileEntry.Offset).To(Equal(int64(15))) + }) + + It("does not update offset when replacement is after embed area", func() { + content := "prefix some text suffix" + offset := int64(7) // Points to "some" + fileEntry := &kargsFileEntry{Offset: &offset} + + // Replace "suffix" (after offset) with "END" + newContent, err := editString(content, `(?Psuffix)`, "END", fileEntry) + Expect(err).ToNot(HaveOccurred()) + Expect(newContent).To(Equal("prefix some text END")) + // Offset should not change + Expect(*fileEntry.Offset).To(Equal(int64(7))) + }) + + It("updates offset when shrinking content before embed area", func() { + content := "prefix LONGER text suffix" + offset := int64(14) // Points to "text" + fileEntry := &kargsFileEntry{Offset: &offset} + + // Replace "LONGER" with "some" + newContent, err := editString(content, `(?PLONGER)`, "some", fileEntry) + Expect(err).ToNot(HaveOccurred()) + Expect(newContent).To(Equal("prefix some text suffix")) + // Offset should decrease by 2 (4 - 6 = -2) + Expect(*fileEntry.Offset).To(Equal(int64(12))) + }) + + It("returns error when replacement spans embed area boundary", func() { + content := "prefix some text suffix" + offset := int64(10) // Points to middle of "some text" + fileEntry := &kargsFileEntry{Offset: &offset} + + // Try to replace "some text" which spans across the offset + _, err := editString(content, `(?Psome text)`, "REPLACEMENT", fileEntry) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("replacement spans across embed area boundary")) + }) + + It("handles no change gracefully", func() { + content := "some text" + offset := int64(5) + fileEntry := &kargsFileEntry{Offset: &offset} + + // Replace with same content + newContent, err := editString(content, `(?Psome)`, "some", fileEntry) + Expect(err).ToNot(HaveOccurred()) + Expect(newContent).To(Equal(content)) + // Offset should not change + Expect(*fileEntry.Offset).To(Equal(int64(5))) + }) + }) + + Context("kargs.json handling", func() { + It("successfully round-trips kargs.json", func() { + // Create a temporary directory with a kargs.json file + tmpDir, err := os.MkdirTemp("", "kargs-test") + Expect(err).ToNot(HaveOccurred()) + defer os.RemoveAll(tmpDir) + + kargsDir := filepath.Join(tmpDir, "coreos") + err = os.MkdirAll(kargsDir, 0755) + Expect(err).ToNot(HaveOccurred()) + + // Create EFI/fedora and isolinux directories + err = os.MkdirAll(filepath.Join(tmpDir, "EFI/fedora"), 0755) + Expect(err).ToNot(HaveOccurred()) + err = os.MkdirAll(filepath.Join(tmpDir, "isolinux"), 0755) + Expect(err).ToNot(HaveOccurred()) + + // Create grub.cfg + grubConfig := "\tlinux /images/pxeboot/vmlinuz random.trust_cpu=on rd.luks.options=discard ignition.firstboot ignition.platform.id=metal coreos.liveiso=rhcos-416.94.202404301731-0\n\tinitrd /images/pxeboot/initrd.img /images/ignition.img\n" + err = os.WriteFile(filepath.Join(tmpDir, "EFI/fedora/grub.cfg"), []byte(grubConfig), 0600) + Expect(err).ToNot(HaveOccurred()) + + // Create isolinux.cfg + isolinuxConfig := " append initrd=/images/pxeboot/initrd.img,/images/ignition.img random.trust_cpu=on rd.luks.options=discard ignition.firstboot ignition.platform.id=metal coreos.liveiso=rhcos-416.94.202404301731-0\n" + err = os.WriteFile(filepath.Join(tmpDir, "isolinux/isolinux.cfg"), []byte(isolinuxConfig), 0600) + Expect(err).ToNot(HaveOccurred()) + + kargsPath := filepath.Join(kargsDir, "kargs.json") + originalJSON := `{ + "default": "random.trust_cpu=on rd.luks.options=discard ignition.firstboot ignition.platform.id=metal coreos.liveiso=rhcos-416.94.202404301731-0", + "files": [ + { + "path": "EFI/fedora/grub.cfg", + "offset": 1000, + "size": 2000 + } + ], + "size": 1024 +}` + err = os.WriteFile(kargsPath, []byte(originalJSON), 0600) + Expect(err).ToNot(HaveOccurred()) + + // Call updateKargs with a rootFS URL (no nmstate, x86_64 arch) + err = updateKargs(tmpDir, testRootFSURL, false, "x86_64") + Expect(err).ToNot(HaveOccurred()) + + // Read the file back and verify it's valid JSON + updatedData, err := os.ReadFile(kargsPath) + Expect(err).ToNot(HaveOccurred()) + + var config kargsConfig + err = json.Unmarshal(updatedData, &config) + Expect(err).ToNot(HaveOccurred()) + + // Verify the default kargs were updated: + // - coreos.liveiso parameter should be removed + // - coreos.live.rootfs_url should be added + Expect(config.Default).To(ContainSubstring("coreos.live.rootfs_url")) + Expect(config.Default).To(ContainSubstring(testRootFSURL)) + Expect(config.Default).ToNot(ContainSubstring("coreos.liveiso")) + + // Verify size was updated to reflect the change in default kargs length + // Removed: "coreos.liveiso=rhcos-416.94.202404301731-0 " (43 chars) + // Added: " coreos.live.rootfs_url=\"https://example.com/pub/openshift-v4/dependencies/rhcos/4.7/4.7.7/rhcos-live-rootfs.x86_64.img\"" (121 chars) + // Net change: 121 - 43 = 78 chars + Expect(config.Size).To(Equal(int64(1024 + 78))) + }) + }) }) })