github.com/openshift/installer@v1.4.17/pkg/asset/agent/image/agentimage.go (about)

     1  package image
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"io/fs"
     9  	"os"
    10  	"path/filepath"
    11  
    12  	"github.com/sirupsen/logrus"
    13  
    14  	"github.com/openshift/assisted-image-service/pkg/isoeditor"
    15  	hiveext "github.com/openshift/assisted-service/api/hiveextension/v1beta1"
    16  	"github.com/openshift/installer/pkg/asset"
    17  	"github.com/openshift/installer/pkg/asset/agent/joiner"
    18  	"github.com/openshift/installer/pkg/asset/agent/manifests"
    19  	"github.com/openshift/installer/pkg/asset/agent/workflow"
    20  )
    21  
    22  const (
    23  	agentISOFilename         = "agent.%s.iso"
    24  	agentAddNodesISOFilename = "node.%s.iso"
    25  	iso9660Level1ExtLen      = 3
    26  )
    27  
    28  // AgentImage is an asset that generates the bootable image used to install clusters.
    29  type AgentImage struct {
    30  	cpuArch              string
    31  	rendezvousIP         string
    32  	tmpPath              string
    33  	volumeID             string
    34  	isoPath              string
    35  	rootFSURL            string
    36  	bootArtifactsBaseURL string
    37  	platform             hiveext.PlatformType
    38  	isoFilename          string
    39  }
    40  
    41  var _ asset.WritableAsset = (*AgentImage)(nil)
    42  
    43  // Dependencies returns the assets on which the Bootstrap asset depends.
    44  func (a *AgentImage) Dependencies() []asset.Asset {
    45  	return []asset.Asset{
    46  		&workflow.AgentWorkflow{},
    47  		&joiner.ClusterInfo{},
    48  		&AgentArtifacts{},
    49  		&manifests.AgentManifests{},
    50  		&BaseIso{},
    51  	}
    52  }
    53  
    54  // Generate generates the image file for to ISO asset.
    55  func (a *AgentImage) Generate(ctx context.Context, dependencies asset.Parents) error {
    56  	agentWorkflow := &workflow.AgentWorkflow{}
    57  	clusterInfo := &joiner.ClusterInfo{}
    58  	agentArtifacts := &AgentArtifacts{}
    59  	agentManifests := &manifests.AgentManifests{}
    60  	baseIso := &BaseIso{}
    61  	dependencies.Get(agentArtifacts, agentManifests, baseIso, agentWorkflow, clusterInfo)
    62  
    63  	switch agentWorkflow.Workflow {
    64  	case workflow.AgentWorkflowTypeInstall:
    65  		a.platform = agentManifests.AgentClusterInstall.Spec.PlatformType
    66  		a.isoFilename = agentISOFilename
    67  
    68  	case workflow.AgentWorkflowTypeAddNodes:
    69  		a.platform = clusterInfo.PlatformType
    70  		a.isoFilename = agentAddNodesISOFilename
    71  
    72  	default:
    73  		return fmt.Errorf("AgentWorkflowType value not supported: %s", agentWorkflow.Workflow)
    74  	}
    75  
    76  	a.cpuArch = agentArtifacts.CPUArch
    77  	a.rendezvousIP = agentArtifacts.RendezvousIP
    78  	a.tmpPath = agentArtifacts.TmpPath
    79  	a.isoPath = agentArtifacts.ISOPath
    80  	a.bootArtifactsBaseURL = agentArtifacts.BootArtifactsBaseURL
    81  
    82  	volumeID, err := isoeditor.VolumeIdentifier(a.isoPath)
    83  	if err != nil {
    84  		return err
    85  	}
    86  	a.volumeID = volumeID
    87  
    88  	if a.platform == hiveext.ExternalPlatformType {
    89  		// when the bootArtifactsBaseURL is specified, construct the custom rootfs URL
    90  		if a.bootArtifactsBaseURL != "" {
    91  			a.rootFSURL = fmt.Sprintf("%s/%s", a.bootArtifactsBaseURL, fmt.Sprintf("agent.%s-rootfs.img", a.cpuArch))
    92  			logrus.Debugf("Using custom rootfs URL: %s", a.rootFSURL)
    93  		} else {
    94  			// Default to the URL from the RHCOS streams file
    95  			defaultRootFSURL, err := baseIso.getRootFSURL(ctx, a.cpuArch)
    96  			if err != nil {
    97  				return err
    98  			}
    99  			a.rootFSURL = defaultRootFSURL
   100  			logrus.Debugf("Using default rootfs URL: %s", a.rootFSURL)
   101  		}
   102  	}
   103  
   104  	// Update Ignition images
   105  	err = a.updateIgnitionContent(agentArtifacts)
   106  	if err != nil {
   107  		return err
   108  	}
   109  
   110  	err = a.appendKargs(agentArtifacts.Kargs)
   111  	if err != nil {
   112  		return err
   113  	}
   114  
   115  	return nil
   116  }
   117  
   118  // updateIgnitionContent updates the ignition data into the corresponding images in the ISO.
   119  func (a *AgentImage) updateIgnitionContent(agentArtifacts *AgentArtifacts) error {
   120  	ignitionc := &isoeditor.IgnitionContent{}
   121  	ignitionc.Config = agentArtifacts.IgnitionByte
   122  	fileInfo, err := isoeditor.NewIgnitionImageReader(a.isoPath, ignitionc)
   123  	if err != nil {
   124  		return err
   125  	}
   126  
   127  	return a.overwriteFileData(fileInfo)
   128  }
   129  
   130  func (a *AgentImage) overwriteFileData(fileInfo []isoeditor.FileData) error {
   131  	var errs []error
   132  	for _, fileData := range fileInfo {
   133  		defer fileData.Data.Close()
   134  
   135  		filename := filepath.Join(a.tmpPath, fileData.Filename)
   136  		file, err := os.Create(filename)
   137  		if err != nil {
   138  			errs = append(errs, err)
   139  			continue
   140  		}
   141  		defer file.Close()
   142  
   143  		_, err = io.Copy(file, fileData.Data)
   144  		if err != nil {
   145  			errs = append(errs, err)
   146  		}
   147  	}
   148  
   149  	return errors.Join(errs...)
   150  }
   151  
   152  func (a *AgentImage) appendKargs(kargs string) error {
   153  	if kargs == "" {
   154  		return nil
   155  	}
   156  
   157  	fileInfo, err := isoeditor.NewKargsReader(a.isoPath, kargs)
   158  	if err != nil {
   159  		return err
   160  	}
   161  	return a.overwriteFileData(fileInfo)
   162  }
   163  
   164  // normalizeFilesExtension scans the extracted ISO files and trims
   165  // the file extensions longer than three chars.
   166  func (a *AgentImage) normalizeFilesExtension() error {
   167  	var skipFiles = map[string]bool{
   168  		"boot.catalog": true, // Required for arm64 iso
   169  	}
   170  
   171  	return filepath.WalkDir(a.tmpPath, func(p string, d fs.DirEntry, err error) error {
   172  		if err != nil {
   173  			return err
   174  		}
   175  
   176  		if d.IsDir() {
   177  			return nil
   178  		}
   179  
   180  		ext := filepath.Ext(p)
   181  		// ext includes also the dot separator
   182  		if len(ext) > iso9660Level1ExtLen+1 {
   183  			b := filepath.Base(p)
   184  			if _, ok := skipFiles[filepath.Base(b)]; ok {
   185  				return nil
   186  			}
   187  
   188  			// Replaces file extensions longer than three chars
   189  			np := p[:len(p)-len(ext)] + ext[:iso9660Level1ExtLen+1]
   190  			err = os.Rename(p, np)
   191  
   192  			if err != nil {
   193  				return err
   194  			}
   195  		}
   196  
   197  		return nil
   198  	})
   199  }
   200  
   201  // PersistToFile writes the iso image in the assets folder
   202  func (a *AgentImage) PersistToFile(directory string) error {
   203  	defer os.RemoveAll(a.tmpPath)
   204  
   205  	// If the volumeId or tmpPath are not set then it means that either one of the AgentImage
   206  	// dependencies or the asset itself failed for some reason
   207  	if a.tmpPath == "" || a.volumeID == "" {
   208  		return errors.New("cannot generate ISO image due to configuration errors")
   209  	}
   210  
   211  	agentIsoFile := filepath.Join(directory, fmt.Sprintf(a.isoFilename, a.cpuArch))
   212  
   213  	// Remove symlink if it exists
   214  	os.Remove(agentIsoFile)
   215  
   216  	err := a.normalizeFilesExtension()
   217  	if err != nil {
   218  		return err
   219  	}
   220  
   221  	// For external platform when the bootArtifactsBaseURL is specified,
   222  	// output the rootfs file alongside the minimal ISO
   223  	if a.platform == hiveext.ExternalPlatformType {
   224  		if a.bootArtifactsBaseURL != "" {
   225  			bootArtifactsFullPath := filepath.Join(directory, bootArtifactsPath)
   226  			err := createDir(bootArtifactsFullPath)
   227  			if err != nil {
   228  				return err
   229  			}
   230  			err = extractRootFS(bootArtifactsFullPath, a.tmpPath, a.cpuArch)
   231  			if err != nil {
   232  				return err
   233  			}
   234  			logrus.Infof("RootFS file created in: %s. Upload it at %s", bootArtifactsFullPath, a.rootFSURL)
   235  		}
   236  		err = isoeditor.CreateMinimalISO(a.tmpPath, a.volumeID, a.rootFSURL, a.cpuArch, agentIsoFile)
   237  		if err != nil {
   238  			return err
   239  		}
   240  		logrus.Infof("Generated minimal ISO at %s", agentIsoFile)
   241  	} else {
   242  		// Generate full ISO
   243  		err = isoeditor.Create(agentIsoFile, a.tmpPath, a.volumeID)
   244  		if err != nil {
   245  			return err
   246  		}
   247  		logrus.Infof("Generated ISO at %s", agentIsoFile)
   248  	}
   249  
   250  	err = os.WriteFile(filepath.Join(directory, "rendezvousIP"), []byte(a.rendezvousIP), 0o644) //nolint:gosec // no sensitive info
   251  	if err != nil {
   252  		return err
   253  	}
   254  	// For external platform OCI, add CCM manifests in the openshift directory.
   255  	if a.platform == hiveext.ExternalPlatformType {
   256  		logrus.Infof("When using %s oci platform, always make sure CCM manifests were added in the %s directory.", hiveext.ExternalPlatformType, manifests.OpenshiftManifestDir())
   257  	}
   258  
   259  	return nil
   260  }
   261  
   262  // Name returns the human-friendly name of the asset.
   263  func (a *AgentImage) Name() string {
   264  	return "Agent Installer ISO"
   265  }
   266  
   267  // Load returns the ISO from disk.
   268  func (a *AgentImage) Load(f asset.FileFetcher) (bool, error) {
   269  	// The ISO will not be needed by another asset so load is noop.
   270  	// This is implemented because it is required by WritableAsset
   271  	return false, nil
   272  }
   273  
   274  // Files returns the files generated by the asset.
   275  func (a *AgentImage) Files() []*asset.File {
   276  	// Return empty array because File will never be loaded.
   277  	return []*asset.File{}
   278  }