cache_images.go 13.3 KB
Newer Older
M
Matt Rickard 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
/*
Copyright 2016 The Kubernetes Authors All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package machine

import (
20
	"context"
21
	"fmt"
M
Matt Rickard 已提交
22 23
	"io/ioutil"
	"os"
H
Hiroshi Nomura 已提交
24
	"os/exec"
25
	"path"
M
Matt Rickard 已提交
26
	"path/filepath"
27
	"runtime"
M
Matt Rickard 已提交
28
	"strings"
29
	"sync"
T
tstromberg 已提交
30
	"time"
M
Matt Rickard 已提交
31

32
	"github.com/docker/docker/client"
33
	"github.com/docker/machine/libmachine/state"
34
	"github.com/golang/glog"
35 36
	"github.com/google/go-containerregistry/pkg/authn"
	"github.com/google/go-containerregistry/pkg/name"
37 38
	v1 "github.com/google/go-containerregistry/pkg/v1"
	"github.com/google/go-containerregistry/pkg/v1/daemon"
39
	"github.com/google/go-containerregistry/pkg/v1/remote"
40 41
	"github.com/google/go-containerregistry/pkg/v1/tarball"
	"github.com/pkg/errors"
K
kairen 已提交
42
	"golang.org/x/sync/errgroup"
M
Matt Rickard 已提交
43 44
	"k8s.io/minikube/pkg/minikube/assets"
	"k8s.io/minikube/pkg/minikube/bootstrapper"
45
	"k8s.io/minikube/pkg/minikube/cluster"
46
	"k8s.io/minikube/pkg/minikube/command"
P
Priya Wadhwa 已提交
47
	"k8s.io/minikube/pkg/minikube/config"
M
Matt Rickard 已提交
48
	"k8s.io/minikube/pkg/minikube/constants"
49
	"k8s.io/minikube/pkg/minikube/cruntime"
50
	"k8s.io/minikube/pkg/minikube/vmpath"
M
Matt Rickard 已提交
51 52
)

53
// loadRoot is where images should be loaded from within the guest VM
54
var loadRoot = path.Join(vmpath.GuestPersistentDir, "images")
M
Matt Rickard 已提交
55

H
Hiroshi Nomura 已提交
56 57
var getWindowsVolumeName = getWindowsVolumeNameCmd

58 59
// loadImageLock is used to serialize image loads to avoid overloading the guest VM
var loadImageLock sync.Mutex
60

61
// CacheImagesForBootstrapper will cache images for a bootstrapper
62
func CacheImagesForBootstrapper(imageRepository string, version string, clusterBootstrapper string) error {
63 64 65 66
	images, err := bootstrapper.GetCachedImageList(imageRepository, version, clusterBootstrapper)
	if err != nil {
		return errors.Wrap(err, "cached images list")
	}
M
Matt Rickard 已提交
67

M
Medya Gh 已提交
68
	if err := CacheImagesToTar(images, constants.ImageCacheDir); err != nil {
M
Matt Rickard 已提交
69 70 71 72 73 74
		return errors.Wrapf(err, "Caching images for %s", clusterBootstrapper)
	}

	return nil
}

M
Medya Gh 已提交
75
// CacheImagesToTar will cache images on the host
M
Matt Rickard 已提交
76 77
//
// The cache directory currently caches images using the imagename_tag
T
Tim Hockin 已提交
78 79
// For example, k8s.gcr.io/kube-addon-manager:v6.5 would be
// stored at $CACHE_DIR/k8s.gcr.io/kube-addon-manager_v6.5
M
Medya Gh 已提交
80
func CacheImagesToTar(images []string, cacheDir string) error {
M
Matt Rickard 已提交
81 82 83 84 85 86
	var g errgroup.Group
	for _, image := range images {
		image := image
		g.Go(func() error {
			dst := filepath.Join(cacheDir, image)
			dst = sanitizeCacheDir(dst)
M
Medya Gh 已提交
87
			if err := cacheImageToTarFile(image, dst); err != nil {
T
tstromberg 已提交
88
				glog.Errorf("CacheImage %s -> %s failed: %v", image, dst, err)
M
Medya Gh 已提交
89
				return errors.Wrapf(err, "caching image %q", dst)
M
Matt Rickard 已提交
90
			}
T
tstromberg 已提交
91
			glog.Infof("CacheImage %s -> %s succeeded", image, dst)
M
Matt Rickard 已提交
92 93 94 95 96 97 98 99 100 101
			return nil
		})
	}
	if err := g.Wait(); err != nil {
		return errors.Wrap(err, "caching images")
	}
	glog.Infoln("Successfully cached all images.")
	return nil
}

102
// LoadImages loads previously cached images into the container runtime
103
func LoadImages(cc *config.MachineConfig, runner command.Runner, images []string, cacheDir string) error {
104 105
	glog.Infof("LoadImages start: %s", images)
	defer glog.Infof("LoadImages end")
M
Matt Rickard 已提交
106
	var g errgroup.Group
M
Medya Gh 已提交
107 108 109
	cr, err := cruntime.New(cruntime.Config{Type: cc.ContainerRuntime, Runner: runner})
	if err != nil {
		return errors.Wrap(err, "runtime")
110
	}
M
Medya Gh 已提交
111

112 113 114 115 116 117
	imgClient, err := client.NewEnvClient() // image client
	if err != nil {
		glog.Infof("couldn't get a local image daemon which might be ok: %v", err)
		imgClient = nil
	}

M
Matt Rickard 已提交
118 119 120
	for _, image := range images {
		image := image
		g.Go(func() error {
121
			err := needsTransfer(imgClient, image, cr)
122
			if err == nil {
M
lint  
Medya Gh 已提交
123
				return nil
M
Medya Gh 已提交
124
			}
125 126
			glog.Infof("%q needs transfer: %v", image, err)
			return transferAndLoadImage(runner, cc.KubernetesConfig, image, cacheDir)
M
Matt Rickard 已提交
127 128 129 130 131
		})
	}
	if err := g.Wait(); err != nil {
		return errors.Wrap(err, "loading cached images")
	}
132 133 134 135 136
	glog.Infoln("Successfully loaded all cached images")
	return nil
}

// needsTransfer returns an error if an image needs to be retransfered
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
func needsTransfer(imgClient *client.Client, imgName string, cr cruntime.Manager) error {
	imgHex := ""
	if imgClient != nil { // if possible try to get img digest from Client lib which is 4s faster.
		imgHex = imgDigestByDaemonClient(imgClient, imgName)
		if imgHex != "" {
			if !cr.ImageExists(imgName, imgHex) {
				return fmt.Errorf("%q does not exist at hash %q in container runtime", imgName, imgHex)
			}
			return nil
		}
	}
	// if not found with method above try go-container lib (which is 4s slower)
	imgHex = imgDigestByLib(imgName)
	if imgHex == "" {
		return fmt.Errorf("got empty img digest %q for %s", imgHex, imgName)
	}
	if !cr.ImageExists(imgName, imgHex) {
		return fmt.Errorf("%q does not exist at hash %q in container runtime", imgName, imgHex)
	}
	return nil
}

// uses client by docker lib
func imgDigestByDaemonClient(imgClient *client.Client, imgName string) string {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
	defer cancel()
	img, _, err := imgClient.ImageInspectWithRaw(ctx, imgName)
	if err != nil && !client.IsErrImageNotFound(err) {
		glog.Infof("couldn't find image digest %s from local daemon: %v ", imgName, err)
		return ""
	}
	return img.ID
}

// gets image digest sha256: uses go-containerregistry lib which is 4s slower per lookup https://github.com/google/go-containerregistry/issues/627
func imgDigestByLib(imgName string) string {
	ref, err := name.ParseReference(imgName, name.WeakValidation)
174
	if err != nil {
175
		glog.Infof("error parsing image name %s ref %v ", imgName, err)
176 177 178 179
	}

	img, err := retrieveImage(ref)
	if err != nil {
180
		glog.Infof("error retrieve Image %s ref %v ", imgName, err)
181 182 183 184
	}

	cf, err := img.ConfigName()
	if err != nil {
185 186
		glog.Infof("error getting Image config name %s %v ", imgName, err)
		return cf.Hex
187
	}
188
	return cf.Hex
M
Matt Rickard 已提交
189 190
}

191
// CacheAndLoadImages caches and loads images to all profiles
P
Priya Wadhwa 已提交
192
func CacheAndLoadImages(images []string) error {
M
Medya Gh 已提交
193
	if err := CacheImagesToTar(images, constants.ImageCacheDir); err != nil {
P
Priya Wadhwa 已提交
194
		return err
P
Priya Wadhwa 已提交
195
	}
P
Priya Wadhwa 已提交
196 197 198 199 200
	api, err := NewAPIClient()
	if err != nil {
		return err
	}
	defer api.Close()
201
	profiles, _, err := config.ListProfiles() // need to load image to all profiles
202
	if err != nil {
203
		return errors.Wrap(err, "list profiles")
204
	}
M
Medya Gh 已提交
205
	for _, p := range profiles { // loading images to all running profiles
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
		pName := p.Name // capture the loop variable
		status, err := cluster.GetHostStatus(api, pName)
		if err != nil {
			glog.Warningf("skipping loading cache for profile %s", pName)
			glog.Errorf("error getting status for %s: %v", pName, err)
			continue // try next machine
		}
		if status == state.Running.String() { // the not running hosts will load on next start
			h, err := api.Load(pName)
			if err != nil {
				return err
			}
			cr, err := CommandRunner(h)
			if err != nil {
				return err
			}
222 223 224 225 226
			c, err := config.Load(pName)
			if err != nil {
				return err
			}
			err = LoadImages(c, cr, images, constants.ImageCacheDir)
227 228 229 230
			if err != nil {
				glog.Warningf("Failed to load cached images for profile %s. make sure the profile is running. %v", pName, err)
			}
		}
P
Priya Wadhwa 已提交
231
	}
232
	return err
P
Priya Wadhwa 已提交
233 234
}

M
Matt Rickard 已提交
235 236
// # ParseReference cannot have a : in the directory path
func sanitizeCacheDir(image string) string {
H
Hiroshi Nomura 已提交
237
	if runtime.GOOS == "windows" && hasWindowsDriveLetter(image) {
238
		// not sanitize Windows drive letter.
T
tstromberg 已提交
239 240 241
		s := image[:2] + strings.Replace(image[2:], ":", "_", -1)
		glog.Infof("windows sanitize: %s -> %s", image, s)
		return s
242
	}
M
Matt Rickard 已提交
243 244 245
	return strings.Replace(image, ":", "_", -1)
}

246 247 248 249 250 251
func hasWindowsDriveLetter(s string) bool {
	if len(s) < 3 {
		return false
	}

	drive := s[:3]
M
Mark Gibbons 已提交
252
	for _, b := range "CDEFGHIJKLMNOPQRSTUVWXYZABcdefghijklmnopqrstuvwxyzab" {
253 254 255 256 257 258 259 260
		if d := string(b) + ":"; drive == d+`\` || drive == d+`/` {
			return true
		}
	}

	return false
}

H
Hiroshi Nomura 已提交
261 262 263 264 265 266 267 268 269 270 271 272
// Replace a drive letter to a volume name.
func replaceWinDriveLetterToVolumeName(s string) (string, error) {
	vname, err := getWindowsVolumeName(s[:1])
	if err != nil {
		return "", err
	}
	path := vname + s[3:]

	return path, nil
}

func getWindowsVolumeNameCmd(d string) (string, error) {
H
Hiroshi Nomura 已提交
273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
	cmd := exec.Command("wmic", "volume", "where", "DriveLetter = '"+d+":'", "get", "DeviceID")

	stdout, err := cmd.Output()
	if err != nil {
		return "", err
	}

	outs := strings.Split(strings.Replace(string(stdout), "\r", "", -1), "\n")

	var vname string
	for _, l := range outs {
		s := strings.TrimSpace(l)
		if strings.HasPrefix(s, `\\?\Volume{`) && strings.HasSuffix(s, `}\`) {
			vname = s
			break
		}
	}

	if vname == "" {
		return "", errors.New("failed to get a volume GUID")
	}

	return vname, nil
}

298
// transferAndLoadImage transfers and loads a single image from the cache
299 300 301 302 303 304 305
func transferAndLoadImage(cr command.Runner, k8s config.KubernetesConfig, imgName string, cacheDir string) error {
	r, err := cruntime.New(cruntime.Config{Type: k8s.ContainerRuntime, Runner: cr})
	if err != nil {
		return errors.Wrap(err, "runtime")
	}
	src := filepath.Join(cacheDir, imgName)
	src = sanitizeCacheDir(src)
306
	glog.Infof("Loading image from cache: %s", src)
M
Matt Rickard 已提交
307
	filename := filepath.Base(src)
308
	if _, err := os.Stat(src); err != nil {
309
		return err
M
Matt Rickard 已提交
310
	}
311 312
	dst := path.Join(loadRoot, filename)
	f, err := assets.NewFileAsset(src, loadRoot, filename, "0644")
M
Matt Rickard 已提交
313 314 315
	if err != nil {
		return errors.Wrapf(err, "creating copyable file asset: %s", filename)
	}
316
	if err := cr.Copy(f); err != nil {
M
Matt Rickard 已提交
317 318 319
		return errors.Wrap(err, "transferring cached image")
	}

320 321
	loadImageLock.Lock()
	defer loadImageLock.Unlock()
M
Matt Rickard 已提交
322

323 324 325
	err = r.LoadImage(dst)
	if err != nil {
		return errors.Wrapf(err, "%s load %s", r.Name(), dst)
326 327
	}

328
	glog.Infof("Transferred and loaded %s from cache", src)
M
Matt Rickard 已提交
329 330 331
	return nil
}

332
// DeleteFromImageCacheDir deletes images from the cache
P
Priya Wadhwa 已提交
333 334 335
func DeleteFromImageCacheDir(images []string) error {
	for _, image := range images {
		path := filepath.Join(constants.ImageCacheDir, image)
K
kairen 已提交
336
		path = sanitizeCacheDir(path)
P
Priya Wadhwa 已提交
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
		glog.Infoln("Deleting image in cache at ", path)
		if err := os.Remove(path); err != nil {
			return err
		}
	}
	return cleanImageCacheDir()
}

func cleanImageCacheDir() error {
	err := filepath.Walk(constants.ImageCacheDir, func(path string, info os.FileInfo, err error) error {
		// If error is not nil, it's because the path was already deleted and doesn't exist
		// Move on to next path
		if err != nil {
			return nil
		}
		// Check if path is directory
		if !info.IsDir() {
			return nil
		}
		// If directory is empty, delete it
		entries, err := ioutil.ReadDir(path)
		if err != nil {
			return err
		}
		if len(entries) == 0 {
			if err = os.Remove(path); err != nil {
				return err
			}
		}
		return nil
	})
	return err
}

T
tstromberg 已提交
371
func getDstPath(dst string) (string, error) {
H
Hiroshi Nomura 已提交
372
	if runtime.GOOS == "windows" && hasWindowsDriveLetter(dst) {
H
Hiroshi Nomura 已提交
373
		// ParseReference does not support a Windows drive letter.
H
Hiroshi Nomura 已提交
374 375 376
		// Therefore, will replace the drive letter to a volume name.
		var err error
		if dst, err = replaceWinDriveLetterToVolumeName(dst); err != nil {
377
			return "", errors.Wrap(err, "parsing docker archive dst ref: replace a Win drive letter to a volume name")
H
Hiroshi Nomura 已提交
378 379
		}
	}
H
Hiroshi Nomura 已提交
380

381
	return dst, nil
M
Matt Rickard 已提交
382 383
}

M
Medya Gh 已提交
384 385
// cacheImageToTarFile caches an image
func cacheImageToTarFile(image, dst string) error {
T
tstromberg 已提交
386 387 388 389 390 391
	start := time.Now()
	glog.Infof("CacheImage: %s -> %s", image, dst)
	defer func() {
		glog.Infof("CacheImage: %s -> %s completed in %s", image, dst, time.Since(start))
	}()

M
Matt Rickard 已提交
392
	if _, err := os.Stat(dst); err == nil {
T
tstromberg 已提交
393
		glog.Infof("%s exists", dst)
M
Matt Rickard 已提交
394 395 396
		return nil
	}

T
tstromberg 已提交
397
	dstPath, err := getDstPath(dst)
M
Matt Rickard 已提交
398
	if err != nil {
399
		return errors.Wrap(err, "getting destination path")
M
Matt Rickard 已提交
400 401
	}

402 403
	if err := os.MkdirAll(filepath.Dir(dstPath), 0777); err != nil {
		return errors.Wrapf(err, "making cache image directory: %s", dst)
M
Matt Rickard 已提交
404 405
	}

406
	ref, err := name.ParseReference(image, name.WeakValidation)
M
Matt Rickard 已提交
407
	if err != nil {
M
Medya Gh 已提交
408
		return errors.Wrapf(err, "parsing image ref name for %s", image)
M
Matt Rickard 已提交
409 410
	}

411
	img, err := retrieveImage(ref)
M
Matt Rickard 已提交
412
	if err != nil {
413
		glog.Warningf("unable to retrieve image: %v", err)
M
Matt Rickard 已提交
414
	}
415
	glog.Infoln("OPENING: ", dstPath)
416
	f, err := ioutil.TempFile(filepath.Dir(dstPath), filepath.Base(dstPath)+".*.tmp")
417 418 419
	if err != nil {
		return err
	}
420 421 422 423 424 425 426 427
	defer func() {
		// If we left behind a temp file, remove it.
		_, err := os.Stat(f.Name())
		if err == nil {
			os.Remove(f.Name())
			if err != nil {
				glog.Warningf("Failed to clean up the temp file %s: %v", f.Name(), err)
			}
M
Medya Gh 已提交
428 429
		}
	}()
P
Priya Wadhwa 已提交
430 431
	tag, err := name.NewTag(image, name.WeakValidation)
	if err != nil {
432
		return errors.Wrap(err, "newtag")
P
Priya Wadhwa 已提交
433 434
	}
	err = tarball.Write(tag, img, &tarball.WriteOptions{}, f)
435
	if err != nil {
436
		return errors.Wrap(err, "write")
437 438 439
	}
	err = f.Close()
	if err != nil {
440
		return errors.Wrap(err, "close")
441 442 443
	}
	err = os.Rename(f.Name(), dstPath)
	if err != nil {
444
		return errors.Wrap(err, "rename")
445
	}
T
tstromberg 已提交
446
	glog.Infof("%s exists", dst)
447
	return nil
M
Matt Rickard 已提交
448
}
449 450

func retrieveImage(ref name.Reference) (v1.Image, error) {
T
tstromberg 已提交
451
	glog.Infof("retrieving image: %+v", ref)
452 453
	img, err := daemon.Image(ref)
	if err == nil {
454 455 456 457 458 459
		glog.Infof("found %s locally: %+v", ref.Name(), img)
		return img, nil
	}
	// reference does not exist in the local daemon
	if err != nil {
		glog.Infof("daemon lookup for %+v: %v", ref, err)
460
	}
461

T
tstromberg 已提交
462
	img, err = remote.Image(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
T
tstromberg 已提交
463
	if err == nil {
464
		return img, nil
T
tstromberg 已提交
465
	}
466 467

	glog.Warningf("authn lookup for %+v (trying anon): %+v", ref, err)
T
tstromberg 已提交
468
	img, err = remote.Image(ref)
T
tstromberg 已提交
469
	return img, err
470
}