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
142 changes: 43 additions & 99 deletions pkg/asset/appliance/appliance_liveiso.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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
Expand All @@ -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
}
100 changes: 40 additions & 60 deletions pkg/coreos/coreos.go
Original file line number Diff line number Diff line change
@@ -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"
)

Expand All @@ -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)
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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
}