Skip to content

Commit

Permalink
Merge pull request #12162 from afbjorklund/image-save-cmd
Browse files Browse the repository at this point in the history
Image save command
  • Loading branch information
medyagh authored Aug 13, 2021
2 parents e51c5b1 + b75bf27 commit 41ba49d
Show file tree
Hide file tree
Showing 14 changed files with 554 additions and 0 deletions.
74 changes: 74 additions & 0 deletions cmd/minikube/cmd/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,77 @@ var loadImageCmd = &cobra.Command{
},
}

func readFile(w io.Writer, tmp string) error {
r, err := os.Open(tmp)
if err != nil {
return err
}
_, err = io.Copy(w, r)
if err != nil {
return err
}
err = r.Close()
if err != nil {
return err
}
return nil
}

// saveImageCmd represents the image load command
var saveImageCmd = &cobra.Command{
Use: "save IMAGE [ARCHIVE | -]",
Short: "Save a image from minikube",
Long: "Save a image from minikube",
Example: "minikube image save image\nminikube image save image image.tar",
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
exit.Message(reason.Usage, "Please provide an image in the container runtime to save from minikube via <minikube image save IMAGE_NAME>")
}
// Save images from container runtime
profile, err := config.LoadProfile(viper.GetString(config.ProfileName))
if err != nil {
exit.Error(reason.Usage, "loading profile", err)
}

if len(args) > 1 {
output = args[1]

if args[1] == "-" {
tmp, err := ioutil.TempFile("", "image.*.tar")
if err != nil {
exit.Error(reason.GuestImageSave, "Failed to get temp", err)
}
tmp.Close()
output = tmp.Name()
}

if err := machine.DoSaveImages([]string{args[0]}, output, []*config.Profile{profile}, ""); err != nil {
exit.Error(reason.GuestImageSave, "Failed to save image", err)
}

if args[1] == "-" {
err := readFile(os.Stdout, output)
if err != nil {
exit.Error(reason.GuestImageSave, "Failed to read temp", err)
}
os.Remove(output)
}
} else {
if err := machine.SaveAndCacheImages([]string{args[0]}, []*config.Profile{profile}); err != nil {
exit.Error(reason.GuestImageSave, "Failed to save image", err)
}
if imgDaemon || imgRemote {
image.UseDaemon(imgDaemon)
image.UseRemote(imgRemote)
err := image.UploadCachedImage(args[0])
if err != nil {
exit.Error(reason.GuestImageSave, "Failed to save image", err)
}
}
}
},
}

var removeImageCmd = &cobra.Command{
Use: "rm IMAGE [IMAGE...]",
Short: "Remove one or more images",
Expand Down Expand Up @@ -258,5 +329,8 @@ func init() {
buildImageCmd.Flags().StringArrayVar(&buildEnv, "build-env", nil, "Environment variables to pass to the build. (format: key=value)")
buildImageCmd.Flags().StringArrayVar(&buildOpt, "build-opt", nil, "Specify arbitrary flags to pass to the build. (format: key=value)")
imageCmd.AddCommand(buildImageCmd)
saveImageCmd.Flags().BoolVar(&imgDaemon, "daemon", false, "Cache image to docker daemon")
saveImageCmd.Flags().BoolVar(&imgRemote, "remote", false, "Cache image to remote registry")
imageCmd.AddCommand(saveImageCmd)
imageCmd.AddCommand(listImageCmd)
}
59 changes: 59 additions & 0 deletions pkg/minikube/assets/vm_assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"io"
"os"
"path"
"strconv"
"time"

"github.com/pkg/errors"
Expand All @@ -37,8 +38,11 @@ const MemorySource = "memory"
// CopyableFile is something that can be copied
type CopyableFile interface {
io.Reader
io.Writer
GetLength() int
SetLength(int)
GetSourcePath() string
GetTargetPath() string

GetTargetDir() string
GetTargetName() string
Expand All @@ -62,6 +66,11 @@ func (b *BaseAsset) GetSourcePath() string {
return b.SourcePath
}

// GetTargetPath returns target path
func (b *BaseAsset) GetTargetPath() string {
return path.Join(b.GetTargetDir(), b.GetTargetName())
}

// GetTargetDir returns target dir
func (b *BaseAsset) GetTargetDir() string {
return b.TargetDir
Expand All @@ -86,6 +95,7 @@ func (b *BaseAsset) GetModTime() (time.Time, error) {
type FileAsset struct {
BaseAsset
reader io.ReadSeeker
writer io.Writer
file *os.File // Optional pointer to close file through FileAsset.Close()
}

Expand Down Expand Up @@ -134,6 +144,14 @@ func (f *FileAsset) GetLength() (flen int) {
return int(fi.Size())
}

// SetLength sets the file length
func (f *FileAsset) SetLength(flen int) {
err := os.Truncate(f.SourcePath, int64(flen))
if err != nil {
klog.Errorf("truncate(%q) failed: %v", f.SourcePath, err)
}
}

// GetModTime returns modification time of the file
func (f *FileAsset) GetModTime() (time.Time, error) {
fi, err := os.Stat(f.SourcePath)
Expand All @@ -152,6 +170,23 @@ func (f *FileAsset) Read(p []byte) (int, error) {
return f.reader.Read(p)
}

// Write writes the asset
func (f *FileAsset) Write(p []byte) (int, error) {
if f.writer == nil {
f.file.Close()
perms, err := strconv.ParseUint(f.Permissions, 8, 32)
if err != nil || perms > 07777 {
return 0, err
}
f.file, err = os.OpenFile(f.SourcePath, os.O_RDWR|os.O_CREATE, os.FileMode(perms))
if err != nil {
return 0, err
}
f.writer = io.Writer(f.file)
}
return f.writer.Write(p)
}

// Seek resets the reader to offset
func (f *FileAsset) Seek(offset int64, whence int) (int64, error) {
return f.reader.Seek(offset, whence)
Expand All @@ -177,11 +212,23 @@ func (m *MemoryAsset) GetLength() int {
return m.length
}

// SetLength returns length
func (m *MemoryAsset) SetLength(len int) {
m.length = len
}

// Read reads the asset
func (m *MemoryAsset) Read(p []byte) (int, error) {
return m.reader.Read(p)
}

// Writer writes the asset
func (m *MemoryAsset) Write(p []byte) (int, error) {
m.length = len(p)
m.reader = bytes.NewReader(p)
return len(p), nil
}

// Seek resets the reader to offset
func (m *MemoryAsset) Seek(offset int64, whence int) (int64, error) {
return m.reader.Seek(offset, whence)
Expand Down Expand Up @@ -298,6 +345,11 @@ func (m *BinAsset) GetLength() int {
return m.length
}

// SetLength sets length
func (m *BinAsset) SetLength(len int) {
m.length = len
}

// Read reads the asset
func (m *BinAsset) Read(p []byte) (int, error) {
if m.GetLength() == 0 {
Expand All @@ -306,6 +358,13 @@ func (m *BinAsset) Read(p []byte) (int, error) {
return m.reader.Read(p)
}

// Write writes the asset
func (m *BinAsset) Write(p []byte) (int, error) {
m.length = len(p)
m.reader = bytes.NewReader(p)
return len(p), nil
}

// Seek resets the reader to offset
func (m *BinAsset) Seek(offset int64, whence int) (int64, error) {
return m.reader.Seek(offset, whence)
Expand Down
3 changes: 3 additions & 0 deletions pkg/minikube/command/command_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ type Runner interface {
// Copy is a convenience method that runs a command to copy a file
Copy(assets.CopyableFile) error

// CopyFrom is a convenience method that runs a command to copy a file back
CopyFrom(assets.CopyableFile) error

// Remove is a convenience method that runs a command to remove a file
Remove(assets.CopyableFile) error
}
Expand Down
18 changes: 18 additions & 0 deletions pkg/minikube/command/exec_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,24 @@ func (e *execRunner) Copy(f assets.CopyableFile) error {
return writeFile(dst, f, os.FileMode(perms))
}

// CopyFrom copies a file
func (e *execRunner) CopyFrom(f assets.CopyableFile) error {
src := path.Join(f.GetTargetDir(), f.GetTargetName())

dst := f.GetSourcePath()
klog.Infof("cp: %s --> %s (%d bytes)", src, dst, f.GetLength())
if f.GetLength() == 0 {
klog.Warningf("0 byte asset: %+v", f)
}

perms, err := strconv.ParseInt(f.GetPermissions(), 8, 0)
if err != nil || perms > 07777 {
return errors.Wrapf(err, "error converting permissions %s to integer", f.GetPermissions())
}

return writeFile(dst, f, os.FileMode(perms))
}

// Remove removes a file
func (e *execRunner) Remove(f assets.CopyableFile) error {
dst := filepath.Join(f.GetTargetDir(), f.GetTargetName())
Expand Down
13 changes: 13 additions & 0 deletions pkg/minikube/command/fake_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,19 @@ func (f *FakeCommandRunner) Copy(file assets.CopyableFile) error {
return nil
}

func (f *FakeCommandRunner) CopyFrom(file assets.CopyableFile) error {
v, ok := f.fileMap.Load(file.GetSourcePath())
if !ok {
return fmt.Errorf("not found in map")
}
b := v.(bytes.Buffer)
_, err := io.Copy(file, &b)
if err != nil {
return errors.Wrapf(err, "error writing file: %+v", file)
}
return nil
}

// Remove removes the filename, file contents key value pair from the stored map
func (f *FakeCommandRunner) Remove(file assets.CopyableFile) error {
f.fileMap.Delete(file.GetSourcePath())
Expand Down
17 changes: 17 additions & 0 deletions pkg/minikube/command/kic_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,15 @@ func (k *kicRunner) Copy(f assets.CopyableFile) error {
return k.copy(tf.Name(), dst)
}

// CopyFrom copies a file
func (k *kicRunner) CopyFrom(f assets.CopyableFile) error {
src := f.GetTargetPath()
dst := f.GetSourcePath()

klog.Infof("%s (direct): %s --> %s", k.ociBin, src, dst)
return k.copyFrom(src, dst)
}

// tempDirectory returns the directory to use as the temp directory
// or an empty string if it should use the os default temp directory.
func tempDirectory(isMinikubeSnap bool, isDockerSnap bool) (string, error) {
Expand All @@ -229,6 +238,14 @@ func (k *kicRunner) copy(src string, dst string) error {
return copyToDocker(src, fullDest)
}

func (k *kicRunner) copyFrom(src string, dst string) error {
fullSource := fmt.Sprintf("%s:%s", k.nameOrID, src)
if k.ociBin == oci.Podman {
return copyToPodman(fullSource, dst)
}
return copyToDocker(fullSource, dst)
}

func (k *kicRunner) chmod(dst string, perm string) error {
_, err := k.RunCmd(exec.Command("sudo", "chmod", perm, dst))
return err
Expand Down
82 changes: 82 additions & 0 deletions pkg/minikube/command/ssh_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ limitations under the License.
package command

import (
"bufio"
"bytes"
"fmt"
"io"
"os/exec"
"path"
"strconv"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -373,3 +376,82 @@ func (s *SSHRunner) Copy(f assets.CopyableFile) error {
}
return g.Wait()
}

// CopyFrom copies a file from the remote over SSH.
func (s *SSHRunner) CopyFrom(f assets.CopyableFile) error {
dst := path.Join(path.Join(f.GetTargetDir(), f.GetTargetName()))

sess, err := s.session()
if err != nil {
return errors.Wrap(err, "NewSession")
}
defer func() {
if err := sess.Close(); err != nil {
if err != io.EOF {
klog.Errorf("session close: %v", err)
}
}
}()

cmd := exec.Command("stat", "-c", "%s", dst)
rr, err := s.RunCmd(cmd)
if err != nil {
return fmt.Errorf("%s: %v", cmd, err)
}
length, err := strconv.Atoi(strings.TrimSuffix(rr.Stdout.String(), "\n"))
if err != nil {
return err
}
src := f.GetSourcePath()
klog.Infof("scp %s --> %s (%d bytes)", dst, src, length)
f.SetLength(length)

r, err := sess.StdoutPipe()
if err != nil {
return errors.Wrap(err, "StdoutPipe")
}
w, err := sess.StdinPipe()
if err != nil {
return errors.Wrap(err, "StdinPipe")
}
// The scpcmd below *should not* return until all data is copied and the
// StdinPipe is closed. But let's use errgroup to make it explicit.
var g errgroup.Group
var copied int64

g.Go(func() error {
defer w.Close()
br := bufio.NewReader(r)
fmt.Fprint(w, "\x00")
b, err := br.ReadBytes('\n')
if err != nil {
return errors.Wrap(err, "ReadBytes")
}
if b[0] != 'C' {
return fmt.Errorf("unexpected: %v", b)
}
fmt.Fprint(w, "\x00")

copied = 0
for copied < int64(length) {
n, err := io.CopyN(f, br, int64(length))
if err != nil {
return errors.Wrap(err, "io.CopyN")
}
copied += n
}
fmt.Fprint(w, "\x00")
err = sess.Wait()
if err != nil {
return err
}
return nil
})

scp := fmt.Sprintf("sudo scp -f %s", f.GetTargetPath())
err = sess.Start(scp)
if err != nil {
return fmt.Errorf("%s: %s", scp, err)
}
return g.Wait()
}
Loading

0 comments on commit 41ba49d

Please sign in to comment.