diff --git a/pkg/minikube/assets/vm_assets.go b/pkg/minikube/assets/vm_assets.go index fbc405e19704..88c92089fcf3 100644 --- a/pkg/minikube/assets/vm_assets.go +++ b/pkg/minikube/assets/vm_assets.go @@ -23,6 +23,7 @@ import ( "io" "os" "path" + "time" "github.com/golang/glog" "github.com/pkg/errors" @@ -36,6 +37,7 @@ type CopyableFile interface { GetTargetDir() string GetTargetName() string GetPermissions() string + GetModTime() (time.Time, error) } // BaseAsset is the base asset class @@ -66,6 +68,11 @@ func (b *BaseAsset) GetPermissions() string { return b.Permissions } +// GetModTime returns mod time +func (b *BaseAsset) GetModTime() (time.Time, error) { + return time.Time{}, nil +} + // FileAsset is an asset using a file type FileAsset struct { BaseAsset @@ -104,6 +111,12 @@ func (f *FileAsset) GetLength() (flen int) { return int(fi.Size()) } +// GetModTime returns modification time of the file +func (f *FileAsset) GetModTime() (time.Time, error) { + fi, err := os.Stat(f.AssetName) + return fi.ModTime(), err +} + func (f *FileAsset) Read(p []byte) (int, error) { if f.reader == nil { return 0, errors.New("Error attempting FileAsset.Read, FileAsset.reader uninitialized") diff --git a/pkg/minikube/command/ssh_runner.go b/pkg/minikube/command/ssh_runner.go index a341afb0498c..fe4bc41046e1 100644 --- a/pkg/minikube/command/ssh_runner.go +++ b/pkg/minikube/command/ssh_runner.go @@ -23,6 +23,8 @@ import ( "io" "os/exec" "path" + "strconv" + "strings" "sync" "time" @@ -35,6 +37,10 @@ import ( "k8s.io/minikube/pkg/util" ) +var ( + layout = "2006-01-02 15:04:05.999999999 -0700" +) + // SSHRunner runs commands through SSH. // // It implements the CommandRunner interface. @@ -143,6 +149,15 @@ func (s *SSHRunner) RunCmd(cmd *exec.Cmd) (*RunResult, error) { // Copy copies a file to the remote over SSH. func (s *SSHRunner) Copy(f assets.CopyableFile) error { + dst := path.Join(path.Join(f.GetTargetDir(), f.GetTargetName())) + exists, err := s.sameFileExists(f, dst) + if err != nil { + glog.Infof("Checked if %s exists, but got error: %v", dst, err) + } + if exists { + glog.Infof("Skipping copying %s as it already exists", dst) + return nil + } sess, err := s.c.NewSession() if err != nil { return errors.Wrap(err, "NewSession") @@ -156,7 +171,6 @@ func (s *SSHRunner) Copy(f assets.CopyableFile) error { // StdinPipe is closed. But let's use errgroup to make it explicit. var g errgroup.Group var copied int64 - dst := path.Join(path.Join(f.GetTargetDir(), f.GetTargetName())) glog.Infof("Transferring %d bytes to %s", f.GetLength(), dst) g.Go(func() error { @@ -182,6 +196,12 @@ func (s *SSHRunner) Copy(f assets.CopyableFile) error { }) scp := fmt.Sprintf("sudo mkdir -p %s && sudo scp -t %s", f.GetTargetDir(), f.GetTargetDir()) + mtime, err := f.GetModTime() + if err != nil { + glog.Infof("error getting modtime for %s: %v", dst, err) + } else { + scp += fmt.Sprintf(" && sudo touch -d \"%s\" %s", mtime.Format(layout), dst) + } out, err := sess.CombinedOutput(scp) if err != nil { return fmt.Errorf("%s: %s\noutput: %s", scp, err, out) @@ -189,6 +209,46 @@ func (s *SSHRunner) Copy(f assets.CopyableFile) error { return g.Wait() } +func (s *SSHRunner) sameFileExists(f assets.CopyableFile, dst string) (bool, error) { + // get file size and modtime of the source + srcSize := f.GetLength() + srcModTime, err := f.GetModTime() + if err != nil { + return false, err + } + if srcModTime.IsZero() { + return false, nil + } + + // get file size and modtime of the destination + sess, err := s.c.NewSession() + if err != nil { + return false, err + } + + cmd := "stat -c \"%s %y\" " + dst + out, err := sess.CombinedOutput(cmd) + if err != nil { + return false, err + } + outputs := strings.SplitN(strings.Trim(string(out), "\n"), " ", 2) + + dstSize, err := strconv.Atoi(outputs[0]) + if err != nil { + return false, err + } + dstModTime, err := time.Parse(layout, outputs[1]) + if err != nil { + return false, err + } + + // compare sizes and modtimes + if srcSize != dstSize { + return false, errors.New("source file and destination file are different sizes") + } + return srcModTime.Equal(dstModTime), nil +} + // teePrefix copies bytes from a reader to writer, logging each new line. func teePrefix(prefix string, r io.Reader, w io.Writer, logger func(format string, args ...interface{})) error { scanner := bufio.NewScanner(r)