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  }