github.com/openshift/installer@v1.4.17/pkg/rhcos/cache/cache.go (about)

     1  package cache
     2  
     3  import (
     4  	"bytes"
     5  	"compress/gzip"
     6  	"crypto/sha256"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/h2non/filetype/matchers"
    17  	"github.com/pkg/errors"
    18  	"github.com/sirupsen/logrus"
    19  	"github.com/thedevsaddam/retry"
    20  	"github.com/ulikunitz/xz"
    21  	"golang.org/x/sys/unix"
    22  )
    23  
    24  const (
    25  	// InstallerApplicationName is to use as application name by installer.
    26  	InstallerApplicationName = "openshift-installer"
    27  	// AgentApplicationName is to use as application name used by agent.
    28  	AgentApplicationName = "agent"
    29  	// ImageBasedApplicationName is to use as application name used by image-based.
    30  	ImageBasedApplicationName = "imagebased"
    31  	// ImageDataType is used by installer.
    32  	ImageDataType = "image"
    33  	// FilesDataType is used by agent.
    34  	FilesDataType = "files"
    35  )
    36  
    37  // GetFileFromCache returns path of the cached file if found, otherwise returns an empty string
    38  // or error.
    39  func GetFileFromCache(fileName string, cacheDir string) (string, error) {
    40  	filePath := filepath.Join(cacheDir, fileName)
    41  
    42  	// If the file has already been cached, return its path
    43  	_, err := os.Stat(filePath)
    44  	if err == nil {
    45  		logrus.Debugf("The file was found in cache: %v. Reusing...", filePath)
    46  		return filePath, nil
    47  	}
    48  	if !os.IsNotExist(err) {
    49  		return "", err
    50  	}
    51  
    52  	return "", nil
    53  }
    54  
    55  // GetCacheDir returns a local path of the cache, where the installer should put the data:
    56  // <user_cache_dir>/agent/<dataType>_cache
    57  // If the directory doesn't exist, it will be automatically created.
    58  func GetCacheDir(dataType, applicationName string) (string, error) {
    59  	if dataType == "" {
    60  		return "", errors.Errorf("data type can't be an empty string")
    61  	}
    62  
    63  	userCacheDir, err := os.UserCacheDir()
    64  	if err != nil {
    65  		return "", err
    66  	}
    67  
    68  	cacheDir := filepath.Join(userCacheDir, applicationName, dataType+"_cache")
    69  
    70  	_, err = os.Stat(cacheDir)
    71  	if err != nil {
    72  		if os.IsNotExist(err) {
    73  			err = os.MkdirAll(cacheDir, 0755)
    74  			if err != nil {
    75  				return "", err
    76  			}
    77  		} else {
    78  			return "", err
    79  		}
    80  	}
    81  
    82  	return cacheDir, nil
    83  }
    84  
    85  // cacheFile puts data in the cache.
    86  func cacheFile(reader io.Reader, filePath string, sha256Checksum string) (err error) {
    87  	logrus.Debugf("Unpacking file into %q...", filePath)
    88  
    89  	flockPath := fmt.Sprintf("%s.lock", filePath)
    90  	flock, err := os.Create(flockPath)
    91  	if err != nil {
    92  		return err
    93  	}
    94  	defer flock.Close()
    95  	defer func() {
    96  		err2 := os.Remove(flockPath)
    97  		if err == nil {
    98  			err = err2
    99  		}
   100  	}()
   101  
   102  	err = unix.Flock(int(flock.Fd()), unix.LOCK_EX)
   103  	if err != nil {
   104  		return err
   105  	}
   106  	defer func() {
   107  		err2 := unix.Flock(int(flock.Fd()), unix.LOCK_UN)
   108  		if err == nil {
   109  			err = err2
   110  		}
   111  	}()
   112  
   113  	_, err = os.Stat(filePath)
   114  	if err != nil && !os.IsNotExist(err) {
   115  		return nil // another cacheFile beat us to it
   116  	}
   117  
   118  	tempPath := fmt.Sprintf("%s.tmp", filePath)
   119  
   120  	// Delete the temporary file that may have been left over from previous launches.
   121  	err = os.Remove(tempPath)
   122  	if err != nil {
   123  		if !os.IsNotExist(err) {
   124  			return errors.Errorf("failed to clean up %s: %v", tempPath, err)
   125  		}
   126  	} else {
   127  		logrus.Debugf("Temporary file %v that remained after the previous launches was deleted", tempPath)
   128  	}
   129  
   130  	file, err := os.OpenFile(tempPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0444)
   131  	if err != nil {
   132  		return err
   133  	}
   134  	closed := false
   135  	defer func() {
   136  		if !closed {
   137  			file.Close()
   138  		}
   139  	}()
   140  
   141  	// Detect whether we know how to decompress the file
   142  	// See http://golang.org/pkg/net/http/#DetectContentType for why we use 512
   143  	buf := make([]byte, 512)
   144  	_, err = reader.Read(buf)
   145  	if err != nil {
   146  		return err
   147  	}
   148  
   149  	reader = io.MultiReader(bytes.NewReader(buf), reader)
   150  	switch {
   151  	case matchers.Gz(buf):
   152  		logrus.Debug("decompressing the image archive as gz")
   153  		uncompressor, err := gzip.NewReader(reader)
   154  		if err != nil {
   155  			return err
   156  		}
   157  		defer uncompressor.Close()
   158  		reader = uncompressor
   159  	case matchers.Xz(buf):
   160  		logrus.Debug("decompressing the image archive as xz")
   161  		uncompressor, err := xz.NewReader(reader)
   162  		if err != nil {
   163  			return err
   164  		}
   165  		reader = uncompressor
   166  	default:
   167  		// No need for an interposer otherwise
   168  		logrus.Debug("no known archive format detected for image, assuming no decompression necessary")
   169  	}
   170  
   171  	// Wrap the reader in TeeReader to calculate sha256 checksum on the fly
   172  	hasher := sha256.New()
   173  	if sha256Checksum != "" {
   174  		reader = io.TeeReader(reader, hasher)
   175  	}
   176  
   177  	written, err := io.Copy(file, reader)
   178  	if err != nil {
   179  		return err
   180  	}
   181  
   182  	// Let's find out how much data was written
   183  	// for future troubleshooting
   184  	logrus.Debugf("writing the RHCOS image was %d bytes", written)
   185  
   186  	err = file.Close()
   187  	if err != nil {
   188  		return err
   189  	}
   190  	closed = true
   191  
   192  	// Validate sha256 checksum
   193  	if sha256Checksum != "" {
   194  		foundChecksum := fmt.Sprintf("%x", hasher.Sum(nil))
   195  		if sha256Checksum != foundChecksum {
   196  			logrus.Error("File sha256 checksum is invalid.")
   197  			return errors.Errorf("Checksum mismatch for %s; expected=%s found=%s", filePath, sha256Checksum, foundChecksum)
   198  		}
   199  
   200  		logrus.Debug("Checksum validation is complete...")
   201  	}
   202  
   203  	return os.Rename(tempPath, filePath)
   204  }
   205  
   206  // urlWithIntegrity pairs a URL with an optional expected sha256 checksum (after decompression, if any)
   207  // If the query string contains sha256 parameter (i.e. https://example.com/data.bin?sha256=098a5a...),
   208  // then the downloaded data checksum will be compared with the provided value.
   209  type urlWithIntegrity struct {
   210  	location           url.URL
   211  	uncompressedSHA256 string
   212  }
   213  
   214  func (u *urlWithIntegrity) uncompressedName() string {
   215  	n := filepath.Base(u.location.Path)
   216  	return strings.TrimSuffix(strings.TrimSuffix(n, ".gz"), ".xz")
   217  }
   218  
   219  // download obtains a file from a given URL, puts it in the cache folder, defined by dataType parameter,
   220  // and returns the local file path.
   221  func (u *urlWithIntegrity) download(dataType, applicationName string) (string, error) {
   222  	fileName := u.uncompressedName()
   223  
   224  	cacheDir, err := GetCacheDir(dataType, applicationName)
   225  	if err != nil {
   226  		return "", err
   227  	}
   228  
   229  	filePath, err := GetFileFromCache(fileName, cacheDir)
   230  	if err != nil {
   231  		return "", err
   232  	}
   233  	if filePath != "" {
   234  		// Found cached file
   235  		return filePath, nil
   236  	}
   237  
   238  	// Send a request to get the file
   239  	err = retry.DoFunc(3, 5*time.Second, func() error {
   240  		resp, err := http.Get(u.location.String())
   241  		if err != nil {
   242  			return err
   243  		}
   244  		defer resp.Body.Close()
   245  
   246  		// Let's find the content length for future debugging
   247  		logrus.Debugf("image download content length: %d", resp.ContentLength)
   248  
   249  		// Check server response
   250  		if resp.StatusCode != http.StatusOK {
   251  			return errors.Errorf("bad status: %s", resp.Status)
   252  		}
   253  
   254  		filePath = filepath.Join(cacheDir, fileName)
   255  		return cacheFile(resp.Body, filePath, u.uncompressedSHA256)
   256  	})
   257  	if err != nil {
   258  		return "", err
   259  	}
   260  
   261  	return filePath, nil
   262  }
   263  
   264  // DownloadImageFile is a helper function that obtains an image file from a given URL,
   265  // puts it in the cache and returns the local file path.  If the file is compressed
   266  // by a known compressor, the file is uncompressed prior to being returned.
   267  func DownloadImageFile(baseURL string, applicationName string) (string, error) {
   268  	return DownloadImageFileWithSha(baseURL, applicationName, "")
   269  }
   270  
   271  // DownloadImageFileWithSha sets the sha256Checksum which is checked on download.
   272  func DownloadImageFileWithSha(baseURL string, applicationName string, sha256Checksum string) (string, error) {
   273  	logrus.Debugf("Obtaining RHCOS image file from '%v'", baseURL)
   274  
   275  	var u urlWithIntegrity
   276  	parsedURL, err := url.ParseRequestURI(baseURL)
   277  	if err != nil {
   278  		return "", err
   279  	}
   280  	q := parsedURL.Query()
   281  	if sha256Checksum != "" {
   282  		u.uncompressedSHA256 = sha256Checksum
   283  	}
   284  	if uncompressedSHA256, ok := q["sha256"]; ok {
   285  		if sha256Checksum != "" && uncompressedSHA256[0] != sha256Checksum {
   286  			return "", errors.Errorf("supplied sha256Checksum does not match URL")
   287  		}
   288  		u.uncompressedSHA256 = uncompressedSHA256[0]
   289  		q.Del("sha256")
   290  		parsedURL.RawQuery = q.Encode()
   291  	}
   292  	u.location = *parsedURL
   293  
   294  	return u.download(ImageDataType, applicationName)
   295  }