2019-01-12 04:58:27 +00:00
/ *
Copyright 2016 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 cp
import (
"archive/tar"
2019-09-27 21:51:53 +00:00
"bytes"
2019-01-12 04:58:27 +00:00
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
2019-09-27 21:51:53 +00:00
"github.com/lithammer/dedent"
"github.com/spf13/cobra"
2019-01-12 04:58:27 +00:00
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
2019-09-27 21:51:53 +00:00
"k8s.io/kubectl/pkg/cmd/exec"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
2019-01-12 04:58:27 +00:00
)
var (
cpExample = templates . Examples ( i18n . T ( `
# ! ! ! Important Note ! ! !
# Requires that the ' tar ' binary is present in your container
# image . If ' tar ' is not present , ' kubectl cp ' will fail .
2019-09-27 21:51:53 +00:00
#
# For advanced use cases , such as symlinks , wildcard expansion or
# file mode preservation consider using ' kubectl exec ' .
# Copy / tmp / foo local file to / tmp / bar in a remote pod in namespace < some - namespace >
tar cf - / tmp / foo | kubectl exec - i - n < some - namespace > < some - pod > -- tar xf - - C / tmp / bar
# Copy / tmp / foo from a remote pod to / tmp / bar locally
kubectl exec - n < some - namespace > < some - pod > -- tar cf - / tmp / foo | tar xf - - C / tmp / bar
2019-01-12 04:58:27 +00:00
# Copy / tmp / foo_dir local directory to / tmp / bar_dir in a remote pod in the default namespace
kubectl cp / tmp / foo_dir < some - pod > : / tmp / bar_dir
# Copy / tmp / foo local file to / tmp / bar in a remote pod in a specific container
kubectl cp / tmp / foo < some - pod > : / tmp / bar - c < specific - container >
# Copy / tmp / foo local file to / tmp / bar in a remote pod in namespace < some - namespace >
kubectl cp / tmp / foo < some - namespace > / < some - pod > : / tmp / bar
# Copy / tmp / foo from a remote pod to / tmp / bar locally
kubectl cp < some - namespace > / < some - pod > : / tmp / foo / tmp / bar ` ) )
cpUsageStr = dedent . Dedent ( `
expected ' cp < file - spec - src > < file - spec - dest > [ - c container ] ' .
< file - spec > is :
[ namespace / ] pod - name : / file / path for a remote file
/ file / path for a local file ` )
)
2019-04-07 17:07:55 +00:00
// CopyOptions have the data required to perform the copy operation
2019-01-12 04:58:27 +00:00
type CopyOptions struct {
Container string
Namespace string
NoPreserve bool
2019-09-27 21:51:53 +00:00
ClientConfig * restclient . Config
Clientset kubernetes . Interface
ExecParentCmdName string
2019-01-12 04:58:27 +00:00
genericclioptions . IOStreams
}
2019-04-07 17:07:55 +00:00
// NewCopyOptions creates the options for copy
2019-01-12 04:58:27 +00:00
func NewCopyOptions ( ioStreams genericclioptions . IOStreams ) * CopyOptions {
return & CopyOptions {
IOStreams : ioStreams ,
}
}
// NewCmdCp creates a new Copy command.
func NewCmdCp ( f cmdutil . Factory , ioStreams genericclioptions . IOStreams ) * cobra . Command {
o := NewCopyOptions ( ioStreams )
cmd := & cobra . Command {
Use : "cp <file-spec-src> <file-spec-dest>" ,
DisableFlagsInUseLine : true ,
Short : i18n . T ( "Copy files and directories to and from containers." ) ,
2020-12-01 01:06:26 +00:00
Long : i18n . T ( "Copy files and directories to and from containers." ) ,
2019-01-12 04:58:27 +00:00
Example : cpExample ,
Run : func ( cmd * cobra . Command , args [ ] string ) {
cmdutil . CheckErr ( o . Complete ( f , cmd ) )
cmdutil . CheckErr ( o . Run ( args ) )
} ,
}
cmd . Flags ( ) . StringVarP ( & o . Container , "container" , "c" , o . Container , "Container name. If omitted, the first container in the pod will be chosen" )
cmd . Flags ( ) . BoolVarP ( & o . NoPreserve , "no-preserve" , "" , false , "The copied file/directory's ownership and permissions will not be preserved in the container" )
return cmd
}
type fileSpec struct {
PodNamespace string
PodName string
File string
}
var (
2019-12-12 01:27:03 +00:00
errFileSpecDoesntMatchFormat = errors . New ( "filespec must match the canonical format: [[namespace/]pod:]file/path" )
errFileCannotBeEmpty = errors . New ( "filepath can not be empty" )
2019-01-12 04:58:27 +00:00
)
func extractFileSpec ( arg string ) ( fileSpec , error ) {
2020-08-10 17:43:49 +00:00
i := strings . Index ( arg , ":" )
if i == - 1 {
2019-01-12 04:58:27 +00:00
return fileSpec { File : arg } , nil
2020-08-10 17:43:49 +00:00
}
// filespec starting with a semicolon is invalid
if i == 0 {
return fileSpec { } , errFileSpecDoesntMatchFormat
2019-01-12 04:58:27 +00:00
}
2020-08-10 17:43:49 +00:00
pod , file := arg [ : i ] , arg [ i + 1 : ]
pieces := strings . Split ( pod , "/" )
switch len ( pieces ) {
case 1 :
return fileSpec {
PodName : pieces [ 0 ] ,
File : file ,
} , nil
case 2 :
return fileSpec {
PodNamespace : pieces [ 0 ] ,
PodName : pieces [ 1 ] ,
File : file ,
} , nil
default :
return fileSpec { } , errFileSpecDoesntMatchFormat
}
2019-01-12 04:58:27 +00:00
}
2019-04-07 17:07:55 +00:00
// Complete completes all the required options
2019-01-12 04:58:27 +00:00
func ( o * CopyOptions ) Complete ( f cmdutil . Factory , cmd * cobra . Command ) error {
2019-09-27 21:51:53 +00:00
if cmd . Parent ( ) != nil {
o . ExecParentCmdName = cmd . Parent ( ) . CommandPath ( )
}
2019-01-12 04:58:27 +00:00
var err error
o . Namespace , _ , err = f . ToRawKubeConfigLoader ( ) . Namespace ( )
if err != nil {
return err
}
o . Clientset , err = f . KubernetesClientSet ( )
if err != nil {
return err
}
o . ClientConfig , err = f . ToRESTConfig ( )
if err != nil {
return err
}
return nil
}
2019-04-07 17:07:55 +00:00
// Validate makes sure provided values for CopyOptions are valid
2019-01-12 04:58:27 +00:00
func ( o * CopyOptions ) Validate ( cmd * cobra . Command , args [ ] string ) error {
if len ( args ) != 2 {
return cmdutil . UsageErrorf ( cmd , cpUsageStr )
}
return nil
}
2019-04-07 17:07:55 +00:00
// Run performs the execution
2019-01-12 04:58:27 +00:00
func ( o * CopyOptions ) Run ( args [ ] string ) error {
if len ( args ) < 2 {
return fmt . Errorf ( "source and destination are required" )
}
srcSpec , err := extractFileSpec ( args [ 0 ] )
if err != nil {
return err
}
destSpec , err := extractFileSpec ( args [ 1 ] )
if err != nil {
return err
}
if len ( srcSpec . PodName ) != 0 && len ( destSpec . PodName ) != 0 {
2020-12-01 01:06:26 +00:00
return fmt . Errorf ( "one of src or dest must be a local file specification" )
2019-01-12 04:58:27 +00:00
}
if len ( srcSpec . PodName ) != 0 {
return o . copyFromPod ( srcSpec , destSpec )
}
if len ( destSpec . PodName ) != 0 {
return o . copyToPod ( srcSpec , destSpec , & exec . ExecOptions { } )
}
return fmt . Errorf ( "one of src or dest must be a remote file specification" )
}
// checkDestinationIsDir receives a destination fileSpec and
// determines if the provided destination path exists on the
// pod. If the destination path does not exist or is _not_ a
// directory, an error is returned with the exit code received.
func ( o * CopyOptions ) checkDestinationIsDir ( dest fileSpec ) error {
options := & exec . ExecOptions {
StreamOptions : exec . StreamOptions {
IOStreams : genericclioptions . IOStreams {
Out : bytes . NewBuffer ( [ ] byte { } ) ,
ErrOut : bytes . NewBuffer ( [ ] byte { } ) ,
} ,
Namespace : dest . PodNamespace ,
PodName : dest . PodName ,
} ,
Command : [ ] string { "test" , "-d" , dest . File } ,
Executor : & exec . DefaultRemoteExecutor { } ,
}
return o . execute ( options )
}
func ( o * CopyOptions ) copyToPod ( src , dest fileSpec , options * exec . ExecOptions ) error {
if len ( src . File ) == 0 || len ( dest . File ) == 0 {
return errFileCannotBeEmpty
}
2020-12-01 01:06:26 +00:00
if _ , err := os . Stat ( src . File ) ; err != nil {
return fmt . Errorf ( "%s doesn't exist in local filesystem" , src . File )
}
2019-01-12 04:58:27 +00:00
reader , writer := io . Pipe ( )
// strip trailing slash (if any)
if dest . File != "/" && strings . HasSuffix ( string ( dest . File [ len ( dest . File ) - 1 ] ) , "/" ) {
dest . File = dest . File [ : len ( dest . File ) - 1 ]
}
if err := o . checkDestinationIsDir ( dest ) ; err == nil {
// If no error, dest.File was found to be a directory.
// Copy specified src into it
dest . File = dest . File + "/" + path . Base ( src . File )
}
go func ( ) {
defer writer . Close ( )
err := makeTar ( src . File , dest . File , writer )
cmdutil . CheckErr ( err )
} ( )
var cmdArr [ ] string
// TODO: Improve error messages by first testing if 'tar' is present in the container?
if o . NoPreserve {
2019-09-27 21:51:53 +00:00
cmdArr = [ ] string { "tar" , "--no-same-permissions" , "--no-same-owner" , "-xmf" , "-" }
2019-01-12 04:58:27 +00:00
} else {
2019-09-27 21:51:53 +00:00
cmdArr = [ ] string { "tar" , "-xmf" , "-" }
2019-01-12 04:58:27 +00:00
}
destDir := path . Dir ( dest . File )
if len ( destDir ) > 0 {
cmdArr = append ( cmdArr , "-C" , destDir )
}
options . StreamOptions = exec . StreamOptions {
IOStreams : genericclioptions . IOStreams {
In : reader ,
Out : o . Out ,
ErrOut : o . ErrOut ,
} ,
Stdin : true ,
Namespace : dest . PodNamespace ,
PodName : dest . PodName ,
}
options . Command = cmdArr
options . Executor = & exec . DefaultRemoteExecutor { }
return o . execute ( options )
}
func ( o * CopyOptions ) copyFromPod ( src , dest fileSpec ) error {
if len ( src . File ) == 0 || len ( dest . File ) == 0 {
return errFileCannotBeEmpty
}
reader , outStream := io . Pipe ( )
options := & exec . ExecOptions {
StreamOptions : exec . StreamOptions {
IOStreams : genericclioptions . IOStreams {
In : nil ,
Out : outStream ,
ErrOut : o . Out ,
} ,
Namespace : src . PodNamespace ,
PodName : src . PodName ,
} ,
// TODO: Improve error messages by first testing if 'tar' is present in the container?
Command : [ ] string { "tar" , "cf" , "-" , src . File } ,
Executor : & exec . DefaultRemoteExecutor { } ,
}
go func ( ) {
defer outStream . Close ( )
2019-08-30 18:33:25 +00:00
err := o . execute ( options )
cmdutil . CheckErr ( err )
2019-01-12 04:58:27 +00:00
} ( )
prefix := getPrefix ( src . File )
prefix = path . Clean ( prefix )
// remove extraneous path shortcuts - these could occur if a path contained extra "../"
// and attempted to navigate beyond "/" in a remote filesystem
prefix = stripPathShortcuts ( prefix )
2019-09-27 21:51:53 +00:00
return o . untarAll ( src , reader , dest . File , prefix )
2019-01-12 04:58:27 +00:00
}
// stripPathShortcuts removes any leading or trailing "../" from a given path
func stripPathShortcuts ( p string ) string {
newPath := path . Clean ( p )
trimmed := strings . TrimPrefix ( newPath , "../" )
for trimmed != newPath {
newPath = trimmed
trimmed = strings . TrimPrefix ( newPath , "../" )
}
2019-04-07 17:07:55 +00:00
// trim leftover {".", ".."}
if newPath == "." || newPath == ".." {
2019-01-12 04:58:27 +00:00
newPath = ""
}
if len ( newPath ) > 0 && string ( newPath [ 0 ] ) == "/" {
return newPath [ 1 : ]
}
return newPath
}
func makeTar ( srcPath , destPath string , writer io . Writer ) error {
// TODO: use compression here?
tarWriter := tar . NewWriter ( writer )
defer tarWriter . Close ( )
srcPath = path . Clean ( srcPath )
destPath = path . Clean ( destPath )
return recursiveTar ( path . Dir ( srcPath ) , path . Base ( srcPath ) , path . Dir ( destPath ) , path . Base ( destPath ) , tarWriter )
}
func recursiveTar ( srcBase , srcFile , destBase , destFile string , tw * tar . Writer ) error {
2019-04-07 17:07:55 +00:00
srcPath := path . Join ( srcBase , srcFile )
matchedPaths , err := filepath . Glob ( srcPath )
2019-01-12 04:58:27 +00:00
if err != nil {
return err
}
2019-04-07 17:07:55 +00:00
for _ , fpath := range matchedPaths {
stat , err := os . Lstat ( fpath )
2019-01-12 04:58:27 +00:00
if err != nil {
return err
}
2019-04-07 17:07:55 +00:00
if stat . IsDir ( ) {
files , err := ioutil . ReadDir ( fpath )
if err != nil {
return err
}
if len ( files ) == 0 {
//case empty directory
hdr , _ := tar . FileInfoHeader ( stat , fpath )
hdr . Name = destFile
if err := tw . WriteHeader ( hdr ) ; err != nil {
return err
}
}
for _ , f := range files {
if err := recursiveTar ( srcBase , path . Join ( srcFile , f . Name ( ) ) , destBase , path . Join ( destFile , f . Name ( ) ) , tw ) ; err != nil {
return err
}
}
return nil
} else if stat . Mode ( ) & os . ModeSymlink != 0 {
//case soft link
hdr , _ := tar . FileInfoHeader ( stat , fpath )
target , err := os . Readlink ( fpath )
if err != nil {
return err
}
hdr . Linkname = target
2019-01-12 04:58:27 +00:00
hdr . Name = destFile
if err := tw . WriteHeader ( hdr ) ; err != nil {
return err
}
2019-04-07 17:07:55 +00:00
} else {
//case regular file or other file type like pipe
hdr , err := tar . FileInfoHeader ( stat , fpath )
if err != nil {
2019-01-12 04:58:27 +00:00
return err
}
2019-04-07 17:07:55 +00:00
hdr . Name = destFile
2019-01-12 04:58:27 +00:00
2019-04-07 17:07:55 +00:00
if err := tw . WriteHeader ( hdr ) ; err != nil {
return err
}
2019-01-12 04:58:27 +00:00
2019-04-07 17:07:55 +00:00
f , err := os . Open ( fpath )
if err != nil {
return err
}
defer f . Close ( )
2019-01-12 04:58:27 +00:00
2019-04-07 17:07:55 +00:00
if _ , err := io . Copy ( tw , f ) ; err != nil {
return err
}
return f . Close ( )
2019-01-12 04:58:27 +00:00
}
}
return nil
}
2019-09-27 21:51:53 +00:00
func ( o * CopyOptions ) untarAll ( src fileSpec , reader io . Reader , destDir , prefix string ) error {
symlinkWarningPrinted := false
2019-01-12 04:58:27 +00:00
// TODO: use compression here?
tarReader := tar . NewReader ( reader )
for {
header , err := tarReader . Next ( )
if err != nil {
if err != io . EOF {
return err
}
break
}
2019-06-12 21:00:25 +00:00
// All the files will start with the prefix, which is the directory where
2019-03-29 00:03:05 +00:00
// they were located on the pod, we need to strip down that prefix, but
2019-06-12 21:00:25 +00:00
// if the prefix is missing it means the tar was tempered with.
// For the case where prefix is empty we need to ensure that the path
// is not absolute, which also indicates the tar file was tempered with.
2019-03-29 00:03:05 +00:00
if ! strings . HasPrefix ( header . Name , prefix ) {
return fmt . Errorf ( "tar contents corrupted" )
}
2019-06-12 21:00:25 +00:00
// basic file information
mode := header . FileInfo ( ) . Mode ( )
2019-08-05 18:39:53 +00:00
destFileName := filepath . Join ( destDir , header . Name [ len ( prefix ) : ] )
2019-06-12 21:00:25 +00:00
2019-08-05 18:39:53 +00:00
if ! isDestRelative ( destDir , destFileName ) {
fmt . Fprintf ( o . IOStreams . ErrOut , "warning: file %q is outside target destination, skipping\n" , destFileName )
continue
}
baseName := filepath . Dir ( destFileName )
2019-01-12 04:58:27 +00:00
if err := os . MkdirAll ( baseName , 0755 ) ; err != nil {
return err
}
if header . FileInfo ( ) . IsDir ( ) {
2019-06-12 21:00:25 +00:00
if err := os . MkdirAll ( destFileName , 0755 ) ; err != nil {
2019-01-12 04:58:27 +00:00
return err
}
continue
}
2019-09-27 21:51:53 +00:00
if mode & os . ModeSymlink != 0 {
if ! symlinkWarningPrinted && len ( o . ExecParentCmdName ) > 0 {
2019-12-12 01:27:03 +00:00
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 )
2019-09-27 21:51:53 +00:00
symlinkWarningPrinted = true
continue
}
fmt . Fprintf ( o . IOStreams . ErrOut , "warning: skipping symlink: %q -> %q\n" , destFileName , header . Linkname )
continue
}
outFile , err := os . Create ( destFileName )
2019-06-12 21:00:25 +00:00
if err != nil {
return err
}
2019-09-27 21:51:53 +00:00
defer outFile . Close ( )
if _ , err := io . Copy ( outFile , tarReader ) ; err != nil {
return err
2019-01-12 04:58:27 +00:00
}
2019-09-27 21:51:53 +00:00
if err := outFile . Close ( ) ; err != nil {
2019-09-19 23:17:14 +00:00
return err
}
}
2019-01-12 04:58:27 +00:00
return nil
}
2019-06-12 21:00:25 +00:00
// isDestRelative returns true if dest is pointing outside the base directory,
// false otherwise.
func isDestRelative ( base , dest string ) bool {
2019-08-05 18:39:53 +00:00
relative , err := filepath . Rel ( base , dest )
2019-06-12 21:00:25 +00:00
if err != nil {
return false
}
return relative == "." || relative == stripPathShortcuts ( relative )
}
2019-01-12 04:58:27 +00:00
func getPrefix ( file string ) string {
// tar strips the leading '/' if it's there, so we will too
return strings . TrimLeft ( file , "/" )
}
func ( o * CopyOptions ) execute ( options * exec . ExecOptions ) error {
if len ( options . Namespace ) == 0 {
options . Namespace = o . Namespace
}
if len ( o . Container ) > 0 {
options . ContainerName = o . Container
}
options . Config = o . ClientConfig
2019-04-07 17:07:55 +00:00
options . PodClient = o . Clientset . CoreV1 ( )
2019-01-12 04:58:27 +00:00
if err := options . Validate ( ) ; err != nil {
return err
}
if err := options . Run ( ) ; err != nil {
return err
}
return nil
}