From 38823e934831b3aa3b5980c37bcf6528d1dbcc5e Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Thu, 7 Nov 2019 11:47:01 -0800 Subject: [PATCH 1/3] Only copy new or modified files into VM on restart When minikube restarts, we can save time by only copying over files that have changes or don't exist in the VM. The code in this PR first checks if the file already exists in the VM, and skips copying it over again if it does. --- pkg/minikube/assets/vm_assets.go | 16 +++++++++ pkg/minikube/command/ssh_runner.go | 52 +++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/pkg/minikube/assets/vm_assets.go b/pkg/minikube/assets/vm_assets.go index fbc405e19704..737ff9403966 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 } // 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 { + return time.Time{} +} + // FileAsset is an asset using a file type FileAsset struct { BaseAsset @@ -104,6 +111,15 @@ func (f *FileAsset) GetLength() (flen int) { return int(fi.Size()) } +// GetModTime returns modification time of the file +func (f *FileAsset) GetModTime() time.Time { + fi, err := os.Stat(f.AssetName) + if err != nil { + return time.Time{} + } + return fi.ModTime() +} + 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..c32ec05f3423 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" @@ -143,6 +145,12 @@ 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())) + if s.fileExistsInVM(f, dst) { + glog.Infof("Skipping copying %s as it already exists", f.GetAssetName()) + return nil + } + sess, err := s.c.NewSession() if err != nil { return errors.Wrap(err, "NewSession") @@ -156,7 +164,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 { @@ -189,6 +196,49 @@ func (s *SSHRunner) Copy(f assets.CopyableFile) error { return g.Wait() } +func (s *SSHRunner) fileExistsInVM(f assets.CopyableFile, dst string) bool { + sess, err := s.c.NewSession() + if err != nil { + return false + } + + // check if sizes of the two files are the same + srcSize := f.GetLength() + size := fmt.Sprintf("ls -l %s | cut -d \" \" -f5", dst) + out, err := sess.CombinedOutput(size) + if err != nil { + return false + } + dstSize, err := strconv.Atoi(strings.Trim(string(out), "\n")) + if err != nil { + return false + } + if srcSize != dstSize { + return false + } + + sess, err = s.c.NewSession() + if err != nil { + return false + } + // ensure src file hasn't been modified since dst was copied over + srcModTime := f.GetModTime() + stat := "stat -c %Y" + fmt.Sprintf(" %s", dst) + out, err = sess.CombinedOutput(stat) + if err != nil { + return false + } + unix, err := strconv.Atoi(strings.Trim(string(out), "\n")) + if err != nil { + return false + } + dstModTime := time.Unix(int64(unix), 0) + if err != nil { + return false + } + return srcModTime.Before(dstModTime) +} + // 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) From 006b802e5c7506d8f1ab2be63b539ee38688ac36 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Fri, 8 Nov 2019 13:56:30 -0800 Subject: [PATCH 2/3] Check for file size and mtime at the same time Also, copy over the mtime correctly in Copy so that we can make sure they are equal in `sameFileExists` --- pkg/minikube/assets/vm_assets.go | 12 +++--- pkg/minikube/command/ssh_runner.go | 69 +++++++++++++++++------------- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/pkg/minikube/assets/vm_assets.go b/pkg/minikube/assets/vm_assets.go index 737ff9403966..b4ce7bffc22c 100644 --- a/pkg/minikube/assets/vm_assets.go +++ b/pkg/minikube/assets/vm_assets.go @@ -37,7 +37,7 @@ type CopyableFile interface { GetTargetDir() string GetTargetName() string GetPermissions() string - GetModTime() time.Time + GetModTime() (time.Time, error) } // BaseAsset is the base asset class @@ -69,8 +69,8 @@ func (b *BaseAsset) GetPermissions() string { } // GetModTime returns mod time -func (b *BaseAsset) GetModTime() time.Time { - return time.Time{} +func (b *BaseAsset) GetModTime() (time.Time, error) { + return time.Time{}, errors.New("modtime isn't available for this asset") } // FileAsset is an asset using a file @@ -112,12 +112,12 @@ func (f *FileAsset) GetLength() (flen int) { } // GetModTime returns modification time of the file -func (f *FileAsset) GetModTime() time.Time { +func (f *FileAsset) GetModTime() (time.Time, error) { fi, err := os.Stat(f.AssetName) if err != nil { - return time.Time{} + return time.Time{}, err } - return fi.ModTime() + return fi.ModTime(), nil } func (f *FileAsset) Read(p []byte) (int, error) { diff --git a/pkg/minikube/command/ssh_runner.go b/pkg/minikube/command/ssh_runner.go index c32ec05f3423..6e1385fe4295 100644 --- a/pkg/minikube/command/ssh_runner.go +++ b/pkg/minikube/command/ssh_runner.go @@ -37,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. @@ -146,11 +150,14 @@ 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())) - if s.fileExistsInVM(f, dst) { + exists, err := s.sameFileExists(f, dst) + if err != nil { + glog.Infof("Checked if %s exists, but got error: %v", f.GetAssetName(), err) + } + if exists { glog.Infof("Skipping copying %s as it already exists", f.GetAssetName()) return nil } - sess, err := s.c.NewSession() if err != nil { return errors.Wrap(err, "NewSession") @@ -189,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", f.GetAssetName(), 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) @@ -196,47 +209,43 @@ func (s *SSHRunner) Copy(f assets.CopyableFile) error { return g.Wait() } -func (s *SSHRunner) fileExistsInVM(f assets.CopyableFile, dst string) bool { - sess, err := s.c.NewSession() - if err != nil { - return false - } - - // check if sizes of the two files are the same +func (s *SSHRunner) sameFileExists(f assets.CopyableFile, dst string) (bool, error) { + // get file size and modtime of the source srcSize := f.GetLength() - size := fmt.Sprintf("ls -l %s | cut -d \" \" -f5", dst) - out, err := sess.CombinedOutput(size) + srcModTime, err := f.GetModTime() if err != nil { - return false + return false, err } - dstSize, err := strconv.Atoi(strings.Trim(string(out), "\n")) + + // get file size and modtime of the destination + sess, err := s.c.NewSession() if err != nil { - return false - } - if srcSize != dstSize { - return false + return false, err } - sess, err = s.c.NewSession() + size := fmt.Sprintf("ls -l %s | cut -d \" \" -f5", dst) + stat := "stat -c %y" + fmt.Sprintf(" %s", dst) + + out, err := sess.CombinedOutput(size + " && " + stat) if err != nil { - return false + return false, err } - // ensure src file hasn't been modified since dst was copied over - srcModTime := f.GetModTime() - stat := "stat -c %Y" + fmt.Sprintf(" %s", dst) - out, err = sess.CombinedOutput(stat) + outputs := strings.Split(strings.Trim(string(out), "\n"), "\n") + + dstSize, err := strconv.Atoi(outputs[0]) if err != nil { - return false + return false, err } - unix, err := strconv.Atoi(strings.Trim(string(out), "\n")) + dstModTime, err := time.Parse(layout, outputs[1]) if err != nil { - return false + return false, err } - dstModTime := time.Unix(int64(unix), 0) - if err != nil { - return false + + // compare sizes and modtimes + if srcSize != dstSize { + return false, errors.New("source file and destination file are different sizes") } - return srcModTime.Before(dstModTime) + return srcModTime.Equal(dstModTime), nil } // teePrefix copies bytes from a reader to writer, logging each new line. From e9d212653dc2d907991b3ab31bd337a86d63cdba Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Mon, 11 Nov 2019 14:28:13 -0800 Subject: [PATCH 3/3] Simplify command to get size and modtime --- pkg/minikube/assets/vm_assets.go | 7 ++----- pkg/minikube/command/ssh_runner.go | 17 +++++++++-------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/pkg/minikube/assets/vm_assets.go b/pkg/minikube/assets/vm_assets.go index b4ce7bffc22c..88c92089fcf3 100644 --- a/pkg/minikube/assets/vm_assets.go +++ b/pkg/minikube/assets/vm_assets.go @@ -70,7 +70,7 @@ func (b *BaseAsset) GetPermissions() string { // GetModTime returns mod time func (b *BaseAsset) GetModTime() (time.Time, error) { - return time.Time{}, errors.New("modtime isn't available for this asset") + return time.Time{}, nil } // FileAsset is an asset using a file @@ -114,10 +114,7 @@ func (f *FileAsset) GetLength() (flen int) { // GetModTime returns modification time of the file func (f *FileAsset) GetModTime() (time.Time, error) { fi, err := os.Stat(f.AssetName) - if err != nil { - return time.Time{}, err - } - return fi.ModTime(), nil + return fi.ModTime(), err } func (f *FileAsset) Read(p []byte) (int, error) { diff --git a/pkg/minikube/command/ssh_runner.go b/pkg/minikube/command/ssh_runner.go index 6e1385fe4295..fe4bc41046e1 100644 --- a/pkg/minikube/command/ssh_runner.go +++ b/pkg/minikube/command/ssh_runner.go @@ -152,10 +152,10 @@ 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", f.GetAssetName(), err) + glog.Infof("Checked if %s exists, but got error: %v", dst, err) } if exists { - glog.Infof("Skipping copying %s as it already exists", f.GetAssetName()) + glog.Infof("Skipping copying %s as it already exists", dst) return nil } sess, err := s.c.NewSession() @@ -198,7 +198,7 @@ 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", f.GetAssetName(), err) + glog.Infof("error getting modtime for %s: %v", dst, err) } else { scp += fmt.Sprintf(" && sudo touch -d \"%s\" %s", mtime.Format(layout), dst) } @@ -216,6 +216,9 @@ func (s *SSHRunner) sameFileExists(f assets.CopyableFile, dst string) (bool, err 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() @@ -223,14 +226,12 @@ func (s *SSHRunner) sameFileExists(f assets.CopyableFile, dst string) (bool, err return false, err } - size := fmt.Sprintf("ls -l %s | cut -d \" \" -f5", dst) - stat := "stat -c %y" + fmt.Sprintf(" %s", dst) - - out, err := sess.CombinedOutput(size + " && " + stat) + cmd := "stat -c \"%s %y\" " + dst + out, err := sess.CombinedOutput(cmd) if err != nil { return false, err } - outputs := strings.Split(strings.Trim(string(out), "\n"), "\n") + outputs := strings.SplitN(strings.Trim(string(out), "\n"), " ", 2) dstSize, err := strconv.Atoi(outputs[0]) if err != nil {