From 16e46c8afd2474177b7ef5d66f7c1f61b28eb06b Mon Sep 17 00:00:00 2001 From: Angus Lees Date: Tue, 7 Aug 2018 11:40:09 +1000 Subject: [PATCH 1/2] kubeadm: chroot to new --rootfs arg This change adds a new --rootfs=path option to kubeadm, and (if provided) chroot()s to this path before performing file operations. This makes it possible to run the kubeadm binary from a container, but perform remaining file operations against the host filesystem using something like: docker run -v /:/rootfs --net=host --uts=host --pid=host \ kubeadm:latest init --rootfs /rootfs... Fixes kubernetes/kubeadm#503 --- .../app/apis/kubeadm/validation/validation.go | 2 +- cmd/kubeadm/app/cmd/cmd.go | 23 +++++++ cmd/kubeadm/app/util/BUILD | 3 + cmd/kubeadm/app/util/chroot_test.go | 66 +++++++++++++++++++ cmd/kubeadm/app/util/chroot_unix.go | 40 +++++++++++ cmd/kubeadm/app/util/chroot_windows.go | 30 +++++++++ 6 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 cmd/kubeadm/app/util/chroot_test.go create mode 100644 cmd/kubeadm/app/util/chroot_unix.go create mode 100644 cmd/kubeadm/app/util/chroot_windows.go diff --git a/cmd/kubeadm/app/apis/kubeadm/validation/validation.go b/cmd/kubeadm/app/apis/kubeadm/validation/validation.go index d8470b7d005..8c25c061280 100644 --- a/cmd/kubeadm/app/apis/kubeadm/validation/validation.go +++ b/cmd/kubeadm/app/apis/kubeadm/validation/validation.go @@ -348,7 +348,7 @@ func ValidateMixedArguments(flag *pflag.FlagSet) error { mixedInvalidFlags := []string{} flag.Visit(func(f *pflag.Flag) { - if f.Name == "config" || f.Name == "ignore-preflight-errors" || strings.HasPrefix(f.Name, "skip-") || f.Name == "dry-run" || f.Name == "kubeconfig" || f.Name == "v" { + if f.Name == "config" || f.Name == "ignore-preflight-errors" || strings.HasPrefix(f.Name, "skip-") || f.Name == "dry-run" || f.Name == "kubeconfig" || f.Name == "v" || f.Name == "rootfs" { // "--skip-*" flags or other whitelisted flags can be set with --config return } diff --git a/cmd/kubeadm/app/cmd/cmd.go b/cmd/kubeadm/app/cmd/cmd.go index d968e53732c..cf3481aac4d 100644 --- a/cmd/kubeadm/app/cmd/cmd.go +++ b/cmd/kubeadm/app/cmd/cmd.go @@ -21,15 +21,19 @@ import ( "github.com/renstrom/dedent" "github.com/spf13/cobra" + "github.com/spf13/pflag" "k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases" "k8s.io/kubernetes/cmd/kubeadm/app/cmd/upgrade" + kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" // Register the kubeadm configuration types because CLI flag generation // depends on the generated defaults. ) // NewKubeadmCommand return cobra.Command to run kubeadm command func NewKubeadmCommand(in io.Reader, out, err io.Writer) *cobra.Command { + var rootfsPath string + cmds := &cobra.Command{ Use: "kubeadm", Short: "kubeadm: easily bootstrap a secure Kubernetes cluster", @@ -65,6 +69,15 @@ func NewKubeadmCommand(in io.Reader, out, err io.Writer) *cobra.Command { You can then repeat the second step on as many other machines as you like. `), + + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if rootfsPath != "" { + if err := kubeadmutil.Chroot(rootfsPath); err != nil { + return err + } + } + return nil + }, } cmds.ResetFlags() @@ -86,5 +99,15 @@ func NewKubeadmCommand(in io.Reader, out, err io.Writer) *cobra.Command { experimentalCmd.AddCommand(phases.NewCmdPhase(out)) cmds.AddCommand(experimentalCmd) + AddKubeadmOtherFlags(cmds.PersistentFlags(), &rootfsPath) + return cmds } + +// AddKubeadmOtherFlags adds flags that are not bound to a configuration file to the given flagset +func AddKubeadmOtherFlags(flagSet *pflag.FlagSet, rootfsPath *string) { + flagSet.StringVar( + rootfsPath, "rootfs", *rootfsPath, + "The path to the 'real' host root filesystem.", + ) +} diff --git a/cmd/kubeadm/app/util/BUILD b/cmd/kubeadm/app/util/BUILD index 78013cc8624..2ce74082df5 100644 --- a/cmd/kubeadm/app/util/BUILD +++ b/cmd/kubeadm/app/util/BUILD @@ -11,6 +11,8 @@ go_library( srcs = [ "arguments.go", "cgroupdriver.go", + "chroot_unix.go", + "chroot_windows.go", "copy.go", "endpoint.go", "error.go", @@ -40,6 +42,7 @@ go_test( srcs = [ "arguments_test.go", "cgroupdriver_test.go", + "chroot_test.go", "endpoint_test.go", "error_test.go", "marshal_test.go", diff --git a/cmd/kubeadm/app/util/chroot_test.go b/cmd/kubeadm/app/util/chroot_test.go new file mode 100644 index 00000000000..bb99f03c404 --- /dev/null +++ b/cmd/kubeadm/app/util/chroot_test.go @@ -0,0 +1,66 @@ +// +build !windows + +/* +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 util + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +// Can't just call Chroot() because it will affect other tests in the +// same process. Golang makes it hard to just call fork(), so instead +// we exec ourselves again, and run the test in the new subprocess. + +func testChrootReal(t *testing.T) { + testfile := filepath.FromSlash("/" + filepath.Base(os.Args[0])) + dir := filepath.Dir(os.Args[0]) + if dir == "." { + t.Skip("skipping: running test at root somehow") + } + if err := Chroot(dir); err != nil { + if strings.Contains(err.Error(), "operation not permitted") { + t.Skip("skipping: insufficient permissions to chroot") + } + t.Fatalf("chroot error: %v", err) + } + + // All file access should now be relative to `dir` + if _, err := os.Stat(testfile); err != nil { + t.Errorf("Expected file %q to exist, but got %v", testfile, err) + } +} + +func TestChroot(t *testing.T) { + if os.Getenv("GO_TEST_CHROOT_FOR_REALZ") == "1" { + testChrootReal(t) + return + } + + cmd := exec.Command(os.Args[0], "-test.v", "-test.run=TestChroot") + cmd.Env = []string{"GO_TEST_CHROOT_FOR_REALZ=1"} + + out, err := cmd.Output() + t.Logf("subprocess output:\n%s", out) + if err != nil { + t.Errorf("subprocess error: %v", err) + } +} diff --git a/cmd/kubeadm/app/util/chroot_unix.go b/cmd/kubeadm/app/util/chroot_unix.go new file mode 100644 index 00000000000..098184e5e55 --- /dev/null +++ b/cmd/kubeadm/app/util/chroot_unix.go @@ -0,0 +1,40 @@ +// +build !windows + +/* +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 util + +import ( + "fmt" + "os" + "path/filepath" + "syscall" +) + +// Chroot chroot()s to the new path. +// NB: All file paths after this call are effectively relative to +// `rootfs` +func Chroot(rootfs string) error { + if err := syscall.Chroot(rootfs); err != nil { + return fmt.Errorf("unable to chroot to %s: %v", rootfs, err) + } + root := filepath.FromSlash("/") + if err := os.Chdir(root); err != nil { + return fmt.Errorf("unable to chdir to %s: %v", root, err) + } + return nil +} diff --git a/cmd/kubeadm/app/util/chroot_windows.go b/cmd/kubeadm/app/util/chroot_windows.go new file mode 100644 index 00000000000..1ddf41dc98f --- /dev/null +++ b/cmd/kubeadm/app/util/chroot_windows.go @@ -0,0 +1,30 @@ +// +build windows + +/* +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 util + +import ( + "fmt" +) + +// Chroot chroot()s to the new path. +// NB: All file paths after this call are effectively relative to +// `rootfs` +func Chroot(rootfs string) error { + return fmt.Errorf("chroot is not implemented on Windows") +} From 7e7712449b3b31c934c656742b8a109aca9026f0 Mon Sep 17 00:00:00 2001 From: Angus Lees Date: Mon, 27 Aug 2018 12:33:55 +1000 Subject: [PATCH 2/2] Add "EXPERIMENTAL" to the option description --- cmd/kubeadm/app/cmd/cmd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/kubeadm/app/cmd/cmd.go b/cmd/kubeadm/app/cmd/cmd.go index cf3481aac4d..97660a3b862 100644 --- a/cmd/kubeadm/app/cmd/cmd.go +++ b/cmd/kubeadm/app/cmd/cmd.go @@ -108,6 +108,6 @@ func NewKubeadmCommand(in io.Reader, out, err io.Writer) *cobra.Command { func AddKubeadmOtherFlags(flagSet *pflag.FlagSet, rootfsPath *string) { flagSet.StringVar( rootfsPath, "rootfs", *rootfsPath, - "The path to the 'real' host root filesystem.", + "[EXPERIMENTAL] The path to the 'real' host root filesystem.", ) }