Repository: kubernetes/minikube
Pull request #6043 (2019-12-10 16:18:32)

Sync cached image list for Kubernetes v1.17
- Updates the core addon versions required by Kubernetes v1.17
- Removes unused kube-dns images
- Removes pause version logic that was required for Kubernetes pre-v1.11
- Simplifies logic to make it easier to reason about what image versions are necessary
- Returns an error
- Adds tests!



Patch:
diff --git a/cmd/minikube/cmd/start.go b/cmd/minikube/cmd/start.go
index cb1e8a0f4..d1e4fbec9 100644
--- a/cmd/minikube/cmd/start.go
+++ b/cmd/minikube/cmd/start.go
@@ -646,7 +646,7 @@ func validateDriver(name string, existing *cfg.MachineConfig) {
 	exit.WithCodeT(exit.Config, "Exiting.")
 }
 
-func selectImageRepository(mirrorCountry string, k8sVersion string) (bool, string, error) {
+func selectImageRepository(mirrorCountry string) (bool, string, error) {
 	var tryCountries []string
 	var fallback string
 	glog.Infof("selecting image repository for country %s ...", mirrorCountry)
@@ -674,7 +674,7 @@ func selectImageRepository(mirrorCountry string, k8sVersion string) (bool, strin
 	}
 
 	checkRepository := func(repo string) error {
-		pauseImage := images.PauseImage(repo, k8sVersion)
+		pauseImage := images.Pause(repo)
 		ref, err := name.ParseReference(pauseImage, name.WeakValidation)
 		if err != nil {
 			return err
@@ -867,7 +867,7 @@ func generateCfgFromFlags(cmd *cobra.Command, k8sVersion string, drvName string)
 	repository := viper.GetString(imageRepository)
 	mirrorCountry := strings.ToLower(viper.GetString(imageMirrorCountry))
 	if strings.ToLower(repository) == "auto" || mirrorCountry != "" {
-		found, autoSelectedRepository, err := selectImageRepository(mirrorCountry, k8sVersion)
+		found, autoSelectedRepository, err := selectImageRepository(mirrorCountry)
 		if err != nil {
 			exit.WithError("Failed to check main repository and mirrors for images for images", err)
 		}
@@ -1109,7 +1109,7 @@ func tryRegistry(r command.Runner) {
 
 	repo := viper.GetString(imageRepository)
 	if repo == "" {
-		repo = images.DefaultImageRepo
+		repo = images.DefaultKubernetesRepo
 	}
 
 	opts = append(opts, fmt.Sprintf("https://%s/", repo))
diff --git a/pkg/minikube/bootstrapper/bootstrapper.go b/pkg/minikube/bootstrapper/bootstrapper.go
index 24b650232..9cc29f34f 100644
--- a/pkg/minikube/bootstrapper/bootstrapper.go
+++ b/pkg/minikube/bootstrapper/bootstrapper.go
@@ -64,12 +64,11 @@ func GetCachedBinaryList(bootstrapper string) []string {
 }
 
 // GetCachedImageList returns the list of images for a version
-func GetCachedImageList(imageRepository string, version string, bootstrapper string) []string {
+func GetCachedImageList(imageRepository string, version string, bootstrapper string) ([]string, error) {
 	switch bootstrapper {
 	case BootstrapperTypeKubeadm:
-		images := images.CachedImages(imageRepository, version)
-		return images
+		return images.Kubeadm(imageRepository, version)
 	default:
-		return []string{}
+		return []string{}, nil
 	}
 }
diff --git a/pkg/minikube/bootstrapper/images/images.go b/pkg/minikube/bootstrapper/images/images.go
index b96902a21..b80e17f37 100644
--- a/pkg/minikube/bootstrapper/images/images.go
+++ b/pkg/minikube/bootstrapper/images/images.go
@@ -1,5 +1,5 @@
 /*
-Copyright 2016 The Kubernetes Authors All rights reserved.
+Copyright 2019 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.
@@ -14,188 +14,63 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+// Package images implements helpers for getting image names
 package images
 
 import (
+	"path"
 	"runtime"
-	"strings"
-
-	"github.com/blang/semver"
-	"github.com/golang/glog"
-	minikubeVersion "k8s.io/minikube/pkg/version"
-)
-
-const (
-	// DefaultImageRepo is the default repository for images
-	DefaultImageRepo = "k8s.gcr.io"
-	// DefaultMinikubeRepo is the default repository for minikube
-	DefaultMinikubeRepo = "gcr.io/k8s-minikube"
 )
 
-// getImageRepositories returns either the k8s image registry on GCR or a mirror if specified
-func getImageRepository(imageRepository string) string {
-	if imageRepository == "" {
-		imageRepository = DefaultImageRepo
-	}
-	if !strings.HasSuffix(imageRepository, "/") {
-		imageRepository += "/"
+// ArchTag returns a CPU architecture suffix for images
+func ArchTag(hasTag bool) string {
+	if runtime.GOARCH == "amd64" && !hasTag {
+		return ":"
 	}
-
-	return imageRepository
+	return "-" + runtime.GOARCH + ":"
 }
 
-// getMinikubeRepository returns either the minikube image registry on GCR or a mirror if specified
-func getMinikubeRepository(imageRepository string) string {
-	minikubeRepository := imageRepository
-	if minikubeRepository == "" {
-		minikubeRepository = DefaultMinikubeRepo
-	}
-	if !strings.HasSuffix(minikubeRepository, "/") {
-		minikubeRepository += "/"
+// Auxiliary returns images that are helpful for running minikube
+func Auxiliary(mirror string) []string {
+	return []string{
+		addonManager(mirror),
+		storageProvisioner(mirror),
+		dashboardFrontend(mirror),
+		dashboardMetrics(mirror),
 	}
-
-	return minikubeRepository
 }
 
-// CachedImages gets the images to cache for kubeadm for a version
-func CachedImages(imageRepositoryStr string, kubernetesVersionStr string) []string {
-	imageRepository := getImageRepository(imageRepositoryStr)
-	minikubeRepository := getMinikubeRepository(imageRepositoryStr)
-
-	v1_16plus := semver.MustParseRange(">=1.16.0")
-	v1_14plus := semver.MustParseRange(">=1.14.0 <1.16.0")
-	v1_13 := semver.MustParseRange(">=1.13.0 <1.14.0")
-	v1_12 := semver.MustParseRange(">=1.12.0 <1.13.0")
-	v1_11 := semver.MustParseRange(">=1.11.0 <1.12.0")
-	v1_12plus := semver.MustParseRange(">=1.12.0")
-
-	kubernetesVersion, err := semver.Make(strings.TrimPrefix(kubernetesVersionStr, minikubeVersion.VersionPrefix))
-	if err != nil {
-		glog.Errorln("Error parsing version semver: ", err)
-	}
-
-	var images []string
-	if v1_12plus(kubernetesVersion) {
-		images = append(images, []string{
-			imageRepository + "kube-proxy" + ArchTag(false) + kubernetesVersionStr,
-			imageRepository + "kube-scheduler" + ArchTag(false) + kubernetesVersionStr,
-			imageRepository + "kube-controller-manager" + ArchTag(false) + kubernetesVersionStr,
-			imageRepository + "kube-apiserver" + ArchTag(false) + kubernetesVersionStr,
-		}...)
-	} else {
-		images = append(images, []string{
-			imageRepository + "kube-proxy" + ArchTag(true) + kubernetesVersionStr,
-			imageRepository + "kube-scheduler" + ArchTag(true) + kubernetesVersionStr,
-			imageRepository + "kube-controller-manager" + ArchTag(true) + kubernetesVersionStr,
-			imageRepository + "kube-apiserver" + ArchTag(true) + kubernetesVersionStr,
-		}...)
-	}
-
-	podInfraContainerImage := PauseImage(imageRepository, kubernetesVersionStr)
-	if v1_16plus(kubernetesVersion) {
-		images = append(images, []string{
-			podInfraContainerImage,
-			imageRepository + "k8s-dns-kube-dns" + ArchTag(true) + "1.14.13",
-			imageRepository + "k8s-dns-dnsmasq-nanny" + ArchTag(true) + "1.14.13",
-			imageRepository + "k8s-dns-sidecar" + ArchTag(true) + "1.14.13",
-			imageRepository + "etcd" + ArchTag(false) + "3.3.15-0",
-			imageRepository + "coredns" + ArchTag(false) + "1.6.2",
-		}...)
-
-	} else if v1_14plus(kubernetesVersion) {
-		images = append(images, []string{
-			podInfraContainerImage,
-			imageRepository + "k8s-dns-kube-dns" + ArchTag(true) + "1.14.13",
-			imageRepository + "k8s-dns-dnsmasq-nanny" + ArchTag(true) + "1.14.13",
-			imageRepository + "k8s-dns-sidecar" + ArchTag(true) + "1.14.13",
-			imageRepository + "etcd" + ArchTag(false) + "3.3.10",
-			imageRepository + "coredns" + ArchTag(false) + "1.3.1",
-		}...)
-
-	} else if v1_13(kubernetesVersion) {
-		images = append(images, []string{
-			podInfraContainerImage,
-			imageRepository + "k8s-dns-kube-dns" + ArchTag(true) + "1.14.8",
-			imageRepository + "k8s-dns-dnsmasq-nanny" + ArchTag(true) + "1.14.8",
-			imageRepository + "k8s-dns-sidecar" + ArchTag(true) + "1.14.8",
-			imageRepository + "etcd" + ArchTag(false) + "3.2.24",
-			imageRepository + "coredns:1.2.6",
-		}...)
-
-	} else if v1_12(kubernetesVersion) {
-		images = append(images, []string{
-			podInfraContainerImage,
-			imageRepository + "k8s-dns-kube-dns" + ArchTag(true) + "1.14.8",
-			imageRepository + "k8s-dns-dnsmasq-nanny" + ArchTag(true) + "1.14.8",
-			imageRepository + "k8s-dns-sidecar" + ArchTag(true) + "1.14.8",
-			imageRepository + "etcd" + ArchTag(false) + "3.2.24",
-			imageRepository + "coredns:1.2.2",
-		}...)
-
-	} else if v1_11(kubernetesVersion) {
-		images = append(images, []string{
-			podInfraContainerImage,
-			imageRepository + "k8s-dns-kube-dns" + ArchTag(true) + "1.14.8",
-			imageRepository + "k8s-dns-dnsmasq-nanny" + ArchTag(true) + "1.14.8",
-			imageRepository + "k8s-dns-sidecar" + ArchTag(true) + "1.14.8",
-			imageRepository + "etcd" + ArchTag(true) + "3.2.18",
-			imageRepository + "coredns:1.1.3",
-		}...)
-	}
-
-	images = append(images, []string{
-		imageRepository + "kube-addon-manager" + ArchTag(false) + "v9.0.2",
-		// This must match deploy/addons/dashboard/dashboard-dp.yaml
-		"kubernetesui/dashboard:v2.0.0-beta8",
-		minikubeRepository + "storage-provisioner" + ArchTag(false) + "v1.8.1",
-	}...)
-
-	return images
+// Pause returns the image name to pull for the pause image
+func Pause(mirror string) string {
+	// Should match `PauseVersion` in:
+	// https://github.com/kubernetes/kubernetes/blob/master/cmd/kubeadm/app/constants/constants.go
+	return path.Join(KubernetesRepo(mirror), "pause"+ArchTag(false)+"3.1")
 }
 
-// PauseImage returns the image name for pause image (for pod infra)
-func PauseImage(imageRepositoryStr string, kubernetesVersionStr string) string {
-	imageRepository := getImageRepository(imageRepositoryStr)
-
-	v1_16plus := semver.MustParseRange(">=1.16.0")
-	v1_14plus := semver.MustParseRange(">=1.14.0 <1.16.0")
-	v1_13 := semver.MustParseRange(">=1.13.0 <1.14.0")
-	v1_12 := semver.MustParseRange(">=1.12.0 <1.13.0")
-	v1_11 := semver.MustParseRange(">=1.11.0 <1.12.0")
-
-	kubernetesVersion, err := semver.Make(strings.TrimPrefix(kubernetesVersionStr, minikubeVersion.VersionPrefix))
-	if err != nil {
-		glog.Errorln("Error parsing version semver: ", err)
-	}
-
-	var podInfraContainerImage string
-	switch {
-	case v1_16plus(kubernetesVersion):
-		podInfraContainerImage = imageRepository + "pause:3.1"
-
-	case v1_14plus(kubernetesVersion):
-		podInfraContainerImage = imageRepository + "pause:3.1"
-
-	case v1_13(kubernetesVersion):
-		podInfraContainerImage = imageRepository + "pause" + ArchTag(false) + "3.1"
-
-	case v1_12(kubernetesVersion):
-		podInfraContainerImage = imageRepository + "pause:3.1"
+// storageProvisioner returns the minikube storage provisioner image
+func storageProvisioner(mirror string) string {
+	return path.Join(minikubeRepo(mirror), "storage-provisioner"+ArchTag(false)+"v1.8.1")
+}
 
-	case v1_11(kubernetesVersion):
-		podInfraContainerImage = imageRepository + "pause" + ArchTag(false) + "3.1"
+// addonManager returns the Kubernetes addon manager image
+func addonManager(mirror string) string {
+	return path.Join(KubernetesRepo(mirror), "kube-addon-manager"+ArchTag(false)+"v9.0.2")
+}
 
-	default:
-		podInfraContainerImage = imageRepository + "pause" + ArchTag(false) + "3.0"
+// dashboardFrontend returns the image used for the dashboard frontend
+func dashboardFrontend(repo string) string {
+	if repo == "" {
+		repo = "kubernetesui"
 	}
-
-	return podInfraContainerImage
+	// See 'kubernetes-dashboard' in deploy/addons/dashboard/dashboard-dp.yaml
+	return path.Join(repo, "dashboard:v2.0.0-beta8")
 }
 
-// ArchTag returns the archtag for images
-func ArchTag(hasTag bool) string {
-	if runtime.GOARCH == "amd64" && !hasTag {
-		return ":"
+// dashboardMetrics returns the image used for the dashboard metrics scraper
+func dashboardMetrics(repo string) string {
+	if repo == "" {
+		repo = "kubernetesui"
 	}
-	return "-" + runtime.GOARCH + ":"
+	// See 'dashboard-metrics-scraper' in deploy/addons/dashboard/dashboard-dp.yaml
+	return path.Join(repo, "metrics-scraper:v1.0.2")
 }
diff --git a/pkg/minikube/bootstrapper/images/kubeadm.go b/pkg/minikube/bootstrapper/images/kubeadm.go
new file mode 100644
index 000000000..7f1071a3f
--- /dev/null
+++ b/pkg/minikube/bootstrapper/images/kubeadm.go
@@ -0,0 +1,101 @@
+/*
+Copyright 2019 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 images
+
+import (
+	"fmt"
+	"path"
+	"strings"
+
+	"github.com/blang/semver"
+	"github.com/pkg/errors"
+)
+
+// Kubeadm returns a list of images necessary to bootstrap kubeadm
+func Kubeadm(mirror string, version string) ([]string, error) {
+	v, err := semver.Make(strings.TrimPrefix(version, "v"))
+	if err != nil {
+		return nil, errors.Wrap(err, "semver")
+	}
+
+	imgs := []string{
+		componentImage("kube-proxy", v, mirror),
+		componentImage("kube-scheduler", v, mirror),
+		componentImage("kube-controller-manager", v, mirror),
+		componentImage("kube-apiserver", v, mirror),
+		coreDNS(v, mirror),
+		etcd(v, mirror),
+		Pause(mirror),
+	}
+	imgs = append(imgs, Auxiliary(mirror)...)
+	return imgs, nil
+}
+
+// componentImage returns a Kubernetes component image to pull
+func componentImage(name string, v semver.Version, mirror string) string {
+	needsArchSuffix := false
+	ancient := semver.MustParseRange("<1.12.0")
+	if ancient(v) {
+		needsArchSuffix = true
+	}
+
+	return fmt.Sprintf("%sv%s", path.Join(KubernetesRepo(mirror), name+ArchTag(needsArchSuffix)), v)
+}
+
+// coreDNS returns the images used for CoreDNS
+func coreDNS(v semver.Version, mirror string) string {
+	// Should match `CoreDNSVersion` in
+	// https://github.com/kubernetes/kubernetes/blob/master/cmd/kubeadm/app/constants/constants.go
+	cv := "1.6.5"
+	switch v.Minor {
+	case 16:
+		cv = "1.6.2"
+	case 15, 14:
+		cv = "1.3.1"
+	case 13:
+		cv = "1.2.6"
+	case 12:
+		cv = "1.2.2"
+	case 11:
+		cv = "1.1.3"
+	}
+	return path.Join(KubernetesRepo(mirror), "coredns"+ArchTag(false)+cv)
+}
+
+// etcd returns the image used for etcd
+func etcd(v semver.Version, mirror string) string {
+	needsArchSuffix := false
+	ancient := semver.MustParseRange("<1.12.0")
+	if ancient(v) {
+		needsArchSuffix = true
+	}
+
+	// Should match `DefaultEtcdVersion` in:
+	// https://github.com/kubernetes/kubernetes/blob/master/cmd/kubeadm/app/constants/constants.go
+	ev := "3.4.3-0"
+	switch v.Minor {
+	case 16:
+		ev = "3.3.15-0"
+	case 14, 15:
+		ev = "3.3.10"
+	case 12, 13:
+		ev = "3.2.24"
+	case 11:
+		ev = "3.2.18"
+	}
+	return path.Join(KubernetesRepo(mirror), "etcd"+ArchTag(needsArchSuffix)+ev)
+}
diff --git a/pkg/minikube/bootstrapper/images/repo.go b/pkg/minikube/bootstrapper/images/repo.go
new file mode 100644
index 000000000..02cf44345
--- /dev/null
+++ b/pkg/minikube/bootstrapper/images/repo.go
@@ -0,0 +1,36 @@
+/*
+Copyright 2019 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 images
+
+// DefaultKubernetesRepo is the default Kubernetes repository
+const DefaultKubernetesRepo = "k8s.gcr.io"
+
+// KubernetesRepo returns the official Kubernetes repository, or an alternate
+func KubernetesRepo(mirror string) string {
+	if mirror != "" {
+		return mirror
+	}
+	return DefaultKubernetesRepo
+}
+
+// minikubeRepo returns the official minikube repository, or an alternate
+func minikubeRepo(mirror string) string {
+	if mirror != "" {
+		return mirror
+	}
+	return "gcr.io/k8s-minikube"
+}
diff --git a/pkg/minikube/bootstrapper/kubeadm/kubeadm.go b/pkg/minikube/bootstrapper/kubeadm/kubeadm.go
index cd06bab9a..cfe28ea26 100644
--- a/pkg/minikube/bootstrapper/kubeadm/kubeadm.go
+++ b/pkg/minikube/bootstrapper/kubeadm/kubeadm.go
@@ -596,7 +596,7 @@ func NewKubeletConfig(k8s config.KubernetesConfig, r cruntime.Manager) ([]byte,
 		extraOpts["node-ip"] = k8s.NodeIP
 	}
 
-	pauseImage := images.PauseImage(k8s.ImageRepository, k8s.KubernetesVersion)
+	pauseImage := images.Pause(k8s.ImageRepository)
 	if _, ok := extraOpts["pod-infra-container-image"]; !ok && k8s.ImageRepository != "" && pauseImage != "" && k8s.ContainerRuntime != remoteContainerRuntime {
 		extraOpts["pod-infra-container-image"] = pauseImage
 	}
@@ -630,7 +630,11 @@ func NewKubeletConfig(k8s config.KubernetesConfig, r cruntime.Manager) ([]byte,
 
 // UpdateCluster updates the cluster
 func (k *Bootstrapper) UpdateCluster(cfg config.MachineConfig) error {
-	images := images.CachedImages(cfg.KubernetesConfig.ImageRepository, cfg.KubernetesConfig.KubernetesVersion)
+	images, err := images.Kubeadm(cfg.KubernetesConfig.ImageRepository, cfg.KubernetesConfig.KubernetesVersion)
+	if err != nil {
+		return errors.Wrap(err, "kubeadm images")
+	}
+
 	if cfg.KubernetesConfig.ShouldLoadCachedImages {
 		if err := machine.LoadImages(&cfg, k.c, images, constants.ImageCacheDir); err != nil {
 			out.FailureT("Unable to load cached images: {{.error}}", out.V{"error": err})
diff --git a/pkg/minikube/cruntime/containerd.go b/pkg/minikube/cruntime/containerd.go
index a4dc06c61..d431ee263 100644
--- a/pkg/minikube/cruntime/containerd.go
+++ b/pkg/minikube/cruntime/containerd.go
@@ -169,13 +169,13 @@ func (r *Containerd) Available() error {
 }
 
 // generateContainerdConfig sets up /etc/containerd/config.toml
-func generateContainerdConfig(cr CommandRunner, imageRepository string, k8sVersion string) error {
+func generateContainerdConfig(cr CommandRunner, imageRepository string) error {
 	cPath := containerdConfigFile
 	t, err := template.New("containerd.config.toml").Parse(containerdConfigTemplate)
 	if err != nil {
 		return err
 	}
-	pauseImage := images.PauseImage(imageRepository, k8sVersion)
+	pauseImage := images.Pause(imageRepository)
 	opts := struct{ PodInfraContainerImage string }{PodInfraContainerImage: pauseImage}
 	var b bytes.Buffer
 	if err := t.Execute(&b, opts); err != nil {
@@ -198,7 +198,7 @@ func (r *Containerd) Enable(disOthers bool) error {
 	if err := populateCRIConfig(r.Runner, r.SocketPath()); err != nil {
 		return err
 	}
-	if err := generateContainerdConfig(r.Runner, r.ImageRepository, r.KubernetesVersion); err != nil {
+	if err := generateContainerdConfig(r.Runner, r.ImageRepository); err != nil {
 		return err
 	}
 	if err := enableIPForwarding(r.Runner); err != nil {
diff --git a/pkg/minikube/cruntime/cri.go b/pkg/minikube/cruntime/cri.go
index 173e65230..f053c7440 100644
--- a/pkg/minikube/cruntime/cri.go
+++ b/pkg/minikube/cruntime/cri.go
@@ -407,13 +407,13 @@ image-endpoint: unix://{{.Socket}}
 }
 
 // generateCRIOConfig sets up /etc/crio/crio.conf
-func generateCRIOConfig(cr CommandRunner, imageRepository string, k8sVersion string) error {
+func generateCRIOConfig(cr CommandRunner, imageRepository string) error {
 	cPath := crioConfigFile
 	t, err := template.New("crio.conf").Parse(crioConfigTemplate)
 	if err != nil {
 		return err
 	}
-	pauseImage := images.PauseImage(imageRepository, k8sVersion)
+	pauseImage := images.Pause(imageRepository)
 	opts := struct{ PodInfraContainerImage string }{PodInfraContainerImage: pauseImage}
 	var b bytes.Buffer
 	if err := t.Execute(&b, opts); err != nil {
diff --git a/pkg/minikube/cruntime/crio.go b/pkg/minikube/cruntime/crio.go
index b5a98a3fa..1b297d2e2 100644
--- a/pkg/minikube/cruntime/crio.go
+++ b/pkg/minikube/cruntime/crio.go
@@ -98,7 +98,7 @@ func (r *CRIO) Enable(disOthers bool) error {
 	if err := populateCRIConfig(r.Runner, r.SocketPath()); err != nil {
 		return err
 	}
-	if err := generateCRIOConfig(r.Runner, r.ImageRepository, r.KubernetesVersion); err != nil {
+	if err := generateCRIOConfig(r.Runner, r.ImageRepository); err != nil {
 		return err
 	}
 	if err := enableIPForwarding(r.Runner); err != nil {
diff --git a/pkg/minikube/machine/cache_images.go b/pkg/minikube/machine/cache_images.go
index ba7cb1fb7..dcfc7792a 100644
--- a/pkg/minikube/machine/cache_images.go
+++ b/pkg/minikube/machine/cache_images.go
@@ -57,7 +57,10 @@ var loadImageLock sync.Mutex
 
 // CacheImagesForBootstrapper will cache images for a bootstrapper
 func CacheImagesForBootstrapper(imageRepository string, version string, clusterBootstrapper string) error {
-	images := bootstrapper.GetCachedImageList(imageRepository, version, clusterBootstrapper)
+	images, err := bootstrapper.GetCachedImageList(imageRepository, version, clusterBootstrapper)
+	if err != nil {
+		return errors.Wrap(err, "cached images list")
+	}
 
 	if err := CacheImages(images, constants.ImageCacheDir); err != nil {
 		return errors.Wrapf(err, "Caching images for %s", clusterBootstrapper)


Test patch:
diff --git a/pkg/minikube/bootstrapper/images/images_test.go b/pkg/minikube/bootstrapper/images/images_test.go
new file mode 100644
index 000000000..31a487e4c
--- /dev/null
+++ b/pkg/minikube/bootstrapper/images/images_test.go
@@ -0,0 +1,49 @@
+/*
+Copyright 2019 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 images
+
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestAuxiliary(t *testing.T) {
+	want := []string{
+		"k8s.gcr.io/kube-addon-manager:v9.0.2",
+		"gcr.io/k8s-minikube/storage-provisioner:v1.8.1",
+		"kubernetesui/dashboard:v2.0.0-beta8",
+		"kubernetesui/metrics-scraper:v1.0.2",
+	}
+	got := Auxiliary("")
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Errorf("images mismatch (-want +got):\n%s", diff)
+	}
+}
+
+func TestAuxiliaryMirror(t *testing.T) {
+	want := []string{
+		"test.mirror/kube-addon-manager:v9.0.2",
+		"test.mirror/storage-provisioner:v1.8.1",
+		"test.mirror/dashboard:v2.0.0-beta8",
+		"test.mirror/metrics-scraper:v1.0.2",
+	}
+	got := Auxiliary("test.mirror")
+	if diff := cmp.Diff(want, got); diff != "" {
+		t.Errorf("images mismatch (-want +got):\n%s", diff)
+	}
+}
diff --git a/pkg/minikube/bootstrapper/images/kubeadm_test.go b/pkg/minikube/bootstrapper/images/kubeadm_test.go
new file mode 100644
index 000000000..d20a05bc5
--- /dev/null
+++ b/pkg/minikube/bootstrapper/images/kubeadm_test.go
@@ -0,0 +1,135 @@
+/*
+Copyright 2019 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 images
+
+import (
+	"sort"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestKubeadmImages(t *testing.T) {
+	tests := []struct {
+		version string
+		mirror  string
+		want    []string
+	}{
+		{"v1.17.0", "", []string{
+			"k8s.gcr.io/kube-proxy:v1.17.0",
+			"k8s.gcr.io/kube-scheduler:v1.17.0",
+			"k8s.gcr.io/kube-controller-manager:v1.17.0",
+			"k8s.gcr.io/kube-apiserver:v1.17.0",
+			"k8s.gcr.io/coredns:1.6.5",
+			"k8s.gcr.io/etcd:3.4.3-0",
+			"k8s.gcr.io/pause:3.1",
+			"k8s.gcr.io/kube-addon-manager:v9.0.2",
+			"gcr.io/k8s-minikube/storage-provisioner:v1.8.1",
+			"kubernetesui/dashboard:v2.0.0-beta8",
+			"kubernetesui/metrics-scraper:v1.0.2",
+		}},
+		{"v1.16.1", "mirror.k8s.io", []string{
+			"mirror.k8s.io/kube-proxy:v1.16.1",
+			"mirror.k8s.io/kube-scheduler:v1.16.1",
+			"mirror.k8s.io/kube-controller-manager:v1.16.1",
+			"mirror.k8s.io/kube-apiserver:v1.16.1",
+			"mirror.k8s.io/coredns:1.6.2",
+			"mirror.k8s.io/etcd:3.3.15-0",
+			"mirror.k8s.io/pause:3.1",
+			"mirror.k8s.io/kube-addon-manager:v9.0.2",
+			"mirror.k8s.io/storage-provisioner:v1.8.1",
+			"mirror.k8s.io/dashboard:v2.0.0-beta8",
+			"mirror.k8s.io/metrics-scraper:v1.0.2",
+		}},
+		{"v1.15.0", "", []string{
+			"k8s.gcr.io/kube-proxy:v1.15.0",
+			"k8s.gcr.io/kube-scheduler:v1.15.0",
+			"k8s.gcr.io/kube-controller-manager:v1.15.0",
+			"k8s.gcr.io/kube-apiserver:v1.15.0",
+			"k8s.gcr.io/coredns:1.3.1",
+			"k8s.gcr.io/etcd:3.3.10",
+			"k8s.gcr.io/pause:3.1",
+			"k8s.gcr.io/kube-addon-manager:v9.0.2",
+			"gcr.io/k8s-minikube/storage-provisioner:v1.8.1",
+			"kubernetesui/dashboard:v2.0.0-beta8",
+			"kubernetesui/metrics-scraper:v1.0.2",
+		}},
+		{"v1.14.0", "", []string{
+			"k8s.gcr.io/kube-proxy:v1.14.0",
+			"k8s.gcr.io/kube-scheduler:v1.14.0",
+			"k8s.gcr.io/kube-controller-manager:v1.14.0",
+			"k8s.gcr.io/kube-apiserver:v1.14.0",
+			"k8s.gcr.io/coredns:1.3.1",
+			"k8s.gcr.io/etcd:3.3.10",
+			"k8s.gcr.io/pause:3.1",
+			"k8s.gcr.io/kube-addon-manager:v9.0.2",
+			"gcr.io/k8s-minikube/storage-provisioner:v1.8.1",
+			"kubernetesui/dashboard:v2.0.0-beta8",
+			"kubernetesui/metrics-scraper:v1.0.2",
+		}},
+		{"v1.13.0", "", []string{
+			"k8s.gcr.io/kube-proxy:v1.13.0",
+			"k8s.gcr.io/kube-scheduler:v1.13.0",
+			"k8s.gcr.io/kube-controller-manager:v1.13.0",
+			"k8s.gcr.io/kube-apiserver:v1.13.0",
+			"k8s.gcr.io/coredns:1.2.6",
+			"k8s.gcr.io/etcd:3.2.24",
+			"k8s.gcr.io/pause:3.1",
+			"k8s.gcr.io/kube-addon-manager:v9.0.2",
+			"gcr.io/k8s-minikube/storage-provisioner:v1.8.1",
+			"kubernetesui/dashboard:v2.0.0-beta8",
+			"kubernetesui/metrics-scraper:v1.0.2",
+		}},
+		{"v1.12.0", "", []string{
+			"k8s.gcr.io/kube-proxy:v1.12.0",
+			"k8s.gcr.io/kube-scheduler:v1.12.0",
+			"k8s.gcr.io/kube-controller-manager:v1.12.0",
+			"k8s.gcr.io/kube-apiserver:v1.12.0",
+			"k8s.gcr.io/coredns:1.2.2",
+			"k8s.gcr.io/etcd:3.2.24",
+			"k8s.gcr.io/pause:3.1",
+			"k8s.gcr.io/kube-addon-manager:v9.0.2",
+			"gcr.io/k8s-minikube/storage-provisioner:v1.8.1",
+			"kubernetesui/dashboard:v2.0.0-beta8",
+			"kubernetesui/metrics-scraper:v1.0.2",
+		}},
+		{"v1.11.10", "", []string{
+			"k8s.gcr.io/kube-proxy-amd64:v1.11.10",
+			"k8s.gcr.io/kube-scheduler-amd64:v1.11.10",
+			"k8s.gcr.io/kube-controller-manager-amd64:v1.11.10",
+			"k8s.gcr.io/kube-apiserver-amd64:v1.11.10",
+			"k8s.gcr.io/coredns:1.1.3",
+			"k8s.gcr.io/etcd-amd64:3.2.18",
+			"k8s.gcr.io/pause:3.1",
+			"k8s.gcr.io/kube-addon-manager:v9.0.2",
+			"gcr.io/k8s-minikube/storage-provisioner:v1.8.1",
+			"kubernetesui/dashboard:v2.0.0-beta8",
+			"kubernetesui/metrics-scraper:v1.0.2",
+		}},
+	}
+	for _, tc := range tests {
+		got, err := Kubeadm(tc.mirror, tc.version)
+		if err != nil {
+			t.Fatalf("unexpected err: %v", err)
+		}
+		sort.Strings(got)
+		sort.Strings(tc.want)
+		if diff := cmp.Diff(tc.want, got); diff != "" {
+			t.Errorf("%s images mismatch (-want +got):\n%s", tc.version, diff)
+		}
+	}
+}
diff --git a/test/integration/a_serial_test.go b/test/integration/a_serial_test.go
index b71b3e877..0efa3e7e5 100644
--- a/test/integration/a_serial_test.go
+++ b/test/integration/a_serial_test.go
@@ -67,7 +67,11 @@ func TestDownloadOnly(t *testing.T) {
 					t.Errorf("%s failed: %v", args, err)
 				}
 
-				imgs := images.CachedImages("", v)
+				imgs, err := images.Kubeadm("", v)
+				if err != nil {
+					t.Errorf("kubeadm images: %v", v)
+				}
+
 				for _, img := range imgs {
 					img = strings.Replace(img, ":", "_", 1) // for example kube-scheduler:v1.15.2 --> kube-scheduler_v1.15.2
 					fp := filepath.Join(localpath.MiniPath(), "cache", "images", img)
diff --git a/test/integration/docker_test.go b/test/integration/docker_test.go
index 7acb0f9dc..6ed9f7620 100644
--- a/test/integration/docker_test.go
+++ b/test/integration/docker_test.go
@@ -38,7 +38,7 @@ func TestDockerFlags(t *testing.T) {
 	defer CleanupWithLogs(t, profile, cancel)
 
 	// Use the most verbose logging for the simplest test. If it fails, something is very wrong.
-	args := append([]string{"start", "-p", profile, "--wait=false", "--docker-env=FOO=BAR", "--docker-env=BAZ=BAT", "--docker-opt=debug", "--docker-opt=icc=true", "--alsologtostderr", "-v=5"}, StartArgs()...)
+	args := append([]string{"start", "-p", profile, "--cache-images=false", "--wait=false", "--docker-env=FOO=BAR", "--docker-env=BAZ=BAT", "--docker-opt=debug", "--docker-opt=icc=true", "--alsologtostderr", "-v=5"}, StartArgs()...)
 	rr, err := Run(t, exec.CommandContext(ctx, Target(), args...))
 	if err != nil {
 		t.Errorf("%s failed: %v", rr.Args, err)
diff --git a/test/integration/start_stop_delete_test.go b/test/integration/start_stop_delete_test.go
index 52ac7a420..8dfd44f66 100644
--- a/test/integration/start_stop_delete_test.go
+++ b/test/integration/start_stop_delete_test.go
@@ -20,15 +20,19 @@ package integration
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"os/exec"
 	"path/filepath"
+	"sort"
 	"strconv"
 	"strings"
 	"testing"
 	"time"
 
 	"github.com/docker/machine/libmachine/state"
+	"github.com/google/go-cmp/cmp"
+	"k8s.io/minikube/pkg/minikube/bootstrapper/images"
 	"k8s.io/minikube/pkg/minikube/constants"
 )
 
@@ -37,12 +41,11 @@ func TestStartStop(t *testing.T) {
 
 	t.Run("group", func(t *testing.T) {
 		tests := []struct {
-			name string
-			args []string
+			name    string
+			version string
+			args    []string
 		}{
-			{"docker", []string{
-				"--cache-images=false",
-				fmt.Sprintf("--kubernetes-version=%s", constants.OldestKubernetesVersion),
+			{"old-docker", constants.OldestKubernetesVersion, []string{
 				// default is the network created by libvirt, if we change the name minikube won't boot
 				// because the given network doesn't exist
 				"--kvm-network=default",
@@ -51,21 +54,20 @@ func TestStartStop(t *testing.T) {
 				"--keep-context=false",
 				"--container-runtime=docker",
 			}},
-			{"cni", []string{
+			{"newest-cni", constants.NewestKubernetesVersion, []string{
 				"--feature-gates",
 				"ServerSideApply=true",
 				"--network-plugin=cni",
 				"--extra-config=kubelet.network-plugin=cni",
 				"--extra-config=kubeadm.pod-network-cidr=192.168.111.111/16",
-				fmt.Sprintf("--kubernetes-version=%s", constants.NewestKubernetesVersion),
 			}},
-			{"containerd", []string{
+			{"containerd", constants.DefaultKubernetesVersion, []string{
 				"--container-runtime=containerd",
 				"--docker-opt",
 				"containerd=/var/run/containerd/containerd.sock",
 				"--apiserver-port=8444",
 			}},
-			{"crio", []string{
+			{"crio", "v1.15.0", []string{
 				"--container-runtime=crio",
 				"--disable-driver-mounts",
 				"--extra-config=kubeadm.ignore-preflight-errors=SystemVerification",
@@ -88,6 +90,7 @@ func TestStartStop(t *testing.T) {
 
 				startArgs := append([]string{"start", "-p", profile, "--alsologtostderr", "-v=3", "--wait=true"}, tc.args...)
 				startArgs = append(startArgs, StartArgs()...)
+				startArgs = append(startArgs, fmt.Sprintf("--kubernetes-version=%s", tc.version))
 				rr, err := Run(t, exec.CommandContext(ctx, Target(), startArgs...))
 				if err != nil {
 					// Fatal so that we may collect logs before stop/delete steps
@@ -144,6 +147,38 @@ func TestStartStop(t *testing.T) {
 					t.Fatalf("%s failed: %v", rr.Args, err)
 				}
 
+				// Make sure that kubeadm did not need to pull in additional images
+				rr, err = Run(t, exec.CommandContext(ctx, Target(), "ssh", "-p", profile, "sudo crictl images -o json"))
+				if err != nil {
+					t.Errorf("%s failed: %v", rr.Args, err)
+				}
+				jv := map[string][]struct {
+					Tags []string `json:"repoTags"`
+				}{}
+				err = json.Unmarshal(rr.Stdout.Bytes(), &jv)
+				if err != nil {
+					t.Errorf("images unmarshal: %v", err)
+				}
+				gotImages := []string{}
+				for _, img := range jv["images"] {
+					for _, i := range img.Tags {
+						// Ignore non-Kubernetes images
+						if !strings.Contains(i, "ingress") && !strings.Contains(i, "busybox") {
+							// Remove docker.io for naming consistency between container runtimes
+							gotImages = append(gotImages, strings.TrimPrefix(i, "docker.io/"))
+						}
+					}
+				}
+				want, err := images.Kubeadm("", tc.version)
+				if err != nil {
+					t.Errorf("kubeadm images: %v", tc.version)
+				}
+				sort.Strings(want)
+				sort.Strings(gotImages)
+				if diff := cmp.Diff(want, gotImages); diff != "" {
+					t.Errorf("%s images mismatch (-want +got):\n%s", tc.version, diff)
+				}
+
 				if strings.Contains(tc.name, "cni") {
 					t.Logf("WARNING: cni mode requires additional setup before pods can schedule :(")
 				} else if _, err := PodWait(ctx, t, profile, "default", "integration-test=busybox", 2*time.Minute); err != nil {
@@ -155,7 +190,7 @@ func TestStartStop(t *testing.T) {
 					t.Errorf("status = %q; want = %q", got, state.Running)
 				}
 
-				if !*cleanup {
+				if *cleanup {
 					// Normally handled by cleanuprofile, but not fatal there
 					rr, err = Run(t, exec.CommandContext(ctx, Target(), "delete", "-p", profile))
 					if err != nil {