Skip to content

Commit 2c6196f

Browse files
committed
feat: copy from host to container
Add CopyHostPathTo to container methods, which is capable of copying files and directories from the host to a container. It identifies the correct copy semantics based on inspecting the source and target, replicating the behaviour of docker cp and other OS copy tools. This deprecates CopyDirToContainer and CopyFileToContainer while still correcting their behaviour to also match docker cp behaviour. Replace nonamedreturns linter with nakedret, as nonamedreturns prevents naming returned parameters which has a number of valid uses including: disambiguating return values and error checking. Copying files to the container now doesn't compression as this is slower and consumes more resources for the typical local transfer case. Fix docker copy tests to they validate the correct behaviour by ensuring that done is reported to the container log. Clean up some error wrapping. Fix testdata wait scripts. Fix invalid FileMode values, so tests don't fail with the new FileMode validation. Switch to exists check for copy instead of running it as a shell. Disable linter check for deprecated methods as we use them internally and test them. Fixes #2780
1 parent b60497e commit 2c6196f

22 files changed

+894
-477
lines changed

.golangci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ linters:
55
- gocritic
66
- gofumpt
77
- misspell
8-
- nonamedreturns
8+
- nakedret
99
- testifylint
1010
- errcheck
1111
- nolintlint

container.go

+95-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"fmt"
88
"io"
9+
"io/fs"
910
"os"
1011
"path/filepath"
1112
"strings"
@@ -63,13 +64,83 @@ type Container interface {
6364
Exec(ctx context.Context, cmd []string, options ...tcexec.ProcessOption) (int, io.Reader, error)
6465
ContainerIP(context.Context) (string, error) // get container ip
6566
ContainerIPs(context.Context) ([]string, error) // get all container IPs
66-
CopyToContainer(ctx context.Context, fileContent []byte, containerFilePath string, fileMode int64) error
67-
CopyDirToContainer(ctx context.Context, hostDirPath string, containerParentPath string, fileMode int64) error
68-
CopyFileToContainer(ctx context.Context, hostFilePath string, containerFilePath string, fileMode int64) error
67+
68+
// CopyHostPathTo copies the contents of a hostPath to containerPath in the container
69+
// with the given options.
70+
// If the parent of the containerPath does not exist an error is returned.
71+
CopyHostPathTo(ctx context.Context, hostPath, containerPath string, options ...CopyToOption) error
72+
73+
// CopyToContainer copies a file with contents of fileContent to containerPath in the
74+
// container with the given fileMode.
75+
// If fileMode contains bits not part of [fs.ModePerm] | [fs.ModeSetuid] | [fs.ModeSetgid] |
76+
// [fs.ModeSticky] an error is returned.
77+
CopyToContainer(ctx context.Context, fileContent []byte, containerPath string, fileMode int64) error
78+
79+
// CopyDirToContainer copies the contents of hostPath to containerPath in the container.
80+
// If fileMode is non-zero all files will have their file permissions set to that of fileMode
81+
// otherwise the file permissions will be copied from the host.
82+
// If fileMode contains bits not part of [fs.ModePerm] | [fs.ModeSetuid] | [fs.ModeSetgid] |
83+
// [fs.ModeSticky] an error is returned.
84+
// If the parent of the containerPath does not exist an error is returned.
85+
//
86+
// Deprecated: use [DockerContainer.CopyHostPathTo] instead.
87+
CopyDirToContainer(ctx context.Context, hostPath, containerPath string, fileMode int64) error
88+
89+
// CopyFileToContainer copies hostPath to containerPath in the container.
90+
// If fileMode is non-zero the files permissions will be set to that of fileMode
91+
// otherwise the file permissions will be set to that of the file in hostPath.
92+
// If fileMode contains bits not part of [fs.ModePerm] | [fs.ModeSetuid] | [fs.ModeSetgid] |
93+
// [fs.ModeSticky] an error is returned.
94+
// If the parent of the containerPath does not exist an error is returned.
95+
// If hostPath is a directory this is equivalent to [DockerContainer.CopyDirToContainer].
96+
//
97+
// Deprecated: use [DockerContainer.CopyHostPathTo] instead.
98+
CopyFileToContainer(ctx context.Context, hostPath string, containerPath string, fileMode int64) error
99+
69100
CopyFileFromContainer(ctx context.Context, filePath string) (io.ReadCloser, error)
70101
GetLogProductionErrorChannel() <-chan error
71102
}
72103

104+
// copyToOptions contains options for the copy operation.
105+
type copyToOptions struct {
106+
// followLink instructs the copy operation to follow symlinks.
107+
followLink bool
108+
109+
// copyUIDGID instructs the copy operation to copy the UID and GID of the source file.
110+
copyUIDGID bool
111+
112+
// allowOverwriteDirWithFile instructs the copy operation to allow overwriting a directory with a file.
113+
allowOverwriteDirWithFile bool
114+
115+
// fileMode if not zero, instructs the copy operation to override the file permissions.
116+
fileMode fs.FileMode
117+
}
118+
119+
// CopyToOption represents a option for CopyTo methods.
120+
type CopyToOption func(*copyToOptions)
121+
122+
// CopyToFollowLink instructs the copy operation to follow symlinks
123+
// when identifying the source.
124+
func CopyToFollowLink() CopyToOption {
125+
return func(o *copyToOptions) {
126+
o.followLink = true
127+
}
128+
}
129+
130+
// CopyToUIDGID instructs the copy operation to copy the UID and GID of the source.
131+
func CopyToUIDGID() CopyToOption {
132+
return func(o *copyToOptions) {
133+
o.copyUIDGID = true
134+
}
135+
}
136+
137+
// CopyToAllowOverwriteDirWithFile instructs the copy operation to allow overwriting a directory with a file.
138+
func CopyToAllowOverwriteDirWithFile() CopyToOption {
139+
return func(o *copyToOptions) {
140+
o.allowOverwriteDirWithFile = true
141+
}
142+
}
143+
73144
// ImageBuildInfo defines what is needed to build an image
74145
type ImageBuildInfo interface {
75146
BuildOptions() (types.ImageBuildOptions, error) // converts the ImageBuildInfo to a types.ImageBuildOptions
@@ -104,11 +175,29 @@ type FromDockerfile struct {
104175
BuildOptionsModifier func(*types.ImageBuildOptions)
105176
}
106177

178+
// ContainerMount represents a file or directory to be copied into a container on startup.
107179
type ContainerFile struct {
108-
HostFilePath string // If Reader is present, HostFilePath is ignored
109-
Reader io.Reader // If Reader is present, HostFilePath is ignored
180+
// HostFilePath is the path to the file on the host machine.
181+
// If Reader is present it is ignored.
182+
// TODO: Rename to HostPath as HostFilePath infers its a file and it could be a
183+
// directory.
184+
HostFilePath string
185+
186+
// Reader provides the file content to be copied to the container.
187+
// If present, HostFilePath is ignored.
188+
Reader io.Reader
189+
190+
// ContainerFilePath is the path where this file will be copied to in the container.
191+
// TODO: Rename to ContainerPath as ContainerFilePath infers its a file and it could
192+
// be a directory.
110193
ContainerFilePath string
111-
FileMode int64
194+
195+
// FileMode is the file mode to set on the file in the container.
196+
// Must be set if Reader is present.
197+
// If zero or not set, the file mode will be that of the host file.
198+
// TODO: Should we only use FileMode for Reader, as it makes more sense to use the
199+
// source file permissions when using a host path source?
200+
FileMode int64
112201
}
113202

114203
// validate validates the ContainerFile

docker.go

+49-55
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"net"
1515
"net/url"
1616
"os"
17-
"path/filepath"
1817
"regexp"
1918
"strings"
2019
"sync"
@@ -602,92 +601,87 @@ func (c *DockerContainer) CopyFileFromContainer(ctx context.Context, filePath st
602601
return ret, nil
603602
}
604603

605-
// CopyDirToContainer copies the contents of a directory to a parent path in the container. This parent path must exist in the container first
606-
// as we cannot create it
607-
func (c *DockerContainer) CopyDirToContainer(ctx context.Context, hostDirPath string, containerParentPath string, fileMode int64) error {
608-
dir, err := isDir(hostDirPath)
609-
if err != nil {
604+
// CopyDirToContainer copies the contents of hostPath to containerPath in the container.
605+
// If fileMode is non-zero all files will have their file permissions set to that of fileMode
606+
// otherwise the file permissions will be copied from the host.
607+
// If fileMode contains bits not part of [fs.ModePerm] | [fs.ModeSetuid] | [fs.ModeSetgid] |
608+
// [fs.ModeSticky] an error is returned.
609+
// If the parent of the containerPath does not exist an error is returned.
610+
//
611+
// Deprecated: use [DockerContainer.CopyHostPathTo] instead.
612+
func (c *DockerContainer) CopyDirToContainer(ctx context.Context, hostPath string, containerPath string, fileMode int64) error {
613+
if err := validateFileMode(fileMode); err != nil {
610614
return err
611615
}
612616

613-
if !dir {
614-
// it's not a dir: let the consumer to handle an error
615-
return fmt.Errorf("path %s is not a directory", hostDirPath)
616-
}
617-
618-
buff, err := tarDir(hostDirPath, fileMode)
617+
dir, err := isDir(hostPath)
619618
if err != nil {
620619
return err
621620
}
622621

623-
// create the directory under its parent
624-
parent := filepath.Dir(containerParentPath)
625-
626-
err = c.provider.client.CopyToContainer(ctx, c.ID, parent, buff, container.CopyToContainerOptions{})
627-
if err != nil {
628-
return err
622+
if !dir {
623+
// It's not a dir: let the consumer to handle an error.
624+
return fmt.Errorf("host dir path %q is not a directory", hostPath)
629625
}
630-
defer c.provider.Close()
631626

632-
return nil
627+
return c.CopyHostPathTo(ctx, hostPath, containerPath, copyToFileMode(fileMode))
633628
}
634629

635-
func (c *DockerContainer) CopyFileToContainer(ctx context.Context, hostFilePath string, containerFilePath string, fileMode int64) error {
636-
dir, err := isDir(hostFilePath)
630+
// CopyFileToContainer copies hostPath to containerPath in the container.
631+
// If fileMode is non-zero the files permissions will be set to that of fileMode
632+
// otherwise the file permissions will be set to that of the file in hostPath.
633+
// If fileMode contains bits not part of [fs.ModePerm] | [fs.ModeSetuid] | [fs.ModeSetgid] |
634+
// [fs.ModeSticky] an error is returned.
635+
// If the parent of the containerPath does not exist an error is returned.
636+
// If hostPath is a directory this is equivalent to [DockerContainer.CopyDirToContainer].
637+
//
638+
// Deprecated: use [DockerContainer.CopyHostPathTo] instead.
639+
func (c *DockerContainer) CopyFileToContainer(ctx context.Context, hostPath string, containerPath string, fileMode int64) error {
640+
dir, err := isDir(hostPath)
637641
if err != nil {
638642
return err
639643
}
640644

641645
if dir {
642-
return c.CopyDirToContainer(ctx, hostFilePath, containerFilePath, fileMode)
643-
}
644-
645-
f, err := os.Open(hostFilePath)
646-
if err != nil {
647-
return err
646+
return c.CopyDirToContainer(ctx, hostPath, containerPath, fileMode)
648647
}
649-
defer f.Close()
650648

651-
info, err := f.Stat()
649+
data, err := os.ReadFile(hostPath)
652650
if err != nil {
653-
return err
651+
return fmt.Errorf("read file: %w", err)
654652
}
655653

656-
// In Go 1.22 os.File is always an io.WriterTo. However, testcontainers
657-
// currently allows Go 1.21, so we need to trick the compiler a little.
658-
var file fs.File = f
659-
return c.copyToContainer(ctx, func(tw io.Writer) error {
660-
// Attempt optimized writeTo, implemented in linux
661-
if wt, ok := file.(io.WriterTo); ok {
662-
_, err := wt.WriteTo(tw)
663-
return err
654+
if fileMode == 0 {
655+
fi, err := os.Stat(hostPath)
656+
if err != nil {
657+
return fmt.Errorf("stat file: %w", err)
664658
}
665-
_, err := io.Copy(tw, f)
666-
return err
667-
}, info.Size(), containerFilePath, fileMode)
668-
}
669659

670-
// CopyToContainer copies fileContent data to a file in container
671-
func (c *DockerContainer) CopyToContainer(ctx context.Context, fileContent []byte, containerFilePath string, fileMode int64) error {
672-
return c.copyToContainer(ctx, func(tw io.Writer) error {
673-
_, err := tw.Write(fileContent)
674-
return err
675-
}, int64(len(fileContent)), containerFilePath, fileMode)
660+
fileMode = int64(fi.Mode())
661+
}
662+
663+
return c.CopyToContainer(ctx, data, containerPath, fileMode)
676664
}
677665

678-
func (c *DockerContainer) copyToContainer(ctx context.Context, fileContent func(tw io.Writer) error, fileContentSize int64, containerFilePath string, fileMode int64) error {
679-
buffer, err := tarFile(containerFilePath, fileContent, fileContentSize, fileMode)
680-
if err != nil {
666+
// CopyToContainer copies a file with contents of fileContent to containerPath in the
667+
// container with the given fileMode.
668+
// If fileMode contains bits not part of [fs.ModePerm] | [fs.ModeSetuid] | [fs.ModeSetgid] |
669+
// [fs.ModeSticky] an error is returned.
670+
func (c *DockerContainer) CopyToContainer(ctx context.Context, fileContent []byte, containerPath string, fileMode int64) error {
671+
if err := validateFileMode(fileMode); err != nil {
681672
return err
682673
}
683674

684-
err = c.provider.client.CopyToContainer(ctx, c.ID, "/", buffer, container.CopyToContainerOptions{})
675+
// Create a tar with the single file containing the a file with the
676+
// fully qualified container path as the name.
677+
tar, err := tarFile(containerPath, fileContent, fs.FileMode(fileMode))
685678
if err != nil {
686679
return err
687680
}
688-
defer c.provider.Close()
689681

690-
return nil
682+
// As the name of the file in the tar is the fully qualified container path
683+
// it should by extracted in the container's root directory.
684+
return c.copyTarTo(ctx, tar, "/")
691685
}
692686

693687
type LogProductionOption func(*DockerContainer)

0 commit comments

Comments
 (0)