forked from toolshed/abra
		
	
		
			
				
	
	
		
			384 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			384 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package app
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"os"
 | |
| 	"path"
 | |
| 	"path/filepath"
 | |
| 	"strings"
 | |
| 
 | |
| 	"coopcloud.tech/abra/cli/internal"
 | |
| 	"coopcloud.tech/abra/pkg/autocomplete"
 | |
| 	"coopcloud.tech/abra/pkg/client"
 | |
| 	containerPkg "coopcloud.tech/abra/pkg/container"
 | |
| 	"coopcloud.tech/abra/pkg/formatter"
 | |
| 	"coopcloud.tech/abra/pkg/log"
 | |
| 	"coopcloud.tech/abra/pkg/upstream/container"
 | |
| 	"github.com/docker/cli/cli/command"
 | |
| 	containertypes "github.com/docker/docker/api/types/container"
 | |
| 	dockerClient "github.com/docker/docker/client"
 | |
| 	"github.com/docker/docker/errdefs"
 | |
| 	"github.com/docker/docker/pkg/archive"
 | |
| 	"github.com/spf13/cobra"
 | |
| )
 | |
| 
 | |
| var AppCpCommand = &cobra.Command{
 | |
| 	Use:     "cp <domain> <src> <dst> [flags]",
 | |
| 	Aliases: []string{"c"},
 | |
| 	Short:   "Copy files to/from a deployed app service",
 | |
| 	Example: `  # copy myfile.txt to the root of the app service
 | |
|   abra app cp 1312.net myfile.txt app:/
 | |
| 
 | |
|   # copy that file back to your current working directory locally
 | |
|   abra app cp 1312.net app:/myfile.txt ./`,
 | |
| 	Args: cobra.ExactArgs(3),
 | |
| 	ValidArgsFunction: func(
 | |
| 		cmd *cobra.Command,
 | |
| 		args []string,
 | |
| 		toComplete string) ([]string, cobra.ShellCompDirective) {
 | |
| 		switch l := len(args); l {
 | |
| 		case 0:
 | |
| 			return autocomplete.AppNameComplete()
 | |
| 		default:
 | |
| 			return nil, cobra.ShellCompDirectiveDefault
 | |
| 		}
 | |
| 	},
 | |
| 	Run: func(cmd *cobra.Command, args []string) {
 | |
| 		app := internal.ValidateApp(args)
 | |
| 
 | |
| 		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		src := args[1]
 | |
| 		dst := args[2]
 | |
| 		srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		cl, err := client.New(app.Server)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		container, err := containerPkg.GetContainerFromStackAndService(cl, app.StackName(), service)
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 		log.Debugf("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server)
 | |
| 
 | |
| 		if toContainer {
 | |
| 			err = CopyToContainer(cl, container.ID, srcPath, dstPath)
 | |
| 		} else {
 | |
| 			err = CopyFromContainer(cl, container.ID, srcPath, dstPath)
 | |
| 		}
 | |
| 		if err != nil {
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 	},
 | |
| }
 | |
| 
 | |
| var errServiceMissing = errors.New("one of <src>/<dest> arguments must take $SERVICE:$PATH form")
 | |
| 
 | |
| // parseSrcAndDst parses src and dest string. One of src or dst must be of the form $SERVICE:$PATH
 | |
| func parseSrcAndDst(src, dst string) (srcPath string, dstPath string, service string, toContainer bool, err error) {
 | |
| 	parsedSrc := strings.SplitN(src, ":", 2)
 | |
| 	parsedDst := strings.SplitN(dst, ":", 2)
 | |
| 	if len(parsedSrc)+len(parsedDst) != 3 {
 | |
| 		return "", "", "", false, errServiceMissing
 | |
| 	}
 | |
| 	if len(parsedSrc) == 2 {
 | |
| 		return parsedSrc[1], dst, parsedSrc[0], false, nil
 | |
| 	}
 | |
| 	if len(parsedDst) == 2 {
 | |
| 		return src, parsedDst[1], parsedDst[0], true, nil
 | |
| 	}
 | |
| 	return "", "", "", false, errServiceMissing
 | |
| }
 | |
| 
 | |
| // CopyToContainer copies a file or directory from the local file system to the container.
 | |
| // See the possible copy modes and their documentation.
 | |
| func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath string) error {
 | |
| 	srcStat, err := os.Stat(srcPath)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("local %s ", err)
 | |
| 	}
 | |
| 
 | |
| 	dstStat, err := cl.ContainerStatPath(context.Background(), containerID, dstPath)
 | |
| 	dstExists := true
 | |
| 	if err != nil {
 | |
| 		if errdefs.IsNotFound(err) {
 | |
| 			dstExists = false
 | |
| 		} else {
 | |
| 			return fmt.Errorf("remote path: %s", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	mode, err := copyMode(srcPath, dstPath, srcStat.Mode(), dstStat.Mode, dstExists)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	movePath := ""
 | |
| 	switch mode {
 | |
| 	case CopyModeDirToDir:
 | |
| 		// Add the src directory to the destination path
 | |
| 		_, srcDir := path.Split(srcPath)
 | |
| 		dstPath = path.Join(dstPath, srcDir)
 | |
| 
 | |
| 		// Make sure the dst directory exits.
 | |
| 		dcli, err := command.NewDockerCli()
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		if _, err := container.RunExec(dcli, cl, containerID, &containertypes.ExecOptions{
 | |
| 			AttachStderr: true,
 | |
| 			AttachStdin:  true,
 | |
| 			AttachStdout: true,
 | |
| 			Cmd:          []string{"mkdir", "-p", dstPath},
 | |
| 			Detach:       false,
 | |
| 			Tty:          true,
 | |
| 		}); err != nil {
 | |
| 			return fmt.Errorf("create remote directory: %s", err)
 | |
| 		}
 | |
| 	case CopyModeFileToFile:
 | |
| 		// Remove the file component from the path, since docker can only copy
 | |
| 		// to a directory.
 | |
| 		dstPath, _ = path.Split(dstPath)
 | |
| 	case CopyModeFileToFileRename:
 | |
| 		// Copy the file to the temp directory and move it to its dstPath
 | |
| 		// afterwards.
 | |
| 		movePath = dstPath
 | |
| 		dstPath = "/tmp"
 | |
| 	}
 | |
| 
 | |
| 	toTarOpts := &archive.TarOptions{IncludeSourceDir: true, NoOverwriteDirNonDir: true, Compression: archive.Gzip}
 | |
| 	content, err := archive.TarWithOptions(srcPath, toTarOpts)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	log.Debugf("copy %s from local to %s on container", srcPath, dstPath)
 | |
| 	copyOpts := containertypes.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
 | |
| 	if err := cl.CopyToContainer(context.Background(), containerID, dstPath, content, copyOpts); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if movePath != "" {
 | |
| 		_, srcFile := path.Split(srcPath)
 | |
| 		dcli, err := command.NewDockerCli()
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		if _, err := container.RunExec(dcli, cl, containerID, &containertypes.ExecOptions{
 | |
| 			AttachStderr: true,
 | |
| 			AttachStdin:  true,
 | |
| 			AttachStdout: true,
 | |
| 			Cmd:          []string{"mv", path.Join("/tmp", srcFile), movePath},
 | |
| 			Detach:       false,
 | |
| 			Tty:          true,
 | |
| 		}); err != nil {
 | |
| 			return fmt.Errorf("create remote directory: %s", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // CopyFromContainer copies a file or directory from the given container to the local file system.
 | |
| // See the possible copy modes and their documentation.
 | |
| func CopyFromContainer(cl *dockerClient.Client, containerID, srcPath, dstPath string) error {
 | |
| 	srcStat, err := cl.ContainerStatPath(context.Background(), containerID, srcPath)
 | |
| 	if err != nil {
 | |
| 		if errdefs.IsNotFound(err) {
 | |
| 			return fmt.Errorf("remote: %s does not exist", srcPath)
 | |
| 		} else {
 | |
| 			return fmt.Errorf("remote path: %s", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	dstStat, err := os.Stat(dstPath)
 | |
| 	dstExists := true
 | |
| 	var dstMode os.FileMode
 | |
| 	if err != nil {
 | |
| 		if os.IsNotExist(err) {
 | |
| 			dstExists = false
 | |
| 		} else {
 | |
| 			return fmt.Errorf("remote path: %s", err)
 | |
| 		}
 | |
| 	} else {
 | |
| 		dstMode = dstStat.Mode()
 | |
| 	}
 | |
| 
 | |
| 	mode, err := copyMode(srcPath, dstPath, srcStat.Mode, dstMode, dstExists)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	moveDstDir := ""
 | |
| 	moveDstFile := ""
 | |
| 	switch mode {
 | |
| 	case CopyModeFileToFile:
 | |
| 		// Remove the file component from the path, since docker can only copy
 | |
| 		// to a directory.
 | |
| 		dstPath, _ = path.Split(dstPath)
 | |
| 	case CopyModeFileToFileRename:
 | |
| 		// Copy the file to the temp directory and move it to its dstPath
 | |
| 		// afterwards.
 | |
| 		moveDstFile = dstPath
 | |
| 		dstPath = "/tmp"
 | |
| 	case CopyModeFilesToDir:
 | |
| 		// Copy the directory to the temp directory and move it to its
 | |
| 		// dstPath afterwards.
 | |
| 		moveDstDir = path.Join(dstPath, "/")
 | |
| 		dstPath = "/tmp"
 | |
| 
 | |
| 		// Make sure the temp directory always gets removed
 | |
| 		defer os.Remove(path.Join("/tmp"))
 | |
| 	}
 | |
| 
 | |
| 	content, _, err := cl.CopyFromContainer(context.Background(), containerID, srcPath)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("copy: %s", err)
 | |
| 	}
 | |
| 	defer content.Close()
 | |
| 	if err := archive.Untar(content, dstPath, &archive.TarOptions{
 | |
| 		NoOverwriteDirNonDir: true,
 | |
| 		Compression:          archive.Gzip,
 | |
| 		NoLchown:             true,
 | |
| 	}); err != nil {
 | |
| 		return fmt.Errorf("untar: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	if moveDstFile != "" {
 | |
| 		_, srcFile := path.Split(strings.TrimSuffix(srcPath, "/"))
 | |
| 		if err := moveFile(path.Join("/tmp", srcFile), moveDstFile); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 	if moveDstDir != "" {
 | |
| 		_, srcDir := path.Split(strings.TrimSuffix(srcPath, "/"))
 | |
| 		if err := moveDir(path.Join("/tmp", srcDir), moveDstDir); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| var (
 | |
| 	ErrCopyDirToFile  = fmt.Errorf("can't copy dir to file")
 | |
| 	ErrDstDirNotExist = fmt.Errorf("destination directory does not exist")
 | |
| )
 | |
| 
 | |
| type CopyMode int
 | |
| 
 | |
| const (
 | |
| 	// Copy a src file to a dest file. The src and dest file names are the same.
 | |
| 	//  <dir_src>/<file> + <dir_dst>/<file> -> <dir_dst>/<file>
 | |
| 	CopyModeFileToFile = CopyMode(iota)
 | |
| 	// Copy a src file to a dest file. The src and dest file names are  not the same.
 | |
| 	//  <dir_src>/<file_src> + <dir_dst>/<file_dst> -> <dir_dst>/<file_dst>
 | |
| 	CopyModeFileToFileRename
 | |
| 	// Copy a src file to dest directory. The dest file gets created in the dest
 | |
| 	// folder with the src filename.
 | |
| 	//  <dir_src>/<file> + <dir_dst> -> <dir_dst>/<file>
 | |
| 	CopyModeFileToDir
 | |
| 	// Copy a src directory to dest directory.
 | |
| 	//  <dir_src> + <dir_dst> -> <dir_dst>/<dir_src>
 | |
| 	CopyModeDirToDir
 | |
| 	// Copy all files in the src directory to the dest directory. This works recursively.
 | |
| 	//  <dir_src>/ + <dir_dst> -> <dir_dst>/<files_from_dir_src>
 | |
| 	CopyModeFilesToDir
 | |
| )
 | |
| 
 | |
| // copyMode takes a src and dest path and file mode to determine the copy mode.
 | |
| // See the possible copy modes and their documentation.
 | |
| func copyMode(srcPath, dstPath string, srcMode os.FileMode, dstMode os.FileMode, dstExists bool) (CopyMode, error) {
 | |
| 	_, srcFile := path.Split(srcPath)
 | |
| 	_, dstFile := path.Split(dstPath)
 | |
| 	if srcMode.IsDir() {
 | |
| 		if !dstExists {
 | |
| 			return -1, ErrDstDirNotExist
 | |
| 		}
 | |
| 		if dstMode.IsDir() {
 | |
| 			if strings.HasSuffix(srcPath, "/") {
 | |
| 				return CopyModeFilesToDir, nil
 | |
| 			}
 | |
| 			return CopyModeDirToDir, nil
 | |
| 		}
 | |
| 		return -1, ErrCopyDirToFile
 | |
| 	}
 | |
| 
 | |
| 	if dstMode.IsDir() {
 | |
| 		return CopyModeFileToDir, nil
 | |
| 	}
 | |
| 
 | |
| 	if srcFile != dstFile {
 | |
| 		return CopyModeFileToFileRename, nil
 | |
| 	}
 | |
| 
 | |
| 	return CopyModeFileToFile, nil
 | |
| }
 | |
| 
 | |
| // moveDir moves all files from a source path to the destination path recursively.
 | |
| func moveDir(sourcePath, destPath string) error {
 | |
| 	return filepath.Walk(sourcePath, func(p string, info os.FileInfo, err error) error {
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		newPath := path.Join(destPath, strings.TrimPrefix(p, sourcePath))
 | |
| 		if info.IsDir() {
 | |
| 			err := os.Mkdir(newPath, info.Mode())
 | |
| 			if err != nil {
 | |
| 				if os.IsExist(err) {
 | |
| 					return nil
 | |
| 				}
 | |
| 				return err
 | |
| 			}
 | |
| 		}
 | |
| 		if info.Mode().IsRegular() {
 | |
| 			return moveFile(p, newPath)
 | |
| 		}
 | |
| 		return nil
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // moveFile moves a file from a source path to a destination path.
 | |
| func moveFile(sourcePath, destPath string) error {
 | |
| 	inputFile, err := os.Open(sourcePath)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	outputFile, err := os.Create(destPath)
 | |
| 	if err != nil {
 | |
| 		inputFile.Close()
 | |
| 		return err
 | |
| 	}
 | |
| 	defer outputFile.Close()
 | |
| 	_, err = io.Copy(outputFile, inputFile)
 | |
| 	inputFile.Close()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Remove file after succesfull copy.
 | |
| 	err = os.Remove(sourcePath)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func init() {
 | |
| 	AppCpCommand.Flags().BoolVarP(
 | |
| 		&internal.Chaos,
 | |
| 		"chaos",
 | |
| 		"C",
 | |
| 		false,
 | |
| 		"ignore uncommitted recipes changes",
 | |
| 	)
 | |
| }
 |