Skip to content

Commit

Permalink
Merge remote-tracking branch 'original/main'
Browse files Browse the repository at this point in the history
  • Loading branch information
catinapoke committed May 10, 2024
2 parents 9b6537d + df8c549 commit 15f4ca3
Show file tree
Hide file tree
Showing 212 changed files with 3,980 additions and 1,896 deletions.
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Expand Up @@ -2,7 +2,7 @@
// README at: https://github.com/devcontainers/templates/tree/main/src/go
{
"name": "Go",
"image": "mcr.microsoft.com/devcontainers/go:0-1.21-bullseye",
"image": "mcr.microsoft.com/devcontainers/go:1.21-bookworm",

// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
Expand Down
3 changes: 3 additions & 0 deletions .github/FUNDING.yml
@@ -0,0 +1,3 @@
# These are supported funding model platforms

github: [testcontainers]
6 changes: 4 additions & 2 deletions .github/workflows/ci-test-go.yml
Expand Up @@ -50,6 +50,8 @@ jobs:
continue-on-error: ${{ !inputs.fail-fast }}
env:
TESTCONTAINERS_RYUK_DISABLED: "${{ inputs.ryuk-disabled }}"
RYUK_CONNECTION_TIMEOUT: "${{ inputs.project-directory == 'modules/compose' && '5m' || '60s' }}"
RYUK_RECONNECTION_TIMEOUT: "${{ inputs.project-directory == 'modules/compose' && '30s' || '10s' }}"
steps:
- name: Setup rootless Docker
if: ${{ inputs.rootless-docker }}
Expand All @@ -72,7 +74,7 @@ jobs:
- name: golangci-lint
# TODO: Remove each example/module once it passes the golangci-lint
if: ${{ inputs.platform == 'ubuntu-latest' && inputs.go-version == '1.20.x' }}
uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3
uses: golangci/golangci-lint-action@9d1e0624a798bb64f6c3cea93db47765312263dc # v5
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: v1.55.2
Expand Down Expand Up @@ -121,7 +123,7 @@ jobs:
./scripts/check_environment.sh
- name: Test Summary
uses: test-summary/action@fee35d7df20790255fe6aa92cf0f6d28092ecf2f # v2
uses: test-summary/action@032c8a9cec6aaa3c20228112cae6ca10a3b29336 # v2.3
with:
paths: "**/${{ inputs.project-directory }}/TEST-unit*.xml"
if: always()
1 change: 1 addition & 0 deletions .golangci.yml
Expand Up @@ -7,6 +7,7 @@ linters:
- misspell
- nonamedreturns
- testifylint
- errcheck

linters-settings:
errorlint:
Expand Down
2 changes: 1 addition & 1 deletion Pipfile
Expand Up @@ -9,7 +9,7 @@ verify_ssl = true
mkdocs = "==1.5.3"
mkdocs-codeinclude-plugin = "==0.2.1"
mkdocs-include-markdown-plugin = "==6.0.4"
mkdocs-material = "==9.5.13"
mkdocs-material = "==9.5.18"
mkdocs-markdownextradata-plugin = "==0.2.5"

[requires]
Expand Down
228 changes: 107 additions & 121 deletions Pipfile.lock

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions container.go
Expand Up @@ -39,8 +39,9 @@ type Container interface {
Endpoint(context.Context, string) (string, error) // get proto://ip:port string for the first exposed port
PortEndpoint(context.Context, nat.Port, string) (string, error) // get proto://ip:port string for the given exposed port
Host(context.Context) (string, error) // get host where the container port is exposed
Inspect(context.Context) (*types.ContainerJSON, error) // get container info
MappedPort(context.Context, nat.Port) (nat.Port, error) // get externally mapped port for a container port
Ports(context.Context) (nat.PortMap, error) // get all exposed ports
Ports(context.Context) (nat.PortMap, error) // Deprecated: Use c.Inspect(ctx).NetworkSettings.Ports instead
SessionID() string // get session id
IsRunning() bool
Start(context.Context) error // start the container
Expand All @@ -50,7 +51,7 @@ type Container interface {
FollowOutput(LogConsumer) // Deprecated: it will be removed in the next major release
StartLogProducer(context.Context, ...LogProductionOption) error // Deprecated: Use the ContainerRequest instead
StopLogProducer() error // Deprecated: it will be removed in the next major release
Name(context.Context) (string, error) // get container name
Name(context.Context) (string, error) // Deprecated: Use c.Inspect(ctx).Name instead
State(context.Context) (*types.ContainerState, error) // returns container's running state
Networks(context.Context) ([]string, error) // get container networks
NetworkAliases(context.Context) (map[string][]string, error) // get container network aliases for a network
Expand Down Expand Up @@ -121,6 +122,7 @@ func (c *ContainerFile) validate() error {
// ContainerRequest represents the parameters used to get a running container
type ContainerRequest struct {
FromDockerfile
HostAccessPorts []int
Image string
ImageSubstitutors []ImageSubstitutor
Entrypoint []string
Expand Down
111 changes: 84 additions & 27 deletions docker.go
Expand Up @@ -98,6 +98,11 @@ func (c *DockerContainer) SetProvider(provider *DockerProvider) {
c.provider = provider
}

// SetTerminationSignal sets the termination signal for the container
func (c *DockerContainer) SetTerminationSignal(signal chan bool) {
c.terminationSignal = signal
}

func (c *DockerContainer) GetContainerID() string {
return c.ID
}
Expand All @@ -109,11 +114,13 @@ func (c *DockerContainer) IsRunning() bool {
// Endpoint gets proto://host:port string for the first exposed port
// Will returns just host:port if proto is ""
func (c *DockerContainer) Endpoint(ctx context.Context, proto string) (string, error) {
ports, err := c.Ports(ctx)
inspect, err := c.Inspect(ctx)
if err != nil {
return "", err
}

ports := inspect.NetworkSettings.Ports

// get first port
var firstPort nat.Port
for p := range ports {
Expand Down Expand Up @@ -156,19 +163,31 @@ func (c *DockerContainer) Host(ctx context.Context) (string, error) {
return host, nil
}

// Inspect gets the raw container info, caching the result for subsequent calls
func (c *DockerContainer) Inspect(ctx context.Context) (*types.ContainerJSON, error) {
if c.raw != nil {
return c.raw, nil
}

json, err := c.inspectRawContainer(ctx)
if err != nil {
return nil, err
}

return json, nil
}

// MappedPort gets externally mapped port for a container port
func (c *DockerContainer) MappedPort(ctx context.Context, port nat.Port) (nat.Port, error) {
inspect, err := c.inspectContainer(ctx)
inspect, err := c.Inspect(ctx)
if err != nil {
return "", err
}
if inspect.ContainerJSONBase.HostConfig.NetworkMode == "host" {
return port, nil
}
ports, err := c.Ports(ctx)
if err != nil {
return "", err
}

ports := inspect.NetworkSettings.Ports

for k, p := range ports {
if k.Port() != port.Port() {
Expand All @@ -186,9 +205,10 @@ func (c *DockerContainer) MappedPort(ctx context.Context, port nat.Port) (nat.Po
return "", errors.New("port not found")
}

// Deprecated: use c.Inspect(ctx).NetworkSettings.Ports instead.
// Ports gets the exposed ports for the container.
func (c *DockerContainer) Ports(ctx context.Context) (nat.PortMap, error) {
inspect, err := c.inspectContainer(ctx)
inspect, err := c.Inspect(ctx)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -255,6 +275,7 @@ func (c *DockerContainer) Stop(ctx context.Context, timeout *time.Duration) erro
defer c.provider.Close()

c.isRunning = false
c.raw = nil // invalidate the cache, as the container representation will change after stopping

err = c.stoppedHook(ctx)
if err != nil {
Expand Down Expand Up @@ -293,6 +314,7 @@ func (c *DockerContainer) Terminate(ctx context.Context) error {

c.sessionID = ""
c.isRunning = false
c.raw = nil // invalidate the cache here too
return errors.Join(errs...)
}

Expand All @@ -308,16 +330,6 @@ func (c *DockerContainer) inspectRawContainer(ctx context.Context) (*types.Conta
return c.raw, nil
}

func (c *DockerContainer) inspectContainer(ctx context.Context) (*types.ContainerJSON, error) {
defer c.provider.Close()
inspect, err := c.provider.client.ContainerInspect(ctx, c.ID)
if err != nil {
return nil, err
}

return &inspect, nil
}

// Logs will fetch both STDOUT and STDERR from the current container. Returns a
// ReadCloser and leaves it up to the caller to extract what it wants.
func (c *DockerContainer) Logs(ctx context.Context) (io.ReadCloser, error) {
Expand Down Expand Up @@ -383,16 +395,18 @@ func (c *DockerContainer) followOutput(consumer LogConsumer) {
c.consumers = append(c.consumers, consumer)
}

// Deprecated: use c.Inspect(ctx).Name instead.
// Name gets the name of the container.
func (c *DockerContainer) Name(ctx context.Context) (string, error) {
inspect, err := c.inspectContainer(ctx)
inspect, err := c.Inspect(ctx)
if err != nil {
return "", err
}
return inspect.Name, nil
}

// State returns container's running state
// State returns container's running state. This method does not use the cache
// and always fetches the latest state from the Docker daemon.
func (c *DockerContainer) State(ctx context.Context) (*types.ContainerState, error) {
inspect, err := c.inspectRawContainer(ctx)
if err != nil {
Expand All @@ -406,7 +420,7 @@ func (c *DockerContainer) State(ctx context.Context) (*types.ContainerState, err

// Networks gets the names of the networks the container is attached to.
func (c *DockerContainer) Networks(ctx context.Context) ([]string, error) {
inspect, err := c.inspectContainer(ctx)
inspect, err := c.Inspect(ctx)
if err != nil {
return []string{}, err
}
Expand All @@ -424,7 +438,7 @@ func (c *DockerContainer) Networks(ctx context.Context) ([]string, error) {

// ContainerIP gets the IP address of the primary network within the container.
func (c *DockerContainer) ContainerIP(ctx context.Context) (string, error) {
inspect, err := c.inspectContainer(ctx)
inspect, err := c.Inspect(ctx)
if err != nil {
return "", err
}
Expand All @@ -447,7 +461,7 @@ func (c *DockerContainer) ContainerIP(ctx context.Context) (string, error) {
func (c *DockerContainer) ContainerIPs(ctx context.Context) ([]string, error) {
ips := make([]string, 0)

inspect, err := c.inspectContainer(ctx)
inspect, err := c.Inspect(ctx)
if err != nil {
return nil, err
}
Expand All @@ -462,7 +476,7 @@ func (c *DockerContainer) ContainerIPs(ctx context.Context) ([]string, error) {

// NetworkAliases gets the aliases of the container for the networks it is attached to.
func (c *DockerContainer) NetworkAliases(ctx context.Context) (map[string][]string, error) {
inspect, err := c.inspectContainer(ctx)
inspect, err := c.Inspect(ctx)
if err != nil {
return map[string][]string{}, err
}
Expand All @@ -478,6 +492,13 @@ func (c *DockerContainer) NetworkAliases(ctx context.Context) (map[string][]stri
return a, nil
}

// Exec executes a command in the current container.
// It returns the exit status of the executed command, an [io.Reader] containing the combined
// stdout and stderr, and any encountered error. Note that reading directly from the [io.Reader]
// may result in unexpected bytes due to custom stream multiplexing headers.
// Use [tcexec.Multiplexed] option to read the combined output without the multiplexing headers.
// Alternatively, to separate the stdout and stderr from [io.Reader] and interpret these headers properly,
// [github.com/docker/docker/pkg/stdcopy.StdCopy] from the Docker API should be used.
func (c *DockerContainer) Exec(ctx context.Context, cmd []string, options ...tcexec.ProcessOption) (int, io.Reader, error) {
cli := c.provider.client

Expand Down Expand Up @@ -846,6 +867,10 @@ func (n *DockerNetwork) Remove(ctx context.Context) error {
return n.provider.client.NetworkRemove(ctx, n.ID)
}

func (n *DockerNetwork) SetTerminationSignal(signal chan bool) {
n.terminationSignal = signal
}

// DockerProvider implements the ContainerProvider interface
type DockerProvider struct {
*DockerProviderOptions
Expand Down Expand Up @@ -886,8 +911,7 @@ func (p *DockerProvider) BuildImage(ctx context.Context, img ImageBuildInfo) (st
resp, err = p.client.ImageBuild(ctx, buildOptions.Context, buildOptions)
if err != nil {
buildError = errors.Join(buildError, err)
var enf errdefs.ErrNotFound
if errors.As(err, &enf) {
if isPermanentClientError(err) {
return backoff.Permanent(err)
}
Logger.Printf("Failed to build image: %s, will retry", err)
Expand Down Expand Up @@ -1088,6 +1112,20 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
defaultReadinessHook(),
}

// in the case the container needs to access a local port
// we need to forward the local port to the container
if len(req.HostAccessPorts) > 0 {
// a container lifecycle hook will be added, which will expose the host ports to the container
// using a SSHD server running in a container. The SSHD server will be started and will
// forward the host ports to the container ports.
sshdForwardPortsHook, err := exposeHostPorts(ctx, &req, req.HostAccessPorts...)
if err != nil {
return nil, fmt.Errorf("failed to expose host ports: %w", err)
}

defaultHooks = append(defaultHooks, sshdForwardPortsHook)
}

req.LifecycleHooks = []ContainerLifecycleHooks{combineContainerHooks(defaultHooks, req.LifecycleHooks)}

err = req.creatingHook(ctx)
Expand Down Expand Up @@ -1166,6 +1204,9 @@ func (p *DockerProvider) waitContainerCreation(ctx context.Context, name string)
return container, backoff.Retry(func() error {
c, err := p.findContainerByName(ctx, name)
if err != nil {
if !errdefs.IsNotFound(err) && isPermanentClientError(err) {
return backoff.Permanent(err)
}
return err
}

Expand Down Expand Up @@ -1266,8 +1307,7 @@ func (p *DockerProvider) attemptToPullImage(ctx context.Context, tag string, pul
err = backoff.Retry(func() error {
pull, err = p.client.ImagePull(ctx, tag, pullOpt)
if err != nil {
var enf errdefs.ErrNotFound
if errors.As(err, &enf) {
if isPermanentClientError(err) {
return backoff.Permanent(err)
}
Logger.Printf("Failed to pull image: %s, will retry", err)
Expand Down Expand Up @@ -1603,3 +1643,20 @@ func (p *DockerProvider) SaveImages(ctx context.Context, output string, images .
func (p *DockerProvider) PullImage(ctx context.Context, image string) error {
return p.attemptToPullImage(ctx, image, types.ImagePullOptions{})
}

var permanentClientErrors = []func(error) bool{
errdefs.IsNotFound,
errdefs.IsInvalidParameter,
errdefs.IsUnauthorized,
errdefs.IsForbidden,
errdefs.IsNotImplemented,
}

func isPermanentClientError(err error) bool {
for _, isErrFn := range permanentClientErrors {
if isErrFn(err) {
return true
}
}
return false
}
13 changes: 11 additions & 2 deletions docker_auth.go
Expand Up @@ -13,11 +13,14 @@ import (
"github.com/testcontainers/testcontainers-go/internal/core"
)

// defaultRegistryFn is variable overwritten in tests to check for behaviour with different default values.
var defaultRegistryFn = defaultRegistry

// DockerImageAuth returns the auth config for the given Docker image, extracting first its Docker registry.
// Finally, it will use the credential helpers to extract the information from the docker config file
// for that registry, if it exists.
func DockerImageAuth(ctx context.Context, image string) (string, registry.AuthConfig, error) {
defaultRegistry := defaultRegistry(ctx)
defaultRegistry := defaultRegistryFn(ctx)
reg := core.ExtractRegistry(image, defaultRegistry)

cfgs, err := getDockerAuthConfigs()
Expand All @@ -44,7 +47,13 @@ func getRegistryAuth(reg string, cfgs map[string]registry.AuthConfig) (registry.
continue
}

if keyURL.Host == reg {
host := keyURL.Host
if keyURL.Scheme == "" {
// url.Parse: The url may be relative (a path, without a host) [...]
host = keyURL.Path
}

if host == reg {
return cfg, true
}
}
Expand Down

0 comments on commit 15f4ca3

Please sign in to comment.