diff --git a/pkg/asset/appliance/appliance_liveiso.go b/pkg/asset/appliance/appliance_liveiso.go index d6e8bd33..41ca6624 100644 --- a/pkg/asset/appliance/appliance_liveiso.go +++ b/pkg/asset/appliance/appliance_liveiso.go @@ -3,9 +3,9 @@ package appliance import ( "encoding/json" "fmt" + "io" "os" "path/filepath" - "regexp" "github.com/openshift/appliance/pkg/asset/config" "github.com/openshift/appliance/pkg/asset/data" @@ -23,13 +23,8 @@ import ( ) const ( - liveIsoWorkDir = "live-iso" - liveIsoDataDir = "registry" - bootstrapImageName = "/images/bootstrap-appliance.img" - bootstrapIgnitionPath = "/usr/lib/ignition/base.d/99-bootstrap.ign" - defaultGrubConfigFilePath = "EFI/redhat/grub.cfg" - defaultIsolinuxConfigFilePath = "isolinux/isolinux.cfg" - defaultKargsConfigFilePath = "coreos/kargs.json" + liveIsoWorkDir = "live-iso" + liveIsoDataDir = "registry" ) // ApplianceLiveISO is an asset that generates the OpenShift-based appliance. @@ -65,22 +60,7 @@ func (a *ApplianceLiveISO) Generate(dependencies asset.Parents) error { return err } - // Embed ignition in ISO - coreOSConfig := coreos.CoreOSConfig{ - ApplianceConfig: applianceConfig, - EnvConfig: envConfig, - } - c := coreos.NewCoreOS(coreOSConfig) - ignitionBytes, err := json.Marshal(recoveryIgnition.Unconfigured) - if err != nil { - logrus.Errorf("Failed to marshal recovery ignition to json: %s", err.Error()) - return err - } applianceLiveIsoFile := filepath.Join(envConfig.AssetsDir, consts.ApplianceLiveIsoFileName) - if err = c.EmbedIgnition(ignitionBytes, applianceLiveIsoFile); err != nil { - logrus.Errorf("Failed to embed ignition in recovery ISO: %s", err.Error()) - return err - } // Get installer binary installerConfig := installer.InstallerConfig{ @@ -165,39 +145,56 @@ func (a *ApplianceLiveISO) buildLiveISO( ) spinner.FileToMonitor = consts.DeployIsoName - // Create bootstrap.img file - coreOSConfig := coreos.CoreOSConfig{ - ApplianceConfig: applianceConfig, - EnvConfig: envConfig, - } - c := coreos.NewCoreOS(coreOSConfig) - ignitionBytes, err := json.Marshal(recoveryIgnition.Bootstrap) + // Append bootstrap ignition to initrd using isoeditor library + sysIgnitionBytes, err := json.Marshal(recoveryIgnition.Bootstrap) if err != nil { logrus.Errorf("Failed to marshal recovery ignition to json: %s", err.Error()) return log.StopSpinner(spinner, err) } - bootstrapImagePath := filepath.Join(workDir, bootstrapImageName) - if err := c.WrapIgnition(ignitionBytes, bootstrapIgnitionPath, bootstrapImagePath); err != nil { - logrus.Errorf("Failed to create bootstrap image: %s", err.Error()) + ignitionContent := &isoeditor.IgnitionContent{ + SystemConfigs: map[string][]byte{ + "99-bootstrap.ign": sysIgnitionBytes, + }, + } + initrdReader, err := isoeditor.NewInitRamFSStreamReaderFromISO(coreosIsoPath, ignitionContent) + if err != nil { + logrus.Errorf("Failed to create initrd with bootstrap ignition: %s", err.Error()) return log.StopSpinner(spinner, err) } + defer func() { + if err := initrdReader.Close(); err != nil { + logrus.Errorf("Failed to close initrd reader: %s", err.Error()) + } + }() - // Add bootstrap.img to initrd - replacement := fmt.Sprintf("$1 $2 %s", bootstrapImageName) - grubCfgPath := filepath.Join(workDir, defaultGrubConfigFilePath) - if err := editFile(grubCfgPath, `(?m)^(\s+initrd) (.+| )+$`, replacement); err != nil { - return err + // Write the updated initrd to the extracted ISO + initrdPath := filepath.Join(workDir, "images/pxeboot/initrd.img") + initrdFile, err := os.Create(initrdPath) + if err != nil { + logrus.Errorf("Failed to create initrd file %s: %s", initrdPath, err.Error()) + return log.StopSpinner(spinner, err) } - replacement = fmt.Sprintf("${1},%s ${2}", bootstrapImageName) - isolinuxConfigFilePath := filepath.Join(workDir, defaultIsolinuxConfigFilePath) - if err := editFile(isolinuxConfigFilePath, `(?m)^(\s+append.*initrd=\S+) (.*)$`, replacement); err != nil { - return err + if _, err := io.Copy(initrdFile, initrdReader); err != nil { + if closeErr := initrdFile.Close(); closeErr != nil { + logrus.Errorf("Failed to close initrd file: %s", closeErr.Error()) + } + logrus.Errorf("Failed to write initrd file %s: %s", initrdPath, err.Error()) + return log.StopSpinner(spinner, err) + } + if err := initrdFile.Close(); err != nil { + logrus.Errorf("Failed to close initrd file: %s", err.Error()) + return log.StopSpinner(spinner, err) } - // Fix offset in kargs.json - initrdImageOffset := int64(len(bootstrapImageName) + 1) - if err := fixKargsOffset(workDir, defaultIsolinuxConfigFilePath, initrdImageOffset); err != nil { - return err + // Embed unconfigured ignition in the extracted ISO before creating the final ISO + ignitionBytes, err := json.Marshal(recoveryIgnition.Unconfigured) + if err != nil { + logrus.Errorf("Failed to marshal unconfigured ignition: %s", err.Error()) + return log.StopSpinner(spinner, err) + } + if err := coreos.WriteIgnitionToExtractedISO(ignitionBytes, coreosIsoPath, workDir); err != nil { + logrus.Errorf("Failed to write ignition to extracted ISO: %s", err.Error()) + return log.StopSpinner(spinner, err) } // Generate live ISO @@ -218,56 +215,3 @@ func (a *ApplianceLiveISO) buildLiveISO( return log.StopSpinner(spinner, nil) } - -func editFile(fileName string, reString string, replacement string) error { - content, err := os.ReadFile(fileName) - if err != nil { - return err - } - - re := regexp.MustCompile(reString) - newContent := re.ReplaceAllString(string(content), replacement) - - if err := os.WriteFile(fileName, []byte(newContent), 0600); err != nil { - return err - } - - return nil -} - -func fixKargsOffset(workDir, configPath string, offset int64) error { - kargsConfigFilePath := filepath.Join(workDir, defaultKargsConfigFilePath) - kargsData, err := os.ReadFile(kargsConfigFilePath) - if err != nil { - return err - } - - var kargsConfig struct { - Default string `json:"default"` - Files []struct { - End string `json:"end"` - Offset int64 `json:"offset"` - Pad string `json:"pad"` - Path string `json:"path"` - } `json:"files"` - Size int64 `json:"size"` - } - if err := json.Unmarshal(kargsData, &kargsConfig); err != nil { - return err - } - for i, file := range kargsConfig.Files { - if file.Path == configPath { - kargsConfig.Files[i].Offset = file.Offset + offset - } - } - - workConfigFileContent, err := json.MarshalIndent(kargsConfig, "", " ") - if err != nil { - return err - } - if err := os.WriteFile(kargsConfigFilePath, workConfigFileContent, 0600); err != nil { - return err - } - - return nil -} diff --git a/pkg/coreos/coreos.go b/pkg/coreos/coreos.go index d40d2b88..d4005133 100644 --- a/pkg/coreos/coreos.go +++ b/pkg/coreos/coreos.go @@ -1,20 +1,19 @@ package coreos import ( - "bytes" - "compress/gzip" "encoding/json" + "errors" "fmt" + "io" "os" "path/filepath" - "github.com/cavaliercoder/go-cpio" "github.com/cavaliergopher/grab/v3" "github.com/itchyny/gojq" "github.com/openshift/appliance/pkg/asset/config" "github.com/openshift/appliance/pkg/executer" "github.com/openshift/appliance/pkg/release" - "github.com/pkg/errors" + "github.com/openshift/assisted-image-service/pkg/isoeditor" "github.com/sirupsen/logrus" ) @@ -32,7 +31,6 @@ type CoreOS interface { DownloadDiskImage() (string, error) DownloadISO() (string, error) EmbedIgnition(ignition []byte, isoPath string) error - WrapIgnition(ignition []byte, ignitionPath, imagePath string) error FetchCoreOSStream() (map[string]any, error) } @@ -121,29 +119,46 @@ func (c *coreos) EmbedIgnition(ignition []byte, isoPath string) error { return err } -func (c *coreos) WrapIgnition(ignition []byte, ignitionPath, imagePath string) error { - ignitionImgFile, err := os.OpenFile(imagePath, os.O_CREATE|os.O_RDWR, 0664) - if err != nil { - return err - } - defer func() { - if err := ignitionImgFile.Close(); err != nil { - logrus.Errorf("Failed to close ignition image file: %s", err.Error()) - } - }() - - compressedCpio, err := generateCompressedCPIO(ignition, ignitionPath, 0o100_644) - if err != nil { - return err +// WriteIgnitionToExtractedISO writes ignition content to an already-extracted ISO directory. +// This should be called before isoeditor.Create() to avoid a redundant Extract/Create cycle. +func WriteIgnitionToExtractedISO(ignition []byte, isoPath string, extractedDir string) error { + // Get the ignition image with embedded ignition content + ignitionContent := &isoeditor.IgnitionContent{ + Config: ignition, } - - _, err = ignitionImgFile.Write(compressedCpio) + fileData, err := isoeditor.NewIgnitionImageReader(isoPath, ignitionContent) if err != nil { - logrus.Errorf("Failed to write ignition data into %s: %s", ignitionImgFile.Name(), err.Error()) - return err + return fmt.Errorf("failed to create ignition image: %w", err) + } + + // Write the returned files to the extracted directory + var errs []error + for _, fd := range fileData { + defer func(data io.ReadCloser, filename string) { + if err := data.Close(); err != nil { + logrus.Errorf("Failed to close data for %s: %s", filename, err.Error()) + } + }(fd.Data, fd.Filename) + + filePath := filepath.Join(extractedDir, fd.Filename) + file, err := os.Create(filePath) + if err != nil { + errs = append(errs, err) + continue + } + defer func(f *os.File) { + if err := f.Close(); err != nil { + logrus.Errorf("Failed to close file %s: %s", f.Name(), err.Error()) + } + }(file) + + _, err = io.Copy(file, fd.Data) + if err != nil { + errs = append(errs, err) + } } - return nil + return errors.Join(errs...) } func (c *coreos) FetchCoreOSStream() (map[string]any, error) { @@ -159,43 +174,8 @@ func (c *coreos) FetchCoreOSStream() (map[string]any, error) { var m map[string]any if err = json.Unmarshal(file, &m); err != nil { - return nil, errors.Wrap(err, "failed to parse CoreOS stream metadata") + return nil, fmt.Errorf("failed to parse CoreOS stream metadata: %w", err) } return m, nil } - -func generateCompressedCPIO(fileContent []byte, filePath string, mode cpio.FileMode) ([]byte, error) { - // Run gzip compression - compressedBuffer := new(bytes.Buffer) - gzipWriter := gzip.NewWriter(compressedBuffer) - // Create CPIO archive - cpioWriter := cpio.NewWriter(gzipWriter) - - if err := cpioWriter.WriteHeader(&cpio.Header{ - Name: filePath, - Mode: mode, - Size: int64(len(fileContent)), - }); err != nil { - return nil, errors.Wrap(err, "Failed to write CPIO header") - } - if _, err := cpioWriter.Write(fileContent); err != nil { - return nil, errors.Wrap(err, "Failed to write CPIO archive") - } - - if err := cpioWriter.Close(); err != nil { - return nil, errors.Wrap(err, "Failed to close CPIO archive") - } - if err := gzipWriter.Close(); err != nil { - return nil, errors.Wrap(err, "Failed to gzip ignition config") - } - - padSize := (4 - (compressedBuffer.Len() % 4)) % 4 - for i := 0; i < padSize; i++ { - if err := compressedBuffer.WriteByte(0); err != nil { - return nil, err - } - } - - return compressedBuffer.Bytes(), nil -}