diff --git a/cmd/minikube/cmd/cp.go b/cmd/minikube/cmd/cp.go new file mode 100644 index 000000000000..dc641e069afd --- /dev/null +++ b/cmd/minikube/cmd/cp.go @@ -0,0 +1,93 @@ +/* +Copyright 2021 The Kubernetes Authors All rights reserved. + +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 + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "os" + pt "path" + "path/filepath" + + "k8s.io/minikube/pkg/minikube/assets" + "k8s.io/minikube/pkg/minikube/exit" + "k8s.io/minikube/pkg/minikube/mustload" + "k8s.io/minikube/pkg/minikube/out" + "k8s.io/minikube/pkg/minikube/reason" +) + +// placeholders for flag values +var ( + srcPath string + dstPath string +) + +// cpCmd represents the cp command, similar to docker cp +var cpCmd = &cobra.Command{ + Use: "cp ", + Short: "Copy the specified file into minikube", + Long: "Copy the specified file into minikube, it will be saved at path in your minikube.\n" + + "Example Command : \"minikube cp a.txt /home/docker/b.txt\"\n", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 2 { + exit.Message(reason.Usage, `Please specify the path to copy: + minikube cp (example: "minikube cp a/b.txt /copied.txt")`) + } + + srcPath = args[0] + dstPath = args[1] + validateArgs(srcPath, dstPath) + + co := mustload.Running(ClusterFlagValue()) + fa, err := assets.NewFileAsset(srcPath, pt.Dir(dstPath), pt.Base(dstPath), "0644") + if err != nil { + out.ErrLn("%v", errors.Wrap(err, "getting file asset")) + os.Exit(1) + } + + if err = co.CP.Runner.Copy(fa); err != nil { + out.ErrLn("%v", errors.Wrap(err, "copying file")) + os.Exit(1) + } + }, +} + +func init() { +} + +func validateArgs(srcPath string, dstPath string) { + if srcPath == "" { + exit.Message(reason.Usage, "Source {{.path}} can not be empty", out.V{"path": srcPath}) + } + + if dstPath == "" { + exit.Message(reason.Usage, "Target {{.path}} can not be empty", out.V{"path": dstPath}) + } + + if _, err := os.Stat(srcPath); err != nil { + if os.IsNotExist(err) { + exit.Message(reason.HostPathMissing, "Cannot find directory {{.path}} for copy", out.V{"path": srcPath}) + } else { + exit.Error(reason.HostPathStat, "stat failed", err) + } + } + + if !filepath.IsAbs(dstPath) { + exit.Message(reason.Usage, ` must be an absolute Path. Relative Path is not allowed (example: "/home/docker/copied.txt")`) + } +} diff --git a/cmd/minikube/cmd/root.go b/cmd/minikube/cmd/root.go index 590e523d60ad..405d40e0bf2f 100644 --- a/cmd/minikube/cmd/root.go +++ b/cmd/minikube/cmd/root.go @@ -243,6 +243,7 @@ func init() { sshCmd, kubectlCmd, nodeCmd, + cpCmd, }, }, { diff --git a/site/content/en/docs/commands/cp.md b/site/content/en/docs/commands/cp.md new file mode 100644 index 000000000000..b04ec3ab8ffc --- /dev/null +++ b/site/content/en/docs/commands/cp.md @@ -0,0 +1,43 @@ +--- +title: "cp" +description: > + Copy the specified file into minikube +--- + + +## minikube cp + +Copy the specified file into minikube + +### Synopsis + +Copy the specified file into minikube, it will be saved at path in your minikube. +Example Command : "minikube cp a.txt /home/docker/b.txt" + + +```shell +minikube cp [flags] +``` + +### Options inherited from parent commands + +``` + --add_dir_header If true, adds the file directory to the header of the log messages + --alsologtostderr log to standard error as well as files + -b, --bootstrapper string The name of the cluster bootstrapper that will set up the Kubernetes cluster. (default "kubeadm") + -h, --help + --log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0) + --log_dir string If non-empty, write log files in this directory + --log_file string If non-empty, use this log file + --log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800) + --logtostderr log to standard error instead of files + --one_output If true, only write logs to their native severity level (vs also writing to each lower severity level + -p, --profile string The name of the minikube VM being used. This can be set to allow having multiple instances of minikube independently. (default "minikube") + --skip_headers If true, avoid header prefixes in the log messages + --skip_log_headers If true, avoid headers when opening log files + --stderrthreshold severity logs at or above this threshold go to stderr (default 2) + --user string Specifies the user executing the operation. Useful for auditing operations executed by 3rd party tools. Defaults to the operating system username. + -v, --v Level number for the log level verbosity + --vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging +``` + diff --git a/test/integration/functional_test.go b/test/integration/functional_test.go index acd7570bfd8e..c21821419b43 100644 --- a/test/integration/functional_test.go +++ b/test/integration/functional_test.go @@ -123,6 +123,7 @@ func TestFunctional(t *testing.T) { {"PersistentVolumeClaim", validatePersistentVolumeClaim}, {"TunnelCmd", validateTunnelCmd}, {"SSHCmd", validateSSHCmd}, + {"CpCmd", validateCpCmd}, {"MySQL", validateMySQL}, {"FileSync", validateFileSync}, {"CertSync", validateCertSync}, @@ -1143,6 +1144,34 @@ func validateSSHCmd(ctx context.Context, t *testing.T, profile string) { } } +// validateCpCmd asserts basic "cp" command functionality +func validateCpCmd(ctx context.Context, t *testing.T, profile string) { + if NoneDriver() { + t.Skipf("skipping: cp is unsupported by none driver") + } + + cpPath := filepath.Join(*testdataDir, "cp-test.txt") + rr, err := Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "cp", cpPath, "/home/docker/hello_cp.txt")) + if ctx.Err() == context.DeadlineExceeded { + t.Errorf("failed to run command by deadline. exceeded timeout : %s", rr.Command()) + } + if err != nil { + t.Errorf("failed to run an cp command. args %q : %v", rr.Command(), err) + } + + expected := "Test file for checking file cp process" + rr, err = Run(t, exec.CommandContext(ctx, Target(), "-p", profile, "ssh", "cat /home/docker/hello_cp.txt")) + if ctx.Err() == context.DeadlineExceeded { + t.Errorf("failed to run command by deadline. exceeded timeout : %s", rr.Command()) + } + if err != nil { + t.Errorf("failed to run an cp command. args %q : %v", rr.Command(), err) + } + if diff := cmp.Diff(expected, rr.Stdout.String()); diff != "" { + t.Errorf("/testdata/cp-test.txt content mismatch (-want +got):\n%s", diff) + } +} + // validateMySQL validates a minimalist MySQL deployment func validateMySQL(ctx context.Context, t *testing.T, profile string) { if arm64Platform() { diff --git a/test/integration/testdata/cp-test.txt b/test/integration/testdata/cp-test.txt new file mode 100644 index 000000000000..dd81c405fd47 --- /dev/null +++ b/test/integration/testdata/cp-test.txt @@ -0,0 +1 @@ +Test file for checking file cp process \ No newline at end of file