diff --git a/cmd/kubeadm/app/cmd/BUILD b/cmd/kubeadm/app/cmd/BUILD index ab8feed120d..5624cc9705e 100644 --- a/cmd/kubeadm/app/cmd/BUILD +++ b/cmd/kubeadm/app/cmd/BUILD @@ -126,3 +126,14 @@ filegroup( ], tags = ["automanaged"], ) + +go_test( + name = "go_default_xtest", + srcs = ["config_test.go"], + deps = [ + ":go_default_library", + "//cmd/kubeadm/app/apis/kubeadm/v1alpha1:go_default_library", + "//cmd/kubeadm/app/features:go_default_library", + "//vendor/github.com/renstrom/dedent:go_default_library", + ], +) diff --git a/cmd/kubeadm/app/cmd/config.go b/cmd/kubeadm/app/cmd/config.go index f518d1993b2..36cb40cb878 100644 --- a/cmd/kubeadm/app/cmd/config.go +++ b/cmd/kubeadm/app/cmd/config.go @@ -19,17 +19,21 @@ package cmd import ( "fmt" "io" + "strings" "github.com/golang/glog" "github.com/renstrom/dedent" "github.com/spf13/cobra" + flag "github.com/spf13/pflag" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clientset "k8s.io/client-go/kubernetes" + kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" "k8s.io/kubernetes/cmd/kubeadm/app/constants" "k8s.io/kubernetes/cmd/kubeadm/app/features" + "k8s.io/kubernetes/cmd/kubeadm/app/images" "k8s.io/kubernetes/cmd/kubeadm/app/phases/uploadconfig" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" @@ -62,7 +66,7 @@ func NewCmdConfig(out io.Writer) *cobra.Command { cmd.AddCommand(NewCmdConfigUpload(out, &kubeConfigFile)) cmd.AddCommand(NewCmdConfigView(out, &kubeConfigFile)) - + cmd.AddCommand(NewCmdConfigListImages(out)) return cmd } @@ -201,3 +205,70 @@ func uploadConfiguration(client clientset.Interface, cfgPath string, defaultcfg // Then just call the uploadconfig phase to do the rest of the work return uploadconfig.UploadConfiguration(internalcfg, client) } + +// NewCmdConfigListImages returns the "kubeadm images" command +func NewCmdConfigListImages(out io.Writer) *cobra.Command { + cfg := &kubeadmapiext.MasterConfiguration{} + kubeadmapiext.SetDefaults_MasterConfiguration(cfg) + var cfgPath, featureGatesString string + var err error + + cmd := &cobra.Command{ + Use: "list-images", + Short: "Print a list of images kubeadm will use. The configuration file is used in case any images or image repositories are customized.", + Run: func(_ *cobra.Command, _ []string) { + if cfg.FeatureGates, err = features.NewFeatureGate(&features.InitFeatureGates, featureGatesString); err != nil { + kubeadmutil.CheckErr(err) + } + listImages, err := NewListImages(cfgPath, cfg) + kubeadmutil.CheckErr(err) + kubeadmutil.CheckErr(listImages.Run(out)) + }, + } + AddListImagesConfigFlag(cmd.PersistentFlags(), cfg, &featureGatesString) + AddListImagesFlags(cmd.PersistentFlags(), &cfgPath) + + return cmd +} + +// NewListImages returns a "kubeadm images" command +func NewListImages(cfgPath string, cfg *kubeadmapiext.MasterConfiguration) (*ListImages, error) { + internalcfg, err := configutil.ConfigFileAndDefaultsToInternalConfig(cfgPath, cfg) + if err != nil { + return nil, fmt.Errorf("could not convert cfg to an internal cfg: %v", err) + } + + return &ListImages{ + cfg: internalcfg, + }, nil +} + +// ListImages defines the struct used for "kubeadm images" +type ListImages struct { + cfg *kubeadmapi.MasterConfiguration +} + +// Run runs the images command and writes the result to the io.Writer passed in +func (i *ListImages) Run(out io.Writer) error { + imgs := images.GetAllImages(i.cfg) + for _, img := range imgs { + fmt.Fprintln(out, img) + } + + return nil +} + +// AddListImagesConfigFlag adds the flags that configure kubeadm +func AddListImagesConfigFlag(flagSet *flag.FlagSet, cfg *kubeadmapiext.MasterConfiguration, featureGatesString *string) { + flagSet.StringVar( + &cfg.KubernetesVersion, "kubernetes-version", cfg.KubernetesVersion, + `Choose a specific Kubernetes version for the control plane.`, + ) + flagSet.StringVar(featureGatesString, "feature-gates", *featureGatesString, "A set of key=value pairs that describe feature gates for various features. "+ + "Options are:\n"+strings.Join(features.KnownFeatures(&features.InitFeatureGates), "\n")) +} + +// AddListImagesFlags adds the flag that defines the location of the config file +func AddListImagesFlags(flagSet *flag.FlagSet, cfgPath *string) { + flagSet.StringVar(cfgPath, "config", *cfgPath, "Path to kubeadm config file.") +} diff --git a/cmd/kubeadm/app/cmd/config_test.go b/cmd/kubeadm/app/cmd/config_test.go new file mode 100644 index 00000000000..836fa5a8e5a --- /dev/null +++ b/cmd/kubeadm/app/cmd/config_test.go @@ -0,0 +1,172 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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 cmd_test + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/renstrom/dedent" + + kubeadmapiext "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" + "k8s.io/kubernetes/cmd/kubeadm/app/cmd" + "k8s.io/kubernetes/cmd/kubeadm/app/features" +) + +const ( + defaultNumberOfImages = 8 +) + +func TestNewCmdConfigListImages(t *testing.T) { + var output bytes.Buffer + images := cmd.NewCmdConfigListImages(&output) + images.Run(nil, nil) + actual := strings.Split(output.String(), "\n") + if len(actual) != defaultNumberOfImages { + t.Fatalf("Expected %v but found %v images", defaultNumberOfImages, len(actual)) + } +} + +func TestListImagesRunWithCustomConfigPath(t *testing.T) { + testcases := []struct { + name string + expectedImageCount int + // each string provided here must appear in at least one image returned by Run + expectedImageSubstrings []string + configContents []byte + }{ + { + name: "empty config contents", + expectedImageCount: defaultNumberOfImages, + configContents: []byte{}, + }, + { + name: "set k8s version", + expectedImageCount: defaultNumberOfImages, + expectedImageSubstrings: []string{ + ":v1.9.1", + }, + configContents: []byte(dedent.Dedent(` + apiVersion: kubeadm.k8s.io/v1alpha1 + kind: MasterConfiguration + kubernetesVersion: 1.9.1 + `)), + }, + { + name: "use coredns", + expectedImageCount: defaultNumberOfImages, + expectedImageSubstrings: []string{ + "coredns", + }, + configContents: []byte(dedent.Dedent(` + apiVersion: kubeadm.k8s.io/v1alpha1 + kind: MasterConfiguration + featureGates: + CoreDNS: True + `)), + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "kubeadm-images-test") + if err != nil { + t.Fatalf("Unable to create temporary directory: %v", err) + } + defer os.RemoveAll(tmpDir) + + configFilePath := filepath.Join(tmpDir, "test-config-file") + err = ioutil.WriteFile(configFilePath, tc.configContents, 0644) + if err != nil { + t.Fatalf("Failed writing a config file: %v", err) + } + + i, err := cmd.NewListImages(configFilePath, &kubeadmapiext.MasterConfiguration{}) + if err != nil { + t.Fatalf("Failed getting the kubeadm images command: %v", err) + } + var output bytes.Buffer + if i.Run(&output) != nil { + t.Fatalf("Error from running the images command: %v", err) + } + actual := strings.Split(output.String(), "\n") + if len(actual) != tc.expectedImageCount { + t.Fatalf("did not get the same number of images: actual: %v expected: %v. Actual value: %v", len(actual), tc.expectedImageCount, actual) + } + + for _, substring := range tc.expectedImageSubstrings { + if !strings.Contains(output.String(), substring) { + t.Errorf("Expected to find %v but did not in this list of images: %v", substring, actual) + } + } + }) + } +} + +func TestConfigListImagesRunWithoutPath(t *testing.T) { + testcases := []struct { + name string + cfg kubeadmapiext.MasterConfiguration + expectedImages int + }{ + { + name: "empty config", + expectedImages: defaultNumberOfImages, + }, + { + name: "external etcd configuration", + cfg: kubeadmapiext.MasterConfiguration{ + Etcd: kubeadmapiext.Etcd{ + Endpoints: []string{"hi"}, + }, + }, + expectedImages: defaultNumberOfImages - 1, + }, + { + name: "coredns enabled", + cfg: kubeadmapiext.MasterConfiguration{ + FeatureGates: map[string]bool{ + features.CoreDNS: true, + }, + }, + expectedImages: defaultNumberOfImages, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + i, err := cmd.NewListImages("", &tc.cfg) + if err != nil { + t.Fatalf("did not expect an error while creating the Images command: %v", err) + } + + var output bytes.Buffer + if i.Run(&output) != nil { + t.Fatalf("did not expect an error running the Images command: %v", err) + } + + actual := strings.Split(output.String(), "\n") + if len(actual) != tc.expectedImages { + t.Fatalf("expected %v images but got %v", tc.expectedImages, actual) + } + }) + } +} diff --git a/cmd/kubeadm/app/images/BUILD b/cmd/kubeadm/app/images/BUILD index 2f9e30e518e..a7609f1295b 100644 --- a/cmd/kubeadm/app/images/BUILD +++ b/cmd/kubeadm/app/images/BUILD @@ -11,7 +11,10 @@ go_library( srcs = ["images.go"], importpath = "k8s.io/kubernetes/cmd/kubeadm/app/images", deps = [ + "//cmd/kubeadm/app/apis/kubeadm:go_default_library", "//cmd/kubeadm/app/constants:go_default_library", + "//cmd/kubeadm/app/features:go_default_library", + "//cmd/kubeadm/app/phases/addons/dns:go_default_library", "//cmd/kubeadm/app/util:go_default_library", ], ) diff --git a/cmd/kubeadm/app/images/images.go b/cmd/kubeadm/app/images/images.go index ef6ceb7eed5..3b25bd8bf25 100644 --- a/cmd/kubeadm/app/images/images.go +++ b/cmd/kubeadm/app/images/images.go @@ -20,7 +20,10 @@ import ( "fmt" "runtime" + kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" "k8s.io/kubernetes/cmd/kubeadm/app/constants" + "k8s.io/kubernetes/cmd/kubeadm/app/features" + "k8s.io/kubernetes/cmd/kubeadm/app/phases/addons/dns" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" ) @@ -42,3 +45,25 @@ func GetCoreImage(image, repoPrefix, k8sVersion, overrideImage string) string { constants.KubeScheduler: fmt.Sprintf("%s/%s-%s:%s", repoPrefix, "kube-scheduler", runtime.GOARCH, kubernetesImageTag), }[image] } + +// GetAllImages returns a list of container images kubeadm expects to use on a control plane node +func GetAllImages(cfg *kubeadmapi.MasterConfiguration) []string { + imgs := []string{} + imgs = append(imgs, GetCoreImage(constants.KubeAPIServer, cfg.ImageRepository, cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage)) + imgs = append(imgs, GetCoreImage(constants.KubeControllerManager, cfg.ImageRepository, cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage)) + imgs = append(imgs, GetCoreImage(constants.KubeScheduler, cfg.ImageRepository, cfg.KubernetesVersion, cfg.UnifiedControlPlaneImage)) + imgs = append(imgs, fmt.Sprintf("%v/%v-%v:%v", cfg.ImageRepository, constants.KubeProxy, runtime.GOARCH, kubeadmutil.KubernetesVersionToImageTag(cfg.KubernetesVersion))) + imgs = append(imgs, fmt.Sprintf("%v/pause-%v:%v", cfg.ImageRepository, runtime.GOARCH, "3.1")) + + // if etcd is not external then add the image as it will be required + if len(cfg.Etcd.Endpoints) == 0 { + imgs = append(imgs, GetCoreImage(constants.Etcd, cfg.ImageRepository, cfg.KubernetesVersion, cfg.Etcd.Image)) + } + + dnsImage := fmt.Sprintf("%v/k8s-dns-kube-dns-%v:%v", cfg.ImageRepository, runtime.GOARCH, dns.GetDNSVersion(nil, constants.KubeDNS)) + if features.Enabled(cfg.FeatureGates, features.CoreDNS) { + dnsImage = fmt.Sprintf("coredns/coredns:%v", dns.GetDNSVersion(nil, constants.CoreDNS)) + } + imgs = append(imgs, dnsImage) + return imgs +} diff --git a/docs/.generated_docs b/docs/.generated_docs index aa1c65e3da2..691f514a87b 100644 --- a/docs/.generated_docs +++ b/docs/.generated_docs @@ -58,6 +58,7 @@ docs/admin/kubeadm_alpha_phase_selfhosting_convert-from-staticpods.md docs/admin/kubeadm_alpha_phase_upload-config.md docs/admin/kubeadm_completion.md docs/admin/kubeadm_config.md +docs/admin/kubeadm_config_list-images.md docs/admin/kubeadm_config_upload.md docs/admin/kubeadm_config_upload_from-file.md docs/admin/kubeadm_config_upload_from-flags.md @@ -132,6 +133,7 @@ docs/man/man1/kubeadm-alpha-phase-upload-config.1 docs/man/man1/kubeadm-alpha-phase.1 docs/man/man1/kubeadm-alpha.1 docs/man/man1/kubeadm-completion.1 +docs/man/man1/kubeadm-config-list-images.1 docs/man/man1/kubeadm-config-upload-from-file.1 docs/man/man1/kubeadm-config-upload-from-flags.1 docs/man/man1/kubeadm-config-upload.1 diff --git a/docs/admin/kubeadm_config_list-images.md b/docs/admin/kubeadm_config_list-images.md new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/admin/kubeadm_config_list-images.md @@ -0,0 +1,3 @@ +This file is autogenerated, but we've stopped checking such files into the +repository to reduce the need for rebases. Please run hack/generate-docs.sh to +populate this file. diff --git a/docs/man/man1/kubeadm-config-list-images.1 b/docs/man/man1/kubeadm-config-list-images.1 new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/man/man1/kubeadm-config-list-images.1 @@ -0,0 +1,3 @@ +This file is autogenerated, but we've stopped checking such files into the +repository to reduce the need for rebases. Please run hack/generate-docs.sh to +populate this file.