diff --git a/cmd/kubeadm/app/apis/kubeadm/fuzzer/fuzzer.go b/cmd/kubeadm/app/apis/kubeadm/fuzzer/fuzzer.go index 5dedda4e4b5..d9288ffeee0 100644 --- a/cmd/kubeadm/app/apis/kubeadm/fuzzer/fuzzer.go +++ b/cmd/kubeadm/app/apis/kubeadm/fuzzer/fuzzer.go @@ -40,6 +40,7 @@ func Funcs(codecs runtimeserializer.CodecFactory) []interface{} { obj.Etcd.Image = "foo" obj.Etcd.DataDir = "foo" obj.ImageRepository = "foo" + obj.CIImageRepository = "" obj.UnifiedControlPlaneImage = "foo" obj.FeatureFlags = map[string]bool{} }, diff --git a/cmd/kubeadm/app/apis/kubeadm/types.go b/cmd/kubeadm/app/apis/kubeadm/types.go index 62010f10bea..32d9c3410bb 100644 --- a/cmd/kubeadm/app/apis/kubeadm/types.go +++ b/cmd/kubeadm/app/apis/kubeadm/types.go @@ -49,6 +49,11 @@ type MasterConfiguration struct { // ImageRepository what container registry to pull control plane images from ImageRepository string + + // Container registry for core images generated by CI + // +k8s:conversion-gen=false + CIImageRepository string + // UnifiedControlPlaneImage specifies if a specific container image should be used for all control plane components UnifiedControlPlaneImage string @@ -115,3 +120,15 @@ type NodeConfiguration struct { // the security of kubeadm since other nodes can impersonate the master. DiscoveryTokenUnsafeSkipCAVerification bool } + +// GetControlPlaneImageRepository returns name of image repository +// for control plane images (API,Controller Manager,Scheduler and Proxy) +// It will override location with CI registry name in case user requests special +// Kubernetes version from CI build area. +// (See: kubeadmconstants.DefaultCIImageRepository) +func (cfg *MasterConfiguration) GetControlPlaneImageRepository() string { + if cfg.CIImageRepository != "" { + return cfg.CIImageRepository + } + return cfg.ImageRepository +} diff --git a/cmd/kubeadm/app/apis/kubeadm/v1alpha1/BUILD b/cmd/kubeadm/app/apis/kubeadm/v1alpha1/BUILD index 3feef312b9d..b75dec730b3 100644 --- a/cmd/kubeadm/app/apis/kubeadm/v1alpha1/BUILD +++ b/cmd/kubeadm/app/apis/kubeadm/v1alpha1/BUILD @@ -12,10 +12,12 @@ go_library( "doc.go", "register.go", "types.go", + "zz_generated.conversion.go", "zz_generated.deepcopy.go", "zz_generated.defaults.go", ], deps = [ + "//cmd/kubeadm/app/apis/kubeadm:go_default_library", "//cmd/kubeadm/app/constants:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/conversion:go_default_library", diff --git a/cmd/kubeadm/app/apis/kubeadm/v1alpha1/doc.go b/cmd/kubeadm/app/apis/kubeadm/v1alpha1/doc.go index 4609a57d9eb..d0a277ec0b2 100644 --- a/cmd/kubeadm/app/apis/kubeadm/v1alpha1/doc.go +++ b/cmd/kubeadm/app/apis/kubeadm/v1alpha1/doc.go @@ -17,4 +17,5 @@ limitations under the License. // +k8s:defaulter-gen=TypeMeta // +groupName=kubeadm.k8s.io // +k8s:deepcopy-gen=package +// +k8s:conversion-gen=k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm package v1alpha1 // import "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" diff --git a/cmd/kubeadm/app/constants/constants.go b/cmd/kubeadm/app/constants/constants.go index 8a5a98c0ab7..c3c1d961531 100644 --- a/cmd/kubeadm/app/constants/constants.go +++ b/cmd/kubeadm/app/constants/constants.go @@ -116,6 +116,9 @@ const ( // NodeBootstrapTokenAuthGroup specifies which group a Node Bootstrap Token should be authenticated in // TODO: This should be changed in the v1.8 dev cycle to a node-BT-specific group instead of the generic Bootstrap Token group that is used now NodeBootstrapTokenAuthGroup = "system:bootstrappers" + + // DefaultCIImageRepository points to image registry where CI uploads images from ci-cross build job + DefaultCIImageRepository = "gcr.io/kubernetes-ci-images" ) var ( diff --git a/cmd/kubeadm/app/phases/addons/proxy/proxy.go b/cmd/kubeadm/app/phases/addons/proxy/proxy.go index 3cab1968131..98c5dfe60d6 100644 --- a/cmd/kubeadm/app/phases/addons/proxy/proxy.go +++ b/cmd/kubeadm/app/phases/addons/proxy/proxy.go @@ -63,7 +63,7 @@ func EnsureProxyAddon(cfg *kubeadmapi.MasterConfiguration, client clientset.Inte } proxyDaemonSetBytes, err := kubeadmutil.ParseTemplate(KubeProxyDaemonSet, struct{ ImageRepository, Arch, Version, ImageOverride, ClusterCIDR, MasterTaintKey, CloudTaintKey string }{ - ImageRepository: cfg.ImageRepository, + ImageRepository: cfg.GetControlPlaneImageRepository(), Arch: runtime.GOARCH, Version: kubeadmutil.KubernetesVersionToImageTag(cfg.KubernetesVersion), ImageOverride: cfg.UnifiedControlPlaneImage, diff --git a/cmd/kubeadm/app/phases/controlplane/manifests.go b/cmd/kubeadm/app/phases/controlplane/manifests.go index ed073cedd5f..b6d6c7d5a6d 100644 --- a/cmd/kubeadm/app/phases/controlplane/manifests.go +++ b/cmd/kubeadm/app/phases/controlplane/manifests.go @@ -71,7 +71,7 @@ func GetStaticPodSpecs(cfg *kubeadmapi.MasterConfiguration, k8sVersion *version. staticPodSpecs := map[string]v1.Pod{ kubeadmconstants.KubeAPIServer: staticpodutil.ComponentPod(v1.Container{ Name: kubeadmconstants.KubeAPIServer, - Image: images.GetCoreImage(kubeadmconstants.KubeAPIServer, cfg.ImageRepository, cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage), + Image: images.GetCoreImage(kubeadmconstants.KubeAPIServer, cfg.GetControlPlaneImageRepository(), cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage), Command: getAPIServerCommand(cfg, k8sVersion), VolumeMounts: mounts.GetVolumeMounts(kubeadmconstants.KubeAPIServer), LivenessProbe: staticpodutil.ComponentProbe(int(cfg.API.BindPort), "/healthz", v1.URISchemeHTTPS), @@ -80,7 +80,7 @@ func GetStaticPodSpecs(cfg *kubeadmapi.MasterConfiguration, k8sVersion *version. }, mounts.GetVolumes(kubeadmconstants.KubeAPIServer)), kubeadmconstants.KubeControllerManager: staticpodutil.ComponentPod(v1.Container{ Name: kubeadmconstants.KubeControllerManager, - Image: images.GetCoreImage(kubeadmconstants.KubeControllerManager, cfg.ImageRepository, cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage), + Image: images.GetCoreImage(kubeadmconstants.KubeControllerManager, cfg.GetControlPlaneImageRepository(), cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage), Command: getControllerManagerCommand(cfg, k8sVersion), VolumeMounts: mounts.GetVolumeMounts(kubeadmconstants.KubeControllerManager), LivenessProbe: staticpodutil.ComponentProbe(10252, "/healthz", v1.URISchemeHTTP), @@ -89,7 +89,7 @@ func GetStaticPodSpecs(cfg *kubeadmapi.MasterConfiguration, k8sVersion *version. }, mounts.GetVolumes(kubeadmconstants.KubeControllerManager)), kubeadmconstants.KubeScheduler: staticpodutil.ComponentPod(v1.Container{ Name: kubeadmconstants.KubeScheduler, - Image: images.GetCoreImage(kubeadmconstants.KubeScheduler, cfg.ImageRepository, cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage), + Image: images.GetCoreImage(kubeadmconstants.KubeScheduler, cfg.GetControlPlaneImageRepository(), cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage), Command: getSchedulerCommand(cfg), VolumeMounts: mounts.GetVolumeMounts(kubeadmconstants.KubeScheduler), LivenessProbe: staticpodutil.ComponentProbe(10251, "/healthz", v1.URISchemeHTTP), diff --git a/cmd/kubeadm/app/util/config/masterconfig.go b/cmd/kubeadm/app/util/config/masterconfig.go index 8cb6aaf018c..7a2bb0d94bb 100644 --- a/cmd/kubeadm/app/util/config/masterconfig.go +++ b/cmd/kubeadm/app/util/config/masterconfig.go @@ -44,6 +44,11 @@ func SetInitDynamicDefaults(cfg *kubeadmapi.MasterConfiguration) error { } cfg.API.AdvertiseAddress = ip.String() + // Requested version is automatic CI build, thus use KubernetesCI Image Repository for core images + if kubeadmutil.KubernetesIsCIVersion(cfg.KubernetesVersion) { + cfg.CIImageRepository = kubeadmconstants.DefaultCIImageRepository + } + // Validate version argument ver, err := kubeadmutil.KubernetesReleaseVersion(cfg.KubernetesVersion) if err != nil { diff --git a/cmd/kubeadm/app/util/version.go b/cmd/kubeadm/app/util/version.go index ae442701aa0..81d521c2616 100644 --- a/cmd/kubeadm/app/util/version.go +++ b/cmd/kubeadm/app/util/version.go @@ -25,9 +25,10 @@ import ( ) var ( - kubeReleaseBucketURL = "https://storage.googleapis.com/kubernetes-release/release" + kubeReleaseBucketURL = "https://dl.k8s.io" kubeReleaseRegex = regexp.MustCompile(`^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)([-0-9a-zA-Z_\.+]*)?$`) kubeReleaseLabelRegex = regexp.MustCompile(`^[[:lower:]]+(-[-\w_\.]+)?$`) + kubeBucketPrefixes = regexp.MustCompile(`^((release|ci|ci-cross)/)?([-\w_\.+]+)$`) ) // KubernetesReleaseVersion is helper function that can fetch @@ -53,22 +54,20 @@ func KubernetesReleaseVersion(version string) (string, error) { return version, nil } return "v" + version, nil - } else if kubeReleaseLabelRegex.MatchString(version) { - url := fmt.Sprintf("%s/%s.txt", kubeReleaseBucketURL, version) - resp, err := http.Get(url) + } + + bucketURL, versionLabel, err := splitVersion(version) + if err != nil { + return "", err + } + if kubeReleaseLabelRegex.MatchString(versionLabel) { + url := fmt.Sprintf("%s/%s.txt", bucketURL, versionLabel) + body, err := fetchFromURL(url) if err != nil { - return "", fmt.Errorf("unable to get URL %q: %s", url, err.Error()) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("unable to fetch release information. URL: %q Status: %v", url, resp.Status) - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("unable to read content of URL %q: %s", url, err.Error()) + return "", err } // Re-validate received version and return. - return KubernetesReleaseVersion(strings.Trim(string(body), " \t\n")) + return KubernetesReleaseVersion(body) } return "", fmt.Errorf("version %q doesn't match patterns for neither semantic version nor labels (stable, latest, ...)", version) } @@ -83,3 +82,49 @@ func KubernetesVersionToImageTag(version string) string { allowed := regexp.MustCompile(`[^-a-zA-Z0-9_\.]`) return allowed.ReplaceAllString(version, "_") } + +// KubernetesIsCIVersion checks if user requested CI version +func KubernetesIsCIVersion(version string) bool { + subs := kubeBucketPrefixes.FindAllStringSubmatch(version, 1) + if len(subs) == 1 && len(subs[0]) == 4 && strings.HasPrefix(subs[0][2], "ci") { + return true + } + return false +} + +// Internal helper: split version parts, +// Return base URL and cleaned-up version +func splitVersion(version string) (string, string, error) { + var urlSuffix string + subs := kubeBucketPrefixes.FindAllStringSubmatch(version, 1) + if len(subs) != 1 || len(subs[0]) != 4 { + return "", "", fmt.Errorf("invalid version %q", version) + } + + switch { + case strings.HasPrefix(subs[0][2], "ci"): + // Special case. CI images populated only by ci-cross area + urlSuffix = "ci-cross" + default: + urlSuffix = "release" + } + url := fmt.Sprintf("%s/%s", kubeReleaseBucketURL, urlSuffix) + return url, subs[0][3], nil +} + +// Internal helper: return content of URL +func fetchFromURL(url string) (string, error) { + resp, err := http.Get(url) + if err != nil { + return "", fmt.Errorf("unable to get URL %q: %s", url, err.Error()) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("unable to fetch file. URL: %q Status: %v", url, resp.Status) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("unable to read content of URL %q: %s", url, err.Error()) + } + return strings.TrimSpace(string(body)), nil +} diff --git a/cmd/kubeadm/app/util/version_test.go b/cmd/kubeadm/app/util/version_test.go index 491ea24344a..a333031a5b1 100644 --- a/cmd/kubeadm/app/util/version_test.go +++ b/cmd/kubeadm/app/util/version_test.go @@ -45,7 +45,7 @@ func TestValidVersion(t *testing.T) { "v1.6.0-alpha.0.536+d60d9f3269288f", "v1.5.0-alpha.0.1078+1044b6822497da-pull", "v1.5.0-alpha.1.822+49b9e32fad9f32-pull-gke-gci", - "v1.6.1_coreos.0", + "v1.6.1+coreos.0", } for _, s := range validVersions { ver, err := KubernetesReleaseVersion(s) @@ -165,3 +165,70 @@ func TestVersionToTag(t *testing.T) { } } } + +func TestSplitVersion(t *testing.T) { + type T struct { + input string + bucket string + label string + valid bool + } + cases := []T{ + // Release area + {"v1.7.0", "https://dl.k8s.io/release", "v1.7.0", true}, + {"v1.8.0-alpha.2.1231+afabd012389d53a", "https://dl.k8s.io/release", "v1.8.0-alpha.2.1231+afabd012389d53a", true}, + {"release/v1.7.0", "https://dl.k8s.io/release", "v1.7.0", true}, + {"release/latest-1.7", "https://dl.k8s.io/release", "latest-1.7", true}, + // CI builds area, lookup actual builds at ci-cross/*.txt + {"ci-cross/latest", "https://dl.k8s.io/ci-cross", "latest", true}, + {"ci/latest-1.7", "https://dl.k8s.io/ci-cross", "latest-1.7", true}, + // unknown label in default (release) area: splitVersion validate only areas. + {"unknown-1", "https://dl.k8s.io/release", "unknown-1", true}, + // unknown area, not valid input. + {"unknown/latest-1", "", "", false}, + } + + // kubeReleaseBucketURL can be overriden during network tests, thus ensure + // it will contain value corresponding to expected outcome for this unit test + kubeReleaseBucketURL = "https://dl.k8s.io" + + for _, tc := range cases { + bucket, label, err := splitVersion(tc.input) + switch { + case err != nil && tc.valid: + t.Errorf("splitVersion: unexpected error for %q. Error: %v", tc.input, err) + case err == nil && !tc.valid: + t.Errorf("splitVersion: error expected for key %q, but result is %q, %q", tc.input, bucket, label) + case bucket != tc.bucket: + t.Errorf("splitVersion: unexpected bucket result for key %q. Expected: %q Actual: %q", tc.input, tc.bucket, bucket) + case label != tc.label: + t.Errorf("splitVersion: unexpected label result for key %q. Expected: %q Actual: %q", tc.input, tc.label, label) + } + + } +} + +func TestKubernetesIsCIVersion(t *testing.T) { + type T struct { + input string + expected bool + } + cases := []T{ + {"", false}, + // Official releases + {"v1.0.0", false}, + {"release/v1.0.0", false}, + // CI builds + {"ci/latest-1", true}, + {"ci-cross/latest", true}, + } + + for _, tc := range cases { + result := KubernetesIsCIVersion(tc.input) + t.Logf("KubernetesIsCIVersion: Input: %q. Result: %v. Expected: %v", tc.input, result, tc.expected) + if result != tc.expected { + t.Errorf("failed KubernetesIsCIVersion: Input: %q. Result: %v. Expected: %v", tc.input, result, tc.expected) + } + } + +}