1/* 2Copyright 2016 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package cp 18 19import ( 20 "archive/tar" 21 "bytes" 22 "errors" 23 "fmt" 24 "io" 25 "io/ioutil" 26 "os" 27 "path" 28 "path/filepath" 29 "strings" 30 31 "github.com/lithammer/dedent" 32 "github.com/spf13/cobra" 33 34 "k8s.io/cli-runtime/pkg/genericclioptions" 35 "k8s.io/client-go/kubernetes" 36 restclient "k8s.io/client-go/rest" 37 "k8s.io/kubectl/pkg/cmd/exec" 38 "k8s.io/kubectl/pkg/cmd/get" 39 cmdutil "k8s.io/kubectl/pkg/cmd/util" 40 "k8s.io/kubectl/pkg/util/i18n" 41 "k8s.io/kubectl/pkg/util/templates" 42) 43 44var ( 45 cpExample = templates.Examples(i18n.T(` 46 # !!!Important Note!!! 47 # Requires that the 'tar' binary is present in your container 48 # image. If 'tar' is not present, 'kubectl cp' will fail. 49 # 50 # For advanced use cases, such as symlinks, wildcard expansion or 51 # file mode preservation, consider using 'kubectl exec'. 52 53 # Copy /tmp/foo local file to /tmp/bar in a remote pod in namespace <some-namespace> 54 tar cf - /tmp/foo | kubectl exec -i -n <some-namespace> <some-pod> -- tar xf - -C /tmp/bar 55 56 # Copy /tmp/foo from a remote pod to /tmp/bar locally 57 kubectl exec -n <some-namespace> <some-pod> -- tar cf - /tmp/foo | tar xf - -C /tmp/bar 58 59 # Copy /tmp/foo_dir local directory to /tmp/bar_dir in a remote pod in the default namespace 60 kubectl cp /tmp/foo_dir <some-pod>:/tmp/bar_dir 61 62 # Copy /tmp/foo local file to /tmp/bar in a remote pod in a specific container 63 kubectl cp /tmp/foo <some-pod>:/tmp/bar -c <specific-container> 64 65 # Copy /tmp/foo local file to /tmp/bar in a remote pod in namespace <some-namespace> 66 kubectl cp /tmp/foo <some-namespace>/<some-pod>:/tmp/bar 67 68 # Copy /tmp/foo from a remote pod to /tmp/bar locally 69 kubectl cp <some-namespace>/<some-pod>:/tmp/foo /tmp/bar`)) 70 71 cpUsageStr = dedent.Dedent(` 72 expected 'cp <file-spec-src> <file-spec-dest> [-c container]'. 73 <file-spec> is: 74 [namespace/]pod-name:/file/path for a remote file 75 /file/path for a local file`) 76) 77 78// CopyOptions have the data required to perform the copy operation 79type CopyOptions struct { 80 Container string 81 Namespace string 82 NoPreserve bool 83 84 ClientConfig *restclient.Config 85 Clientset kubernetes.Interface 86 ExecParentCmdName string 87 88 genericclioptions.IOStreams 89} 90 91// NewCopyOptions creates the options for copy 92func NewCopyOptions(ioStreams genericclioptions.IOStreams) *CopyOptions { 93 return &CopyOptions{ 94 IOStreams: ioStreams, 95 } 96} 97 98// NewCmdCp creates a new Copy command. 99func NewCmdCp(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { 100 o := NewCopyOptions(ioStreams) 101 102 cmd := &cobra.Command{ 103 Use: "cp <file-spec-src> <file-spec-dest>", 104 DisableFlagsInUseLine: true, 105 Short: i18n.T("Copy files and directories to and from containers"), 106 Long: i18n.T("Copy files and directories to and from containers."), 107 Example: cpExample, 108 ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 109 var comps []string 110 if len(args) == 0 { 111 if strings.IndexAny(toComplete, "/.~") == 0 { 112 // Looks like a path, do nothing 113 } else if strings.Index(toComplete, ":") != -1 { 114 // TODO: complete remote files in the pod 115 } else if idx := strings.Index(toComplete, "/"); idx > 0 { 116 // complete <namespace>/<pod> 117 namespace := toComplete[:idx] 118 template := "{{ range .items }}{{ .metadata.namespace }}/{{ .metadata.name }}: {{ end }}" 119 comps = get.CompGetFromTemplate(&template, f, namespace, cmd, []string{"pod"}, toComplete) 120 } else { 121 // Complete namespaces followed by a / 122 for _, ns := range get.CompGetResource(f, cmd, "namespace", toComplete) { 123 comps = append(comps, fmt.Sprintf("%s/", ns)) 124 } 125 // Complete pod names followed by a : 126 for _, pod := range get.CompGetResource(f, cmd, "pod", toComplete) { 127 comps = append(comps, fmt.Sprintf("%s:", pod)) 128 } 129 130 // Finally, provide file completion if we need to. 131 // We only do this if: 132 // 1- There are other completions found (if there are no completions, 133 // the shell will do file completion itself) 134 // 2- If there is some input from the user (or else we will end up 135 // listing the entire content of the current directory which could 136 // be too many choices for the user) 137 if len(comps) > 0 && len(toComplete) > 0 { 138 if files, err := ioutil.ReadDir("."); err == nil { 139 for _, file := range files { 140 filename := file.Name() 141 if strings.HasPrefix(filename, toComplete) { 142 if file.IsDir() { 143 filename = fmt.Sprintf("%s/", filename) 144 } 145 // We are completing a file prefix 146 comps = append(comps, filename) 147 } 148 } 149 } 150 } else if len(toComplete) == 0 { 151 // If the user didn't provide any input to complete, 152 // we provide a hint that a path can also be used 153 comps = append(comps, "./", "/") 154 } 155 } 156 } 157 return comps, cobra.ShellCompDirectiveNoSpace 158 }, 159 Run: func(cmd *cobra.Command, args []string) { 160 cmdutil.CheckErr(o.Complete(f, cmd)) 161 cmdutil.CheckErr(o.Run(args)) 162 }, 163 } 164 cmdutil.AddContainerVarFlags(cmd, &o.Container, o.Container) 165 cmd.Flags().BoolVarP(&o.NoPreserve, "no-preserve", "", false, "The copied file/directory's ownership and permissions will not be preserved in the container") 166 167 return cmd 168} 169 170type fileSpec struct { 171 PodNamespace string 172 PodName string 173 File string 174} 175 176var ( 177 errFileSpecDoesntMatchFormat = errors.New("filespec must match the canonical format: [[namespace/]pod:]file/path") 178 errFileCannotBeEmpty = errors.New("filepath can not be empty") 179) 180 181func extractFileSpec(arg string) (fileSpec, error) { 182 i := strings.Index(arg, ":") 183 184 if i == -1 { 185 return fileSpec{File: arg}, nil 186 } 187 // filespec starting with a semicolon is invalid 188 if i == 0 { 189 return fileSpec{}, errFileSpecDoesntMatchFormat 190 } 191 192 pod, file := arg[:i], arg[i+1:] 193 pieces := strings.Split(pod, "/") 194 switch len(pieces) { 195 case 1: 196 return fileSpec{ 197 PodName: pieces[0], 198 File: file, 199 }, nil 200 case 2: 201 return fileSpec{ 202 PodNamespace: pieces[0], 203 PodName: pieces[1], 204 File: file, 205 }, nil 206 default: 207 return fileSpec{}, errFileSpecDoesntMatchFormat 208 } 209} 210 211// Complete completes all the required options 212func (o *CopyOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) error { 213 if cmd.Parent() != nil { 214 o.ExecParentCmdName = cmd.Parent().CommandPath() 215 } 216 217 var err error 218 o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() 219 if err != nil { 220 return err 221 } 222 223 o.Clientset, err = f.KubernetesClientSet() 224 if err != nil { 225 return err 226 } 227 228 o.ClientConfig, err = f.ToRESTConfig() 229 if err != nil { 230 return err 231 } 232 return nil 233} 234 235// Validate makes sure provided values for CopyOptions are valid 236func (o *CopyOptions) Validate(cmd *cobra.Command, args []string) error { 237 if len(args) != 2 { 238 return cmdutil.UsageErrorf(cmd, cpUsageStr) 239 } 240 return nil 241} 242 243// Run performs the execution 244func (o *CopyOptions) Run(args []string) error { 245 if len(args) < 2 { 246 return fmt.Errorf("source and destination are required") 247 } 248 srcSpec, err := extractFileSpec(args[0]) 249 if err != nil { 250 return err 251 } 252 destSpec, err := extractFileSpec(args[1]) 253 if err != nil { 254 return err 255 } 256 257 if len(srcSpec.PodName) != 0 && len(destSpec.PodName) != 0 { 258 return fmt.Errorf("one of src or dest must be a local file specification") 259 } 260 261 if len(srcSpec.PodName) != 0 { 262 return o.copyFromPod(srcSpec, destSpec) 263 } 264 if len(destSpec.PodName) != 0 { 265 return o.copyToPod(srcSpec, destSpec, &exec.ExecOptions{}) 266 } 267 return fmt.Errorf("one of src or dest must be a remote file specification") 268} 269 270// checkDestinationIsDir receives a destination fileSpec and 271// determines if the provided destination path exists on the 272// pod. If the destination path does not exist or is _not_ a 273// directory, an error is returned with the exit code received. 274func (o *CopyOptions) checkDestinationIsDir(dest fileSpec) error { 275 options := &exec.ExecOptions{ 276 StreamOptions: exec.StreamOptions{ 277 IOStreams: genericclioptions.IOStreams{ 278 Out: bytes.NewBuffer([]byte{}), 279 ErrOut: bytes.NewBuffer([]byte{}), 280 }, 281 282 Namespace: dest.PodNamespace, 283 PodName: dest.PodName, 284 }, 285 286 Command: []string{"test", "-d", dest.File}, 287 Executor: &exec.DefaultRemoteExecutor{}, 288 } 289 290 return o.execute(options) 291} 292 293func (o *CopyOptions) copyToPod(src, dest fileSpec, options *exec.ExecOptions) error { 294 if len(src.File) == 0 || len(dest.File) == 0 { 295 return errFileCannotBeEmpty 296 } 297 if _, err := os.Stat(src.File); err != nil { 298 return fmt.Errorf("%s doesn't exist in local filesystem", src.File) 299 } 300 reader, writer := io.Pipe() 301 302 // strip trailing slash (if any) 303 if dest.File != "/" && strings.HasSuffix(string(dest.File[len(dest.File)-1]), "/") { 304 dest.File = dest.File[:len(dest.File)-1] 305 } 306 307 if err := o.checkDestinationIsDir(dest); err == nil { 308 // If no error, dest.File was found to be a directory. 309 // Copy specified src into it 310 dest.File = dest.File + "/" + path.Base(src.File) 311 } 312 313 go func() { 314 defer writer.Close() 315 cmdutil.CheckErr(makeTar(src.File, dest.File, writer)) 316 }() 317 var cmdArr []string 318 319 // TODO: Improve error messages by first testing if 'tar' is present in the container? 320 if o.NoPreserve { 321 cmdArr = []string{"tar", "--no-same-permissions", "--no-same-owner", "-xmf", "-"} 322 } else { 323 cmdArr = []string{"tar", "-xmf", "-"} 324 } 325 destDir := path.Dir(dest.File) 326 if len(destDir) > 0 { 327 cmdArr = append(cmdArr, "-C", destDir) 328 } 329 330 options.StreamOptions = exec.StreamOptions{ 331 IOStreams: genericclioptions.IOStreams{ 332 In: reader, 333 Out: o.Out, 334 ErrOut: o.ErrOut, 335 }, 336 Stdin: true, 337 338 Namespace: dest.PodNamespace, 339 PodName: dest.PodName, 340 } 341 342 options.Command = cmdArr 343 options.Executor = &exec.DefaultRemoteExecutor{} 344 return o.execute(options) 345} 346 347func (o *CopyOptions) copyFromPod(src, dest fileSpec) error { 348 if len(src.File) == 0 || len(dest.File) == 0 { 349 return errFileCannotBeEmpty 350 } 351 352 reader, outStream := io.Pipe() 353 options := &exec.ExecOptions{ 354 StreamOptions: exec.StreamOptions{ 355 IOStreams: genericclioptions.IOStreams{ 356 In: nil, 357 Out: outStream, 358 ErrOut: o.Out, 359 }, 360 361 Namespace: src.PodNamespace, 362 PodName: src.PodName, 363 }, 364 365 // TODO: Improve error messages by first testing if 'tar' is present in the container? 366 Command: []string{"tar", "cf", "-", src.File}, 367 Executor: &exec.DefaultRemoteExecutor{}, 368 } 369 370 go func() { 371 defer outStream.Close() 372 cmdutil.CheckErr(o.execute(options)) 373 }() 374 prefix := getPrefix(src.File) 375 prefix = path.Clean(prefix) 376 // remove extraneous path shortcuts - these could occur if a path contained extra "../" 377 // and attempted to navigate beyond "/" in a remote filesystem 378 prefix = stripPathShortcuts(prefix) 379 return o.untarAll(src, reader, dest.File, prefix) 380} 381 382// stripPathShortcuts removes any leading or trailing "../" from a given path 383func stripPathShortcuts(p string) string { 384 newPath := path.Clean(p) 385 trimmed := strings.TrimPrefix(newPath, "../") 386 387 for trimmed != newPath { 388 newPath = trimmed 389 trimmed = strings.TrimPrefix(newPath, "../") 390 } 391 392 // trim leftover {".", ".."} 393 if newPath == "." || newPath == ".." { 394 newPath = "" 395 } 396 397 if len(newPath) > 0 && string(newPath[0]) == "/" { 398 return newPath[1:] 399 } 400 401 return newPath 402} 403 404func makeTar(srcPath, destPath string, writer io.Writer) error { 405 // TODO: use compression here? 406 tarWriter := tar.NewWriter(writer) 407 defer tarWriter.Close() 408 409 srcPath = path.Clean(srcPath) 410 destPath = path.Clean(destPath) 411 return recursiveTar(path.Dir(srcPath), path.Base(srcPath), path.Dir(destPath), path.Base(destPath), tarWriter) 412} 413 414func recursiveTar(srcBase, srcFile, destBase, destFile string, tw *tar.Writer) error { 415 srcPath := path.Join(srcBase, srcFile) 416 matchedPaths, err := filepath.Glob(srcPath) 417 if err != nil { 418 return err 419 } 420 for _, fpath := range matchedPaths { 421 stat, err := os.Lstat(fpath) 422 if err != nil { 423 return err 424 } 425 if stat.IsDir() { 426 files, err := ioutil.ReadDir(fpath) 427 if err != nil { 428 return err 429 } 430 if len(files) == 0 { 431 //case empty directory 432 hdr, _ := tar.FileInfoHeader(stat, fpath) 433 hdr.Name = destFile 434 if err := tw.WriteHeader(hdr); err != nil { 435 return err 436 } 437 } 438 for _, f := range files { 439 if err := recursiveTar(srcBase, path.Join(srcFile, f.Name()), destBase, path.Join(destFile, f.Name()), tw); err != nil { 440 return err 441 } 442 } 443 return nil 444 } else if stat.Mode()&os.ModeSymlink != 0 { 445 //case soft link 446 hdr, _ := tar.FileInfoHeader(stat, fpath) 447 target, err := os.Readlink(fpath) 448 if err != nil { 449 return err 450 } 451 452 hdr.Linkname = target 453 hdr.Name = destFile 454 if err := tw.WriteHeader(hdr); err != nil { 455 return err 456 } 457 } else { 458 //case regular file or other file type like pipe 459 hdr, err := tar.FileInfoHeader(stat, fpath) 460 if err != nil { 461 return err 462 } 463 hdr.Name = destFile 464 465 if err := tw.WriteHeader(hdr); err != nil { 466 return err 467 } 468 469 f, err := os.Open(fpath) 470 if err != nil { 471 return err 472 } 473 defer f.Close() 474 475 if _, err := io.Copy(tw, f); err != nil { 476 return err 477 } 478 return f.Close() 479 } 480 } 481 return nil 482} 483 484func (o *CopyOptions) untarAll(src fileSpec, reader io.Reader, destDir, prefix string) error { 485 symlinkWarningPrinted := false 486 // TODO: use compression here? 487 tarReader := tar.NewReader(reader) 488 for { 489 header, err := tarReader.Next() 490 if err != nil { 491 if err != io.EOF { 492 return err 493 } 494 break 495 } 496 497 // All the files will start with the prefix, which is the directory where 498 // they were located on the pod, we need to strip down that prefix, but 499 // if the prefix is missing it means the tar was tempered with. 500 // For the case where prefix is empty we need to ensure that the path 501 // is not absolute, which also indicates the tar file was tempered with. 502 if !strings.HasPrefix(header.Name, prefix) { 503 return fmt.Errorf("tar contents corrupted") 504 } 505 506 // basic file information 507 mode := header.FileInfo().Mode() 508 destFileName := filepath.Join(destDir, header.Name[len(prefix):]) 509 510 if !isDestRelative(destDir, destFileName) { 511 fmt.Fprintf(o.IOStreams.ErrOut, "warning: file %q is outside target destination, skipping\n", destFileName) 512 continue 513 } 514 515 baseName := filepath.Dir(destFileName) 516 if err := os.MkdirAll(baseName, 0755); err != nil { 517 return err 518 } 519 if header.FileInfo().IsDir() { 520 if err := os.MkdirAll(destFileName, 0755); err != nil { 521 return err 522 } 523 continue 524 } 525 526 if mode&os.ModeSymlink != 0 { 527 if !symlinkWarningPrinted && len(o.ExecParentCmdName) > 0 { 528 fmt.Fprintf(o.IOStreams.ErrOut, "warning: skipping symlink: %q -> %q (consider using \"%s exec -n %q %q -- tar cf - %q | tar xf -\")\n", destFileName, header.Linkname, o.ExecParentCmdName, src.PodNamespace, src.PodName, src.File) 529 symlinkWarningPrinted = true 530 continue 531 } 532 fmt.Fprintf(o.IOStreams.ErrOut, "warning: skipping symlink: %q -> %q\n", destFileName, header.Linkname) 533 continue 534 } 535 outFile, err := os.Create(destFileName) 536 if err != nil { 537 return err 538 } 539 defer outFile.Close() 540 if _, err := io.Copy(outFile, tarReader); err != nil { 541 return err 542 } 543 if err := outFile.Close(); err != nil { 544 return err 545 } 546 } 547 548 return nil 549} 550 551// isDestRelative returns true if dest is pointing outside the base directory, 552// false otherwise. 553func isDestRelative(base, dest string) bool { 554 relative, err := filepath.Rel(base, dest) 555 if err != nil { 556 return false 557 } 558 return relative == "." || relative == stripPathShortcuts(relative) 559} 560 561func getPrefix(file string) string { 562 // tar strips the leading '/' if it's there, so we will too 563 return strings.TrimLeft(file, "/") 564} 565 566func (o *CopyOptions) execute(options *exec.ExecOptions) error { 567 if len(options.Namespace) == 0 { 568 options.Namespace = o.Namespace 569 } 570 571 if len(o.Container) > 0 { 572 options.ContainerName = o.Container 573 } 574 575 options.Config = o.ClientConfig 576 options.PodClient = o.Clientset.CoreV1() 577 578 if err := options.Validate(); err != nil { 579 return err 580 } 581 582 if err := options.Run(); err != nil { 583 return err 584 } 585 return nil 586} 587