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 }