diff --git a/cmd/kubeadm/app/cmd/config.go b/cmd/kubeadm/app/cmd/config.go index 71b419d0bc8..cdcee23204c 100644 --- a/cmd/kubeadm/app/cmd/config.go +++ b/cmd/kubeadm/app/cmd/config.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors. +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. @@ -39,6 +39,7 @@ import ( kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig" + utilsexec "k8s.io/utils/exec" ) // NewCmdConfig returns cobra.Command for "kubeadm config" command @@ -214,13 +215,63 @@ func NewCmdConfigImages(out io.Writer) *cobra.Command { RunE: cmdutil.SubCmdRunE("images"), } cmd.AddCommand(NewCmdConfigImagesList(out)) + cmd.AddCommand(NewCmdConfigImagesPull()) return cmd } -// NewCmdConfigImagesList returns the "config images list" command +// NewCmdConfigImagesPull returns the `config images pull` command +func NewCmdConfigImagesPull() *cobra.Command { + cfg := &kubeadmapiv1alpha2.MasterConfiguration{} + kubeadmscheme.Scheme.Default(cfg) + var cfgPath, featureGatesString string + var err error + cmd := &cobra.Command{ + Use: "pull", + Short: "Pull images used by kubeadm.", + Run: func(_ *cobra.Command, _ []string) { + cfg.FeatureGates, err = features.NewFeatureGate(&features.InitFeatureGates, featureGatesString) + kubeadmutil.CheckErr(err) + internalcfg, err := configutil.ConfigFileAndDefaultsToInternalConfig(cfgPath, cfg) + kubeadmutil.CheckErr(err) + puller, err := images.NewImagePuller(utilsexec.New(), internalcfg.GetCRISocket()) + kubeadmutil.CheckErr(err) + imagesPull := NewImagesPull(puller, images.GetAllImages(internalcfg)) + kubeadmutil.CheckErr(imagesPull.PullAll()) + }, + } + AddImagesCommonConfigFlags(cmd.PersistentFlags(), cfg, &featureGatesString) + return cmd +} + +// ImagesPull is the struct used to hold information relating to image pulling +type ImagesPull struct { + puller images.Puller + images []string +} + +// NewImagesPull initializes and returns the `config images pull` command +func NewImagesPull(puller images.Puller, images []string) *ImagesPull { + return &ImagesPull{ + puller: puller, + images: images, + } +} + +// PullAll pulls all images that the ImagesPull knows about +func (ip *ImagesPull) PullAll() error { + for _, image := range ip.images { + if err := ip.puller.Pull(image); err != nil { + return fmt.Errorf("failed to pull image %q: %v", image, err) + } + glog.Infof("[config/images] Pulled %s\n", image) + } + return nil +} + +// NewCmdConfigImagesList returns the "kubeadm config images list" command func NewCmdConfigImagesList(out io.Writer) *cobra.Command { cfg := &kubeadmapiv1alpha2.MasterConfiguration{} - kubeadmapiv1alpha2.SetDefaults_MasterConfiguration(cfg) + kubeadmscheme.Scheme.Default(cfg) var cfgPath, featureGatesString string var err error @@ -228,15 +279,14 @@ func NewCmdConfigImagesList(out io.Writer) *cobra.Command { Use: "list", 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) - } + cfg.FeatureGates, err = features.NewFeatureGate(&features.InitFeatureGates, featureGatesString) + kubeadmutil.CheckErr(err) imagesList, err := NewImagesList(cfgPath, cfg) kubeadmutil.CheckErr(err) kubeadmutil.CheckErr(imagesList.Run(out)) }, } - AddImagesListConfigFlags(cmd.PersistentFlags(), cfg, &featureGatesString) + AddImagesCommonConfigFlags(cmd.PersistentFlags(), cfg, &featureGatesString) AddImagesListFlags(cmd.PersistentFlags(), &cfgPath) return cmd @@ -259,7 +309,7 @@ type ImagesList struct { cfg *kubeadmapi.MasterConfiguration } -// Run gets a list of images kubeadm expects to use and writes the result to the io.Writer passed in +// Run runs the images command and writes the result to the io.Writer passed in func (i *ImagesList) Run(out io.Writer) error { imgs := images.GetAllImages(i.cfg) for _, img := range imgs { @@ -269,8 +319,8 @@ func (i *ImagesList) Run(out io.Writer) error { return nil } -// AddImagesListConfigFlags adds the flags that configure kubeadm (and affect the images kubeadm will use) -func AddImagesListConfigFlags(flagSet *flag.FlagSet, cfg *kubeadmapiv1alpha2.MasterConfiguration, featureGatesString *string) { +// AddImagesCommonConfigFlags adds the flags that configure kubeadm (and affect the images kubeadm will use) +func AddImagesCommonConfigFlags(flagSet *flag.FlagSet, cfg *kubeadmapiv1alpha2.MasterConfiguration, featureGatesString *string) { flagSet.StringVar( &cfg.KubernetesVersion, "kubernetes-version", cfg.KubernetesVersion, `Choose a specific Kubernetes version for the control plane.`, @@ -283,3 +333,8 @@ func AddImagesListConfigFlags(flagSet *flag.FlagSet, cfg *kubeadmapiv1alpha2.Mas func AddImagesListFlags(flagSet *flag.FlagSet, cfgPath *string) { flagSet.StringVar(cfgPath, "config", *cfgPath, "Path to kubeadm config file.") } + +// AddImagesPullFlags adds flags related to the `config images pull` command +func AddImagesPullFlags(flagSet *flag.FlagSet, criSocketPath *string) { + flagSet.StringVar(criSocketPath, "cri-socket-path", *criSocketPath, "Path to the CRI socket.") +} diff --git a/cmd/kubeadm/app/cmd/config_test.go b/cmd/kubeadm/app/cmd/config_test.go index c9b107b95be..b151a3f59a0 100644 --- a/cmd/kubeadm/app/cmd/config_test.go +++ b/cmd/kubeadm/app/cmd/config_test.go @@ -35,7 +35,7 @@ const ( defaultNumberOfImages = 8 ) -func TestNewCmdConfigListImages(t *testing.T) { +func TestNewCmdConfigImagesList(t *testing.T) { var output bytes.Buffer images := cmd.NewCmdConfigImagesList(&output) images.Run(nil, nil) @@ -170,3 +170,27 @@ func TestConfigImagesListRunWithoutPath(t *testing.T) { }) } } + +type fakePuller struct { + count map[string]int +} + +func (f *fakePuller) Pull(image string) error { + f.count[image]++ + return nil +} + +func TestImagesPull(t *testing.T) { + puller := &fakePuller{ + count: make(map[string]int), + } + images := []string{"a", "b", "c", "d", "a"} + ip := cmd.NewImagesPull(puller, images) + err := ip.PullAll() + if err != nil { + t.Fatalf("expected nil but found %v", err) + } + if puller.count["a"] != 2 { + t.Fatalf("expected 2 but found %v", puller.count["a"]) + } +} diff --git a/cmd/kubeadm/app/images/BUILD b/cmd/kubeadm/app/images/BUILD index a7609f1295b..27e970e71d4 100644 --- a/cmd/kubeadm/app/images/BUILD +++ b/cmd/kubeadm/app/images/BUILD @@ -8,14 +8,19 @@ load( go_library( name = "go_default_library", - srcs = ["images.go"], + srcs = [ + "images.go", + "puller.go", + ], importpath = "k8s.io/kubernetes/cmd/kubeadm/app/images", deps = [ "//cmd/kubeadm/app/apis/kubeadm:go_default_library", + "//cmd/kubeadm/app/apis/kubeadm/v1alpha1: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", + "//vendor/k8s.io/utils/exec:go_default_library", ], ) @@ -38,3 +43,13 @@ filegroup( srcs = [":package-srcs"], tags = ["automanaged"], ) + +go_test( + name = "go_default_xtest", + srcs = ["puller_test.go"], + deps = [ + ":go_default_library", + "//cmd/kubeadm/app/apis/kubeadm/v1alpha1:go_default_library", + "//vendor/k8s.io/utils/exec:go_default_library", + ], +) diff --git a/cmd/kubeadm/app/images/puller.go b/cmd/kubeadm/app/images/puller.go new file mode 100644 index 00000000000..71db11e481f --- /dev/null +++ b/cmd/kubeadm/app/images/puller.go @@ -0,0 +1,57 @@ +/* +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 images + +import ( + "fmt" + + kubeadmapiv1alpha1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" + utilsexec "k8s.io/utils/exec" +) + +// Puller is an interface for pulling images +type Puller interface { + Pull(string) error +} + +// ImagePuller is a struct that can pull images and hides the implementation (crictl vs docker) +type ImagePuller struct { + criSocket string + exec utilsexec.Interface + crictlPath string +} + +// NewImagePuller returns a ready to go ImagePuller +func NewImagePuller(execer utilsexec.Interface, criSocket string) (*ImagePuller, error) { + crictlPath, err := execer.LookPath("crictl") + if err != nil && criSocket != kubeadmapiv1alpha1.DefaultCRISocket { + return nil, fmt.Errorf("crictl is required for non docker container runtimes: %v", err) + } + return &ImagePuller{ + exec: execer, + criSocket: criSocket, + crictlPath: crictlPath, + }, nil +} + +// Pull pulls the actual image using either crictl or docker +func (ip *ImagePuller) Pull(image string) error { + if ip.criSocket != kubeadmapiv1alpha1.DefaultCRISocket { + return ip.exec.Command(ip.crictlPath, "-r", ip.criSocket, "pull", image).Run() + } + return ip.exec.Command("sh", "-c", fmt.Sprintf("docker pull %v", image)).Run() +} diff --git a/cmd/kubeadm/app/images/puller_test.go b/cmd/kubeadm/app/images/puller_test.go new file mode 100644 index 00000000000..6a27ec03276 --- /dev/null +++ b/cmd/kubeadm/app/images/puller_test.go @@ -0,0 +1,138 @@ +/* +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 images_test + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "os" + "strings" + "testing" + + kubeadmdefaults "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1alpha1" + "k8s.io/kubernetes/cmd/kubeadm/app/images" + "k8s.io/utils/exec" +) + +type fakeCmd struct { + cmd string + args []string + out io.Writer +} + +func (f *fakeCmd) Run() error { + fmt.Fprintf(f.out, "%v %v", f.cmd, strings.Join(f.args, " ")) + return nil +} +func (f *fakeCmd) CombinedOutput() ([]byte, error) { return nil, nil } +func (f *fakeCmd) Output() ([]byte, error) { return nil, nil } +func (f *fakeCmd) SetDir(dir string) {} +func (f *fakeCmd) SetStdin(in io.Reader) {} +func (f *fakeCmd) SetStdout(out io.Writer) { + f.out = out +} +func (f *fakeCmd) SetStderr(out io.Writer) {} +func (f *fakeCmd) Stop() {} + +type fakeExecer struct { + cmd exec.Cmd + lookPathSucceeds bool +} + +func (f *fakeExecer) Command(cmd string, args ...string) exec.Cmd { return f.cmd } +func (f *fakeExecer) CommandContext(ctx context.Context, cmd string, args ...string) exec.Cmd { + return f.cmd +} +func (f *fakeExecer) LookPath(file string) (string, error) { + if f.lookPathSucceeds { + return file, nil + } + return "", &os.PathError{Err: errors.New("does not exist")} +} + +func TestImagePuller(t *testing.T) { + testcases := []struct { + name string + criSocket string + cmd exec.Cmd + findCrictl bool + expected string + errorExpected bool + }{ + { + name: "New succeeds even if crictl is not in path", + criSocket: kubeadmdefaults.DefaultCRISocket, + cmd: &fakeCmd{ + cmd: "hello", + args: []string{"world", "and", "friends"}, + }, + findCrictl: false, + expected: "hello world and friends", + }, + { + name: "New succeeds with crictl in path", + criSocket: "/not/default", + cmd: &fakeCmd{ + cmd: "crictl", + args: []string{"-r", "/some/socket", "imagename"}, + }, + findCrictl: true, + expected: "crictl -r /some/socket imagename", + }, + { + name: "New fails with crictl not in path but is required", + criSocket: "/not/docker", + cmd: &fakeCmd{ + cmd: "crictl", + args: []string{"-r", "/not/docker", "an image"}, + }, + findCrictl: false, + errorExpected: true, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + var b bytes.Buffer + tc.cmd.SetStdout(&b) + fe := &fakeExecer{ + cmd: tc.cmd, + lookPathSucceeds: tc.findCrictl, + } + ip, err := images.NewImagePuller(fe, tc.criSocket) + + if tc.errorExpected { + if err == nil { + t.Fatalf("expected an error but found nil: %v", fe) + } + return + } + + if err != nil { + t.Fatalf("expected nil but found an error: %v", err) + } + if err = ip.Pull("imageName"); err != nil { + t.Fatalf("expected nil pulling an image but found: %v", err) + } + if b.String() != tc.expected { + t.Fatalf("expected %v but got: %v", tc.expected, b.String()) + } + }) + } +} diff --git a/docs/.generated_docs b/docs/.generated_docs index 4b6a7490c33..03be070e47f 100644 --- a/docs/.generated_docs +++ b/docs/.generated_docs @@ -60,6 +60,7 @@ docs/admin/kubeadm_completion.md docs/admin/kubeadm_config.md docs/admin/kubeadm_config_images.md docs/admin/kubeadm_config_images_list.md +docs/admin/kubeadm_config_images_pull.md docs/admin/kubeadm_config_upload.md docs/admin/kubeadm_config_upload_from-file.md docs/admin/kubeadm_config_upload_from-flags.md @@ -135,6 +136,7 @@ docs/man/man1/kubeadm-alpha-phase.1 docs/man/man1/kubeadm-alpha.1 docs/man/man1/kubeadm-completion.1 docs/man/man1/kubeadm-config-images-list.1 +docs/man/man1/kubeadm-config-images-pull.1 docs/man/man1/kubeadm-config-images.1 docs/man/man1/kubeadm-config-upload-from-file.1 docs/man/man1/kubeadm-config-upload-from-flags.1 diff --git a/docs/admin/kubeadm_config_images_pull.md b/docs/admin/kubeadm_config_images_pull.md new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/admin/kubeadm_config_images_pull.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-images-list-images.1 b/docs/man/man1/kubeadm-config-images-list-images.1 new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/man/man1/kubeadm-config-images-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. diff --git a/docs/man/man1/kubeadm-config-images-pull.1 b/docs/man/man1/kubeadm-config-images-pull.1 new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/man/man1/kubeadm-config-images-pull.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.