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 }