cache_images.go 11.8 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
	"fmt"
M
Matt Rickard 已提交
21 22
	"io/ioutil"
	"os"
H
Hiroshi Nomura 已提交
23
	"os/exec"
24
	"path"
M
Matt Rickard 已提交
25
	"path/filepath"
26
	"runtime"
M
Matt Rickard 已提交
27
	"strings"
28
	"sync"
T
tstromberg 已提交
29
	"time"
M
Matt Rickard 已提交
30

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

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

H
Hiroshi Nomura 已提交
54 55
var getWindowsVolumeName = getWindowsVolumeNameCmd

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

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

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

	return nil
}

M
Medya Gh 已提交
73
// CacheImagesToTar will cache images on the host
M
Matt Rickard 已提交
74 75
//
// The cache directory currently caches images using the imagename_tag
T
Tim Hockin 已提交
76 77
// 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 已提交
78
func CacheImagesToTar(images []string, cacheDir string) error {
M
Matt Rickard 已提交
79 80 81 82 83 84
	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 已提交
85
			if err := cacheImageToTarFile(image, dst); err != nil {
T
tstromberg 已提交
86
				glog.Errorf("CacheImage %s -> %s failed: %v", image, dst, err)
M
Medya Gh 已提交
87
				return errors.Wrapf(err, "caching image %q", dst)
M
Matt Rickard 已提交
88
			}
T
tstromberg 已提交
89
			glog.Infof("CacheImage %s -> %s succeeded", image, dst)
M
Matt Rickard 已提交
90 91 92 93 94 95 96 97 98 99
			return nil
		})
	}
	if err := g.Wait(); err != nil {
		return errors.Wrap(err, "caching images")
	}
	glog.Infoln("Successfully cached all images.")
	return nil
}

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

M
Matt Rickard 已提交
110 111 112
	for _, image := range images {
		image := image
		g.Go(func() error {
113 114
			err := needsTransfer(image, cr)
			if err == nil {
M
lint  
Medya Gh 已提交
115
				return nil
M
Medya Gh 已提交
116
			}
117 118
			glog.Infof("%q needs transfer: %v", image, err)
			return transferAndLoadImage(runner, cc.KubernetesConfig, image, cacheDir)
M
Matt Rickard 已提交
119 120 121 122 123
		})
	}
	if err := g.Wait(); err != nil {
		return errors.Wrap(err, "loading cached images")
	}
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
	glog.Infoln("Successfully loaded all cached images")
	return nil
}

// needsTransfer returns an error if an image needs to be retransfered
func needsTransfer(image string, cr cruntime.Manager) error {
	ref, err := name.ParseReference(image, name.WeakValidation)
	if err != nil {
		return errors.Wrap(err, "parse ref")
	}

	img, err := retrieveImage(ref)
	if err != nil {
		return errors.Wrap(err, "retrieve")
	}

	cf, err := img.ConfigName()
	if err != nil {
		return errors.Wrap(err, "image hash")
	}

	if !cr.ImageExists(image, cf.Hex) {
		return fmt.Errorf("%q does not exist at hash %q in container runtime", image, cf.Hex)
	}
M
Matt Rickard 已提交
148 149 150
	return nil
}

151
// CacheAndLoadImages caches and loads images to all profiles
P
Priya Wadhwa 已提交
152
func CacheAndLoadImages(images []string) error {
M
Medya Gh 已提交
153
	if err := CacheImagesToTar(images, constants.ImageCacheDir); err != nil {
P
Priya Wadhwa 已提交
154
		return err
P
Priya Wadhwa 已提交
155
	}
P
Priya Wadhwa 已提交
156 157 158 159 160
	api, err := NewAPIClient()
	if err != nil {
		return err
	}
	defer api.Close()
161
	profiles, _, err := config.ListProfiles() // need to load image to all profiles
162
	if err != nil {
163
		return errors.Wrap(err, "list profiles")
164
	}
M
Medya Gh 已提交
165
	for _, p := range profiles { // loading images to all running profiles
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
		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
			}
182 183 184 185 186
			c, err := config.Load(pName)
			if err != nil {
				return err
			}
			err = LoadImages(c, cr, images, constants.ImageCacheDir)
187 188 189 190
			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 已提交
191
	}
192
	return err
P
Priya Wadhwa 已提交
193 194
}

M
Matt Rickard 已提交
195 196
// # ParseReference cannot have a : in the directory path
func sanitizeCacheDir(image string) string {
H
Hiroshi Nomura 已提交
197
	if runtime.GOOS == "windows" && hasWindowsDriveLetter(image) {
198
		// not sanitize Windows drive letter.
T
tstromberg 已提交
199 200 201
		s := image[:2] + strings.Replace(image[2:], ":", "_", -1)
		glog.Infof("windows sanitize: %s -> %s", image, s)
		return s
202
	}
M
Matt Rickard 已提交
203 204 205
	return strings.Replace(image, ":", "_", -1)
}

206 207 208 209 210 211
func hasWindowsDriveLetter(s string) bool {
	if len(s) < 3 {
		return false
	}

	drive := s[:3]
M
Mark Gibbons 已提交
212
	for _, b := range "CDEFGHIJKLMNOPQRSTUVWXYZABcdefghijklmnopqrstuvwxyzab" {
213 214 215 216 217 218 219 220
		if d := string(b) + ":"; drive == d+`\` || drive == d+`/` {
			return true
		}
	}

	return false
}

H
Hiroshi Nomura 已提交
221 222 223 224 225 226 227 228 229 230 231 232
// 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 已提交
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257
	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
}

258
// transferAndLoadImage transfers and loads a single image from the cache
259 260 261 262 263 264 265
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)
266
	glog.Infof("Loading image from cache: %s", src)
M
Matt Rickard 已提交
267
	filename := filepath.Base(src)
268
	if _, err := os.Stat(src); err != nil {
269
		return err
M
Matt Rickard 已提交
270
	}
271 272
	dst := path.Join(loadRoot, filename)
	f, err := assets.NewFileAsset(src, loadRoot, filename, "0644")
M
Matt Rickard 已提交
273 274 275
	if err != nil {
		return errors.Wrapf(err, "creating copyable file asset: %s", filename)
	}
276
	if err := cr.Copy(f); err != nil {
M
Matt Rickard 已提交
277 278 279
		return errors.Wrap(err, "transferring cached image")
	}

280 281
	loadImageLock.Lock()
	defer loadImageLock.Unlock()
M
Matt Rickard 已提交
282

283 284 285
	err = r.LoadImage(dst)
	if err != nil {
		return errors.Wrapf(err, "%s load %s", r.Name(), dst)
286 287
	}

288
	glog.Infof("Transferred and loaded %s from cache", src)
M
Matt Rickard 已提交
289 290 291
	return nil
}

292
// DeleteFromImageCacheDir deletes images from the cache
P
Priya Wadhwa 已提交
293 294 295
func DeleteFromImageCacheDir(images []string) error {
	for _, image := range images {
		path := filepath.Join(constants.ImageCacheDir, image)
K
kairen 已提交
296
		path = sanitizeCacheDir(path)
P
Priya Wadhwa 已提交
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
		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 已提交
331
func getDstPath(dst string) (string, error) {
H
Hiroshi Nomura 已提交
332
	if runtime.GOOS == "windows" && hasWindowsDriveLetter(dst) {
H
Hiroshi Nomura 已提交
333
		// ParseReference does not support a Windows drive letter.
H
Hiroshi Nomura 已提交
334 335 336
		// Therefore, will replace the drive letter to a volume name.
		var err error
		if dst, err = replaceWinDriveLetterToVolumeName(dst); err != nil {
337
			return "", errors.Wrap(err, "parsing docker archive dst ref: replace a Win drive letter to a volume name")
H
Hiroshi Nomura 已提交
338 339
		}
	}
H
Hiroshi Nomura 已提交
340

341
	return dst, nil
M
Matt Rickard 已提交
342 343
}

M
Medya Gh 已提交
344 345
// cacheImageToTarFile caches an image
func cacheImageToTarFile(image, dst string) error {
T
tstromberg 已提交
346 347 348 349 350 351
	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 已提交
352
	if _, err := os.Stat(dst); err == nil {
T
tstromberg 已提交
353
		glog.Infof("%s exists", dst)
M
Matt Rickard 已提交
354 355 356
		return nil
	}

T
tstromberg 已提交
357
	dstPath, err := getDstPath(dst)
M
Matt Rickard 已提交
358
	if err != nil {
359
		return errors.Wrap(err, "getting destination path")
M
Matt Rickard 已提交
360 361
	}

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

366
	ref, err := name.ParseReference(image, name.WeakValidation)
M
Matt Rickard 已提交
367
	if err != nil {
M
Medya Gh 已提交
368
		return errors.Wrapf(err, "parsing image ref name for %s", image)
M
Matt Rickard 已提交
369 370
	}

371
	img, err := retrieveImage(ref)
M
Matt Rickard 已提交
372
	if err != nil {
373
		glog.Warningf("unable to retrieve image: %v", err)
M
Matt Rickard 已提交
374
	}
375
	glog.Infoln("OPENING: ", dstPath)
376
	f, err := ioutil.TempFile(filepath.Dir(dstPath), filepath.Base(dstPath)+".*.tmp")
377 378 379
	if err != nil {
		return err
	}
380 381 382 383 384 385 386 387
	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 已提交
388 389
		}
	}()
P
Priya Wadhwa 已提交
390 391
	tag, err := name.NewTag(image, name.WeakValidation)
	if err != nil {
392
		return errors.Wrap(err, "newtag")
P
Priya Wadhwa 已提交
393 394
	}
	err = tarball.Write(tag, img, &tarball.WriteOptions{}, f)
395
	if err != nil {
396
		return errors.Wrap(err, "write")
397 398 399
	}
	err = f.Close()
	if err != nil {
400
		return errors.Wrap(err, "close")
401 402 403
	}
	err = os.Rename(f.Name(), dstPath)
	if err != nil {
404
		return errors.Wrap(err, "rename")
405
	}
T
tstromberg 已提交
406
	glog.Infof("%s exists", dst)
407
	return nil
M
Matt Rickard 已提交
408
}
409 410

func retrieveImage(ref name.Reference) (v1.Image, error) {
T
tstromberg 已提交
411
	glog.Infof("retrieving image: %+v", ref)
412 413
	img, err := daemon.Image(ref)
	if err == nil {
414 415 416 417 418 419
		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)
420
	}
421

T
tstromberg 已提交
422
	img, err = remote.Image(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
T
tstromberg 已提交
423
	if err == nil {
424
		return img, nil
T
tstromberg 已提交
425
	}
426 427

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