diff --git a/pkg/minikube/bootstrapper/images/images.go b/pkg/minikube/bootstrapper/images/images.go index 5000478df59e..0d3db5d14fb2 100644 --- a/pkg/minikube/bootstrapper/images/images.go +++ b/pkg/minikube/bootstrapper/images/images.go @@ -18,7 +18,11 @@ limitations under the License. package images import ( + "encoding/json" "fmt" + "io" + "k8s.io/klog/v2" + "net/http" "path" "k8s.io/minikube/pkg/minikube/constants" @@ -28,6 +32,11 @@ import ( "k8s.io/minikube/pkg/version" ) +const ( + // builds a docker v2 repository API call in the format https://k8s.gcr.io/v2/coredns/coredns/tags/list + tagURLTemplate = "https://%s/v2/%s/tags/list" +) + // Pause returns the image name to pull for a given Kubernetes version func Pause(v semver.Version, mirror string) string { // Note: changing this logic requires bumping the preload version @@ -35,8 +44,10 @@ func Pause(v semver.Version, mirror string) string { // https://github.com/kubernetes/kubernetes/blob/master/cmd/kubeadm/app/constants/constants.go // https://github.com/kubernetes/kubernetes/blob/master/cmd/kubeadm/app/constants/constants_unix.go pv := "3.6" - majorMinorVersion := fmt.Sprintf("v%d.%d", v.Major, v.Minor) imageName := "pause" + pv = findLatestTagFromRepository(fmt.Sprintf(tagURLTemplate, kubernetesRepo(mirror), imageName), pv) + majorMinorVersion := fmt.Sprintf("v%d.%d", v.Major, v.Minor) + if pVersion, ok := constants.KubeadmImages[majorMinorVersion][imageName]; ok { pv = pVersion } @@ -64,14 +75,54 @@ func componentImage(name string, v semver.Version, mirror string) string { return fmt.Sprintf("%s:v%s", path.Join(kubernetesRepo(mirror), name), v) } +// fixes 13136 by getting the latest image version from the k8s.gcr.io repository instead of hardcoded +func findLatestTagFromRepository(url string, lastKnownGood string) string { + client := &http.Client{} + req, err := http.NewRequest("GET", url, nil) + + if err != nil { + klog.Infof("Failed to get latest image version for %s, reverting to version %s\n", url, lastKnownGood) + return lastKnownGood + } + + resp, err := client.Do(req) + + if err != nil || resp.StatusCode != http.StatusOK { + klog.Infof("Failed to get latest image version for %s, reverting to version %s\n", url, lastKnownGood) + return lastKnownGood + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + klog.Infof("Failed to read latest image version for %s, reverting to version %s\n", url, lastKnownGood) + return lastKnownGood + } + + type TagsResponse struct { + Name string `json:"name"` + Tags []string `json:"tags"` + } + + tags := TagsResponse{} + err = json.Unmarshal(body, &tags) + if err != nil || len(tags.Tags) < 1 { + klog.Infof("Failed to read latest image version for %s, reverting to version %s\n", url, lastKnownGood) + return lastKnownGood + } + lastTagNum := len(tags.Tags) - 1 + return tags.Tags[lastTagNum] +} + // coreDNS returns the images used for CoreDNS func coreDNS(v semver.Version, mirror string) string { // Note: changing this logic requires bumping the preload version // Should match `CoreDNSImageName` and `CoreDNSVersion` in // https://github.com/kubernetes/kubernetes/blob/master/cmd/kubeadm/app/constants/constants.go - cv := "1.8.4" in := "coredns/coredns" + cv := "v1.8.6" + cv = findLatestTagFromRepository(fmt.Sprintf(tagURLTemplate, kubernetesRepo(mirror), in), cv) if semver.MustParseRange("<1.21.0-alpha.1")(v) { in = "coredns" } @@ -90,8 +141,9 @@ func etcd(v semver.Version, mirror string) string { // Should match `DefaultEtcdVersion` in: // https://github.com/kubernetes/kubernetes/blob/master/cmd/kubeadm/app/constants/constants.go ev := "3.5.0-0" - majorMinorVersion := fmt.Sprintf("v%d.%d", v.Major, v.Minor) imageName := "etcd" + ev = findLatestTagFromRepository(fmt.Sprintf(tagURLTemplate, kubernetesRepo(mirror), ev), ev) + majorMinorVersion := fmt.Sprintf("v%d.%d", v.Major, v.Minor) if eVersion, ok := constants.KubeadmImages[majorMinorVersion][imageName]; ok { ev = eVersion } diff --git a/pkg/minikube/bootstrapper/images/images_test.go b/pkg/minikube/bootstrapper/images/images_test.go index 2d2280016f5a..65fac2e26e92 100644 --- a/pkg/minikube/bootstrapper/images/images_test.go +++ b/pkg/minikube/bootstrapper/images/images_test.go @@ -17,6 +17,8 @@ limitations under the License. package images import ( + "net/http" + "net/http/httptest" "strings" "testing" @@ -91,6 +93,40 @@ k8s.gcr.io/coredns/coredns:v1.8.4 } } +func TestGetLatestTag(t *testing.T) { + serverResp := "{tags: [\"1.8.7\"]}" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(serverResp)) + if err != nil { + t.Errorf("failed to write https response") + } + })) + defer server.Close() + + var testCases = []struct { + name string + url string + lastKnownGood string + wsResponse string + expect string + }{ + {name: "VersionGetSuccess", url: server.URL, lastKnownGood: "v1.8.6", wsResponse: `{"name": "coredns", "tags": ["v1.8.9"]}`, expect: "v1.8.9"}, + {name: "VersionGetFail", url: server.URL, lastKnownGood: "v1.8.6", wsResponse: `{"name": "nah", "nope": ["v1.8.9"]}`, expect: "v1.8.6"}, + {name: "VersionGetFailNone", url: server.URL, lastKnownGood: "v1.8.6", wsResponse: ``, expect: "v1.8.6"}, + {name: "VersionGetSuccessMultiple", url: server.URL, lastKnownGood: "v1.8.6", wsResponse: `{"name": "coredns", "tags": ["1.8.7","v1.8.9"]}`, expect: "v1.8.9"}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + serverResp = tc.wsResponse + resp := findLatestTagFromRepository(tc.url, tc.lastKnownGood) + if diff := cmp.Diff(tc.expect, resp); diff != "" { + t.Errorf("Incorrect response version (-want +got):\n%s", diff) + } + }) + } +} + func TestAuxiliary(t *testing.T) { want := []string{ "gcr.io/k8s-minikube/storage-provisioner:" + version.GetStorageProvisionerVersion(),