github.com/openshift/installer@v1.4.17/pkg/asset/agent/image/releaseextract.go (about) 1 package image 2 3 import ( 4 "bytes" 5 "context" 6 "crypto/sha256" 7 "encoding/hex" 8 "encoding/json" 9 "fmt" 10 "io" 11 "os" 12 "os/exec" 13 "path" 14 "path/filepath" 15 "strings" 16 "time" 17 18 "github.com/coreos/stream-metadata-go/arch" 19 "github.com/coreos/stream-metadata-go/stream" 20 "github.com/pkg/errors" 21 "github.com/sirupsen/logrus" 22 "github.com/thedevsaddam/retry" 23 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 "sigs.k8s.io/yaml" 25 26 operatorv1alpha1 "github.com/openshift/api/operator/v1alpha1" 27 "github.com/openshift/installer/pkg/asset/agent" 28 "github.com/openshift/installer/pkg/asset/agent/mirror" 29 "github.com/openshift/installer/pkg/rhcos/cache" 30 ) 31 32 const ( 33 machineOsImageName = "machine-os-images" 34 coreOsFileName = "/coreos/coreos-%s.iso" 35 coreOsSha256FileName = "/coreos/coreos-%s.iso.sha256" 36 coreOsStreamFileName = "/coreos/coreos-stream.json" 37 // OcDefaultTries is the number of times to execute the oc command on failures. 38 OcDefaultTries = 5 39 // OcDefaultRetryDelay is the time between retries. 40 OcDefaultRetryDelay = time.Second * 5 41 ) 42 43 // Config is used to set up the retries for extracting the base ISO. 44 type Config struct { 45 MaxTries uint 46 RetryDelay time.Duration 47 } 48 49 // Release is the interface to use the oc command to the get image info. 50 type Release interface { 51 GetBaseIso(architecture string) (string, error) 52 GetBaseIsoVersion(architecture string) (string, error) 53 ExtractFile(image string, filename string, architecture string) ([]string, error) 54 } 55 56 type release struct { 57 config Config 58 releaseImage string 59 pullSecret string 60 mirrorConfig []mirror.RegistriesConfig 61 streamGetter CoreOSBuildFetcher 62 } 63 64 // NewRelease is used to set up the executor to run oc commands. 65 func NewRelease(config Config, releaseImage string, pullSecret string, mirrorConfig []mirror.RegistriesConfig, streamGetter CoreOSBuildFetcher) Release { 66 return &release{ 67 config: config, 68 releaseImage: releaseImage, 69 pullSecret: pullSecret, 70 mirrorConfig: mirrorConfig, 71 streamGetter: streamGetter, 72 } 73 } 74 75 // ExtractFile extracts the specified file from the given image name, and store it in the cache dir. 76 func (r *release) ExtractFile(image string, filename string, architecture string) ([]string, error) { 77 imagePullSpec, err := r.getImageFromRelease(image, architecture) 78 if err != nil { 79 return nil, err 80 } 81 82 cacheDir, err := cache.GetCacheDir(cache.FilesDataType, cache.AgentApplicationName) 83 if err != nil { 84 return nil, err 85 } 86 87 path, err := r.extractFileFromImage(imagePullSpec, filename, cacheDir, architecture) 88 if err != nil { 89 return nil, err 90 } 91 return path, err 92 } 93 94 // Get the CoreOS ISO from the releaseImage. 95 func (r *release) GetBaseIso(architecture string) (string, error) { 96 // Get the machine-os-images pullspec from the release and use that to get the CoreOS ISO 97 image, err := r.getImageFromRelease(machineOsImageName, architecture) 98 if err != nil { 99 return "", err 100 } 101 102 cacheDir, err := cache.GetCacheDir(cache.ImageDataType, cache.AgentApplicationName) 103 if err != nil { 104 return "", err 105 } 106 107 filename := fmt.Sprintf(coreOsFileName, architecture) 108 // Check if file is already cached 109 cachedFile, err := cache.GetFileFromCache(path.Base(filename), cacheDir) 110 if err != nil { 111 return "", err 112 } 113 if cachedFile != "" { 114 logrus.Info("Verifying cached file") 115 valid, err := r.verifyCacheFile(image, cachedFile, architecture) 116 if err != nil { 117 return "", err 118 } 119 if valid { 120 logrus.Infof("Using cached Base ISO %s", cachedFile) 121 return cachedFile, nil 122 } 123 } 124 125 // Get the base ISO from the payload 126 path, err := r.extractFileFromImage(image, filename, cacheDir, architecture) 127 if err != nil { 128 return "", err 129 } 130 logrus.Infof("Base ISO obtained from release and cached at %s", path) 131 return path[0], err 132 } 133 134 func (r *release) GetBaseIsoVersion(architecture string) (string, error) { 135 files, err := r.ExtractFile(machineOsImageName, coreOsStreamFileName, architecture) 136 if err != nil { 137 return "", err 138 } 139 140 if len(files) > 1 { 141 return "", fmt.Errorf("too many files found for %s", coreOsStreamFileName) 142 } 143 144 rawData, err := os.ReadFile(files[0]) 145 if err != nil { 146 return "", err 147 } 148 149 var st stream.Stream 150 if err := json.Unmarshal(rawData, &st); err != nil { 151 return "", errors.Wrap(err, "failed to parse CoreOS stream metadata") 152 } 153 154 streamArch, err := st.GetArchitecture(architecture) 155 if err != nil { 156 return "", err 157 } 158 159 if metal, ok := streamArch.Artifacts["metal"]; ok { 160 return metal.Release, nil 161 } 162 163 return "", errors.New("unable to determine CoreOS release version") 164 } 165 166 func (r *release) getImageFromRelease(imageName string, architecture string) (string, error) { 167 // This requires the 'oc' command so make sure its available 168 _, err := exec.LookPath("oc") 169 if err != nil { 170 if len(r.mirrorConfig) > 0 { 171 logrus.Warning("Unable to validate mirror config because \"oc\" command is not available") 172 } else { 173 logrus.Debug("Skipping ISO extraction; \"oc\" command is not available") 174 } 175 return "", err 176 } 177 178 archName := arch.GoArch(architecture) 179 imagefor := "--image-for=" + imageName 180 filterbyos := "--filter-by-os=linux/" + archName 181 insecure := "--insecure=true" 182 183 var cmd = []string{ 184 "oc", 185 "adm", 186 "release", 187 "info", 188 imagefor, 189 filterbyos, 190 insecure, 191 } 192 if len(r.mirrorConfig) > 0 { 193 logrus.Debugf("Using mirror configuration") 194 icspFile, err := getIcspFileFromRegistriesConfig(r.mirrorConfig) 195 if err != nil { 196 return "", err 197 } 198 defer removeIcspFile(icspFile) 199 icspfile := "--icsp-file=" + icspFile 200 cmd = append(cmd, icspfile) 201 } 202 cmd = append(cmd, r.releaseImage) 203 logrus.Debugf("Fetching image from OCP release (%s)", cmd) 204 image, err := agent.ExecuteOC(r.pullSecret, cmd) 205 if err != nil { 206 if strings.Contains(err.Error(), "unknown flag: --icsp-file") { 207 logrus.Warning("Using older version of \"oc\" that does not support mirroring") 208 } 209 return "", err 210 } 211 212 return image, nil 213 } 214 215 func (r *release) extractFileFromImage(image, file, cacheDir string, architecture string) ([]string, error) { 216 archName := arch.GoArch(architecture) 217 extractpath := "--path=" + file + ":" + cacheDir 218 filterbyos := "--filter-by-os=linux/" + archName 219 220 var cmd = []string{ 221 "oc", 222 "image", 223 "extract", 224 extractpath, 225 filterbyos, 226 "--confirm", 227 } 228 229 if len(r.mirrorConfig) > 0 { 230 icspFile, err := getIcspFileFromRegistriesConfig(r.mirrorConfig) 231 if err != nil { 232 return nil, err 233 } 234 defer removeIcspFile(icspFile) 235 icspfile := "--icsp-file=" + icspFile 236 cmd = append(cmd, icspfile) 237 } 238 path := filepath.Join(cacheDir, path.Base(file)) 239 // Remove file if it exists 240 if err := removeCacheFile(path); err != nil { 241 return nil, err 242 } 243 cmd = append(cmd, image) 244 logrus.Debugf("extracting %s to %s, %s", file, cacheDir, cmd) 245 _, err := retry.Do(r.config.MaxTries, r.config.RetryDelay, agent.ExecuteOC, r.pullSecret, cmd) 246 if err != nil { 247 return nil, err 248 } 249 250 // Make sure file(s) exist after extraction 251 matches, err := filepath.Glob(path) 252 if err != nil { 253 return nil, err 254 } 255 if matches == nil { 256 return nil, fmt.Errorf("file %s was not found", file) 257 } 258 259 return matches, nil 260 } 261 262 // Get hash from rhcos.json. 263 func (r *release) getHashFromInstaller(architecture string) (bool, string) { 264 // Get hash from metadata in the installer 265 ctx, cancel := context.WithTimeout(context.TODO(), 30*time.Second) 266 defer cancel() 267 268 st, err := r.streamGetter(ctx) 269 if err != nil { 270 return false, "" 271 } 272 273 streamArch, err := st.GetArchitecture(architecture) 274 if err != nil { 275 return false, "" 276 } 277 if artifacts, ok := streamArch.Artifacts["metal"]; ok { 278 if format, ok := artifacts.Formats["iso"]; ok { 279 return true, format.Disk.Sha256 280 } 281 } 282 283 return false, "" 284 } 285 286 func matchingHash(imageSha []byte, sha string) bool { 287 decoded, err := hex.DecodeString(sha) 288 if err == nil && bytes.Equal(imageSha, decoded) { 289 return true 290 } 291 292 return false 293 } 294 295 // Check if there is a different base ISO in the release payload. 296 func (r *release) verifyCacheFile(image, file, architecture string) (bool, error) { 297 // Get hash of cached file 298 f, err := os.Open(file) 299 if err != nil { 300 return false, err 301 } 302 defer f.Close() 303 304 h := sha256.New() 305 if _, err := io.Copy(h, f); err != nil { 306 return false, err 307 } 308 fileSha := h.Sum(nil) 309 310 // Check if the hash of cached file matches hash in rhcos.json 311 found, rhcosSha := r.getHashFromInstaller(architecture) 312 if found && matchingHash(fileSha, rhcosSha) { 313 logrus.Debug("Found matching hash in installer metadata") 314 return true, nil 315 } 316 317 // If no match, get the file containing the coreos sha256 and compare that 318 tempDir, err := os.MkdirTemp("", "cache") 319 if err != nil { 320 return false, err 321 } 322 323 defer os.RemoveAll(tempDir) 324 325 shaFilename := fmt.Sprintf(coreOsSha256FileName, architecture) 326 shaFile, err := r.extractFileFromImage(image, shaFilename, tempDir, architecture) 327 if err != nil { 328 logrus.Debug("Could not get SHA from payload for cache comparison") 329 return false, nil 330 } 331 332 payloadSha, err := os.ReadFile(shaFile[0]) 333 if err != nil { 334 return false, err 335 } 336 if matchingHash(fileSha, string(payloadSha)) { 337 logrus.Debugf("Found matching hash in %s", shaFilename) 338 return true, nil 339 } 340 341 logrus.Debugf("Cached file %s is not most recent", file) 342 return false, nil 343 } 344 345 // Remove any existing files in the cache. 346 func removeCacheFile(path string) error { 347 matches, err := filepath.Glob(path) 348 if err != nil { 349 return err 350 } 351 352 for _, file := range matches { 353 if err = os.Remove(file); err != nil { 354 return err 355 } 356 logrus.Debugf("Removed file %s", file) 357 } 358 return nil 359 } 360 361 // Create a temporary file containing the ImageContentPolicySources. 362 func getIcspFileFromRegistriesConfig(mirrorConfig []mirror.RegistriesConfig) (string, error) { 363 contents, err := getIcspContents(mirrorConfig) 364 if err != nil { 365 return "", err 366 } 367 if contents == nil { 368 logrus.Debugf("No registry entries to build ICSP file") 369 return "", nil 370 } 371 372 icspFile, err := os.CreateTemp("", "icsp-file") 373 if err != nil { 374 return "", err 375 } 376 377 if _, err := icspFile.Write(contents); err != nil { 378 icspFile.Close() 379 os.Remove(icspFile.Name()) 380 return "", err 381 } 382 icspFile.Close() 383 384 return icspFile.Name(), nil 385 } 386 387 // Convert the data in registries.conf into ICSP format. 388 func getIcspContents(mirrorConfig []mirror.RegistriesConfig) ([]byte, error) { 389 icsp := operatorv1alpha1.ImageContentSourcePolicy{ 390 TypeMeta: metav1.TypeMeta{ 391 APIVersion: operatorv1alpha1.SchemeGroupVersion.String(), 392 Kind: "ImageContentSourcePolicy", 393 }, 394 ObjectMeta: metav1.ObjectMeta{ 395 Name: "image-policy", 396 // not namespaced 397 }, 398 } 399 400 icsp.Spec.RepositoryDigestMirrors = make([]operatorv1alpha1.RepositoryDigestMirrors, len(mirrorConfig)) 401 for i, mirrorRegistries := range mirrorConfig { 402 icsp.Spec.RepositoryDigestMirrors[i] = operatorv1alpha1.RepositoryDigestMirrors{Source: mirrorRegistries.Location, Mirrors: []string{mirrorRegistries.Mirror}} 403 } 404 405 // Convert to json first so json tags are handled 406 jsonData, err := json.Marshal(&icsp) 407 if err != nil { 408 return nil, err 409 } 410 contents, err := yaml.JSONToYAML(jsonData) 411 if err != nil { 412 return nil, err 413 } 414 415 return contents, nil 416 } 417 418 func removeIcspFile(filename string) { 419 if filename != "" { 420 os.Remove(filename) 421 } 422 }