Explorar o código

Add cp command

Signed-off-by: Julien Tant <[email protected]>
Julien Tant %!s(int64=4) %!d(string=hai) anos
pai
achega
8f9ce9d763

+ 4 - 0
aci/compose.go

@@ -80,6 +80,10 @@ func (cs *aciComposeService) UnPause(ctx context.Context, project string, option
 	return errdefs.ErrNotImplemented
 }
 
+func (cs *aciComposeService) Copy(ctx context.Context, project *types.Project, options compose.CopyOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
 func (cs *aciComposeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error {
 	logrus.Debugf("Up on project with name %q", project.Name)
 

+ 4 - 0
api/client/compose.go

@@ -96,6 +96,10 @@ func (c *composeService) Exec(ctx context.Context, project *types.Project, opts
 	return 0, errdefs.ErrNotImplemented
 }
 
+func (c *composeService) Copy(ctx context.Context, project *types.Project, opts compose.CopyOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
 func (c *composeService) Pause(ctx context.Context, project string, options compose.PauseOptions) error {
 	return errdefs.ErrNotImplemented
 }

+ 11 - 0
api/compose/api.go

@@ -62,6 +62,8 @@ type Service interface {
 	Remove(ctx context.Context, project *types.Project, options RemoveOptions) ([]string, error)
 	// Exec executes a command in a running service container
 	Exec(ctx context.Context, project *types.Project, opts RunOptions) (int, error)
+	// Copy copies a file/folder between a service container and the local filesystem
+	Copy(ctx context.Context, project *types.Project, opts CopyOptions) error
 	// Pause executes the equivalent to a `compose pause`
 	Pause(ctx context.Context, project string, options PauseOptions) error
 	// UnPause executes the equivalent to a `compose unpause`
@@ -271,6 +273,15 @@ type PsOptions struct {
 	All bool
 }
 
+// CopyOptions group options of the cp API
+type CopyOptions struct {
+	Source      string
+	Destination string
+	Index       int
+	FollowLink  bool
+	CopyUIDGID  bool
+}
+
 // PortPublisher hold status about published port
 type PortPublisher struct {
 	URL           string

+ 5 - 0
api/compose/delegator.go

@@ -112,6 +112,11 @@ func (s *ServiceDelegator) Exec(ctx context.Context, project *types.Project, opt
 	return s.Delegate.Exec(ctx, project, options)
 }
 
+//Copy implements Service interface
+func (s *ServiceDelegator) Copy(ctx context.Context, project *types.Project, options CopyOptions) error {
+	return s.Delegate.Copy(ctx, project, options)
+}
+
 //Pause implements Service interface
 func (s *ServiceDelegator) Pause(ctx context.Context, project string, options PauseOptions) error {
 	return s.Delegate.Pause(ctx, project, options)

+ 5 - 0
api/compose/noimpl.go

@@ -112,6 +112,11 @@ func (s NoImpl) Exec(ctx context.Context, project *types.Project, opts RunOption
 	return 0, errdefs.ErrNotImplemented
 }
 
+//Copy implements Service interface
+func (s NoImpl) Copy(ctx context.Context, project *types.Project, opts CopyOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
 //Pause implements Service interface
 func (s NoImpl) Pause(ctx context.Context, project string, options PauseOptions) error {
 	return errdefs.ErrNotImplemented

+ 1 - 0
cli/cmd/compose/compose.go

@@ -228,6 +228,7 @@ func RootCommand(contextType string, backend compose.Service) *cobra.Command {
 			pushCommand(&opts, backend),
 			pullCommand(&opts, backend),
 			createCommand(&opts, backend),
+			copyCommand(&opts, backend),
 		)
 	}
 	command.Flags().SetInterspersed(false)

+ 83 - 0
cli/cmd/compose/cp.go

@@ -0,0 +1,83 @@
+/*
+   Copyright 2020 Docker Compose CLI 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 compose
+
+import (
+	"context"
+	"errors"
+
+	"github.com/docker/cli/cli"
+	"github.com/spf13/cobra"
+
+	"github.com/docker/compose-cli/api/compose"
+)
+
+type copyOptions struct {
+	*projectOptions
+
+	source      string
+	destination string
+	index       int
+	followLink  bool
+	copyUIDGID  bool
+}
+
+func copyCommand(p *projectOptions, backend compose.Service) *cobra.Command {
+	opts := copyOptions{
+		projectOptions: p,
+	}
+	copyCmd := &cobra.Command{
+		Use: `cp [OPTIONS] SERVICE:SRC_PATH DEST_PATH|-
+	docker compose cp [OPTIONS] SRC_PATH|- SERVICE:DEST_PATH`,
+		Short: "Copy files/folders between a service container and the local filesystem",
+		Args:  cli.ExactArgs(2),
+		RunE: Adapt(func(ctx context.Context, args []string) error {
+			if args[0] == "" {
+				return errors.New("source can not be empty")
+			}
+			if args[1] == "" {
+				return errors.New("destination can not be empty")
+			}
+
+			opts.source = args[0]
+			opts.destination = args[1]
+			return runCopy(ctx, backend, opts)
+		}),
+	}
+
+	flags := copyCmd.Flags()
+	flags.IntVar(&opts.index, "index", 1, "index of the container if there are multiple instances of a service [default: 1].")
+	flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH")
+	flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)")
+
+	return copyCmd
+}
+
+func runCopy(ctx context.Context, backend compose.Service, opts copyOptions) error {
+	projects, err := opts.toProject(nil)
+	if err != nil {
+		return err
+	}
+
+	return backend.Copy(ctx, projects, compose.CopyOptions{
+		Source:      opts.source,
+		Destination: opts.destination,
+		Index:       opts.index,
+		FollowLink:  opts.followLink,
+		CopyUIDGID:  opts.copyUIDGID,
+	})
+}

+ 1 - 0
cli/metrics/commands.go

@@ -63,6 +63,7 @@ var commands = []string{
 	"deploy",
 	"list",
 	"ls",
+	"cp",
 	"merge",
 	"pull",
 	"push",

+ 4 - 0
ecs/local/compose.go

@@ -188,6 +188,10 @@ func (e ecsLocalSimulation) Exec(ctx context.Context, project *types.Project, op
 	return 0, errdefs.ErrNotImplemented
 }
 
+func (e ecsLocalSimulation) Copy(ctx context.Context, project *types.Project, opts compose.CopyOptions) error {
+	return e.compose.Copy(ctx, project, opts)
+}
+
 func (e ecsLocalSimulation) Pause(ctx context.Context, project string, options compose.PauseOptions) error {
 	return e.compose.Pause(ctx, project, options)
 }

+ 4 - 0
ecs/up.go

@@ -75,6 +75,10 @@ func (b *ecsAPIService) Port(ctx context.Context, project string, service string
 	return "", 0, errdefs.ErrNotImplemented
 }
 
+func (b *ecsAPIService) Copy(ctx context.Context, project *types.Project, options compose.CopyOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
 func (b *ecsAPIService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error {
 	logrus.Debugf("deploying on AWS with region=%q", b.Region)
 	err := b.aws.CheckRequirements(ctx, b.Region)

+ 5 - 0
kube/compose.go

@@ -198,6 +198,11 @@ func (s *composeService) Stop(ctx context.Context, project *types.Project, optio
 	return errdefs.ErrNotImplemented
 }
 
+// Copy copies a file/folder between a service container and the local filesystem
+func (s *composeService) Copy(ctx context.Context, project *types.Project, options compose.CopyOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
 // Logs executes the equivalent to a `compose logs`
 func (s *composeService) Logs(ctx context.Context, projectName string, consumer compose.LogConsumer, options compose.LogOptions) error {
 	if len(options.Services) > 0 {

+ 265 - 0
local/compose/cp.go

@@ -0,0 +1,265 @@
+/*
+   Copyright 2020 Docker Compose CLI 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 compose
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/compose-spec/compose-go/types"
+	"github.com/docker/cli/cli/command"
+	"github.com/docker/compose-cli/api/compose"
+	apitypes "github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/pkg/archive"
+	"github.com/docker/docker/pkg/system"
+	"github.com/pkg/errors"
+)
+
+type copyDirection int
+
+const (
+	fromService copyDirection = 1 << iota
+	toService
+	acrossServices = fromService | toService
+)
+
+func (s *composeService) Copy(ctx context.Context, project *types.Project, opts compose.CopyOptions) error {
+	srcService, srcPath := splitCpArg(opts.Source)
+	destService, dstPath := splitCpArg(opts.Destination)
+
+	var direction copyDirection
+	var serviceName string
+	if srcService != "" {
+		direction |= fromService
+		serviceName = srcService
+	}
+	if destService != "" {
+		direction |= toService
+		serviceName = destService
+	}
+
+	containers, err := s.apiClient.ContainerList(ctx, apitypes.ContainerListOptions{
+		Filters: filters.NewArgs(
+			projectFilter(project.Name),
+			serviceFilter(serviceName),
+			filters.Arg("label", fmt.Sprintf("%s=%d", containerNumberLabel, opts.Index)),
+		),
+	})
+	if err != nil {
+		return err
+	}
+
+	if len(containers) < 1 {
+		return fmt.Errorf("service %s not running", serviceName)
+	}
+
+	containerID := containers[0].ID
+	switch direction {
+	case fromService:
+		return s.copyFromContainer(ctx, containerID, srcPath, dstPath, opts)
+	case toService:
+		return s.copyToContainer(ctx, containerID, srcPath, dstPath, opts)
+	case acrossServices:
+		return errors.New("copying between services is not supported")
+	default:
+		return errors.New("unknown copy direction")
+	}
+}
+
+func (s *composeService) copyToContainer(ctx context.Context, containerID string, srcPath string, dstPath string, opts compose.CopyOptions) error {
+	var err error
+	if srcPath != "-" {
+		// Get an absolute source path.
+		srcPath, err = resolveLocalPath(srcPath)
+		if err != nil {
+			return err
+		}
+	}
+
+	// Prepare destination copy info by stat-ing the container path.
+	dstInfo := archive.CopyInfo{Path: dstPath}
+	dstStat, err := s.apiClient.ContainerStatPath(ctx, containerID, dstPath)
+
+	// If the destination is a symbolic link, we should evaluate it.
+	if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
+		linkTarget := dstStat.LinkTarget
+		if !system.IsAbs(linkTarget) {
+			// Join with the parent directory.
+			dstParent, _ := archive.SplitPathDirEntry(dstPath)
+			linkTarget = filepath.Join(dstParent, linkTarget)
+		}
+
+		dstInfo.Path = linkTarget
+		dstStat, err = s.apiClient.ContainerStatPath(ctx, containerID, linkTarget)
+	}
+
+	// Validate the destination path
+	if err := command.ValidateOutputPathFileMode(dstStat.Mode); err != nil {
+		return errors.Wrapf(err, `destination "%s:%s" must be a directory or a regular file`, containerID, dstPath)
+	}
+
+	// Ignore any error and assume that the parent directory of the destination
+	// path exists, in which case the copy may still succeed. If there is any
+	// type of conflict (e.g., non-directory overwriting an existing directory
+	// or vice versa) the extraction will fail. If the destination simply did
+	// not exist, but the parent directory does, the extraction will still
+	// succeed.
+	if err == nil {
+		dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()
+	}
+
+	var (
+		content         io.Reader
+		resolvedDstPath string
+	)
+
+	if srcPath == "-" {
+		content = os.Stdin
+		resolvedDstPath = dstInfo.Path
+		if !dstInfo.IsDir {
+			return errors.Errorf("destination \"%s:%s\" must be a directory", containerID, dstPath)
+		}
+	} else {
+		// Prepare source copy info.
+		srcInfo, err := archive.CopyInfoSourcePath(srcPath, opts.FollowLink)
+		if err != nil {
+			return err
+		}
+
+		srcArchive, err := archive.TarResource(srcInfo)
+		if err != nil {
+			return err
+		}
+		defer srcArchive.Close() //nolint:errcheck
+
+		// With the stat info about the local source as well as the
+		// destination, we have enough information to know whether we need to
+		// alter the archive that we upload so that when the server extracts
+		// it to the specified directory in the container we get the desired
+		// copy behavior.
+
+		// See comments in the implementation of `archive.PrepareArchiveCopy`
+		// for exactly what goes into deciding how and whether the source
+		// archive needs to be altered for the correct copy behavior when it is
+		// extracted. This function also infers from the source and destination
+		// info which directory to extract to, which may be the parent of the
+		// destination that the user specified.
+		dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo)
+		if err != nil {
+			return err
+		}
+		defer preparedArchive.Close() //nolint:errcheck
+
+		resolvedDstPath = dstDir
+		content = preparedArchive
+	}
+
+	options := apitypes.CopyToContainerOptions{
+		AllowOverwriteDirWithFile: false,
+		CopyUIDGID:                opts.CopyUIDGID,
+	}
+	return s.apiClient.CopyToContainer(ctx, containerID, resolvedDstPath, content, options)
+}
+
+func (s *composeService) copyFromContainer(ctx context.Context, containerID, srcPath, dstPath string, opts compose.CopyOptions) error {
+	var err error
+	if dstPath != "-" {
+		// Get an absolute destination path.
+		dstPath, err = resolveLocalPath(dstPath)
+		if err != nil {
+			return err
+		}
+	}
+
+	if err := command.ValidateOutputPath(dstPath); err != nil {
+		return err
+	}
+
+	// if client requests to follow symbol link, then must decide target file to be copied
+	var rebaseName string
+	if opts.FollowLink {
+		srcStat, err := s.apiClient.ContainerStatPath(ctx, containerID, srcPath)
+
+		// If the destination is a symbolic link, we should follow it.
+		if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
+			linkTarget := srcStat.LinkTarget
+			if !system.IsAbs(linkTarget) {
+				// Join with the parent directory.
+				srcParent, _ := archive.SplitPathDirEntry(srcPath)
+				linkTarget = filepath.Join(srcParent, linkTarget)
+			}
+
+			linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget)
+			srcPath = linkTarget
+		}
+	}
+
+	content, stat, err := s.apiClient.CopyFromContainer(ctx, containerID, srcPath)
+	if err != nil {
+		return err
+	}
+	defer content.Close() //nolint:errcheck
+
+	if dstPath == "-" {
+		_, err = io.Copy(os.Stdout, content)
+		return err
+	}
+
+	srcInfo := archive.CopyInfo{
+		Path:       srcPath,
+		Exists:     true,
+		IsDir:      stat.Mode.IsDir(),
+		RebaseName: rebaseName,
+	}
+
+	preArchive := content
+	if len(srcInfo.RebaseName) != 0 {
+		_, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
+		preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
+	}
+
+	return archive.CopyTo(preArchive, srcInfo, dstPath)
+}
+
+func splitCpArg(arg string) (container, path string) {
+	if system.IsAbs(arg) {
+		// Explicit local absolute path, e.g., `C:\foo` or `/foo`.
+		return "", arg
+	}
+
+	parts := strings.SplitN(arg, ":", 2)
+
+	if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {
+		// Either there's no `:` in the arg
+		// OR it's an explicit local relative path like `./file:name.txt`.
+		return "", arg
+	}
+
+	return parts[0], parts[1]
+}
+
+func resolveLocalPath(localPath string) (absPath string, err error) {
+	if absPath, err = filepath.Abs(localPath); err != nil {
+		return
+	}
+	return archive.PreserveTrailingDotOrSeparator(absPath, localPath, filepath.Separator), nil
+}