Skip to content

Commit

Permalink
Merge branch 'main' into user-defined-lifecycle-hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
mdelapenya authored Jan 11, 2024
2 parents d8316b7 + 62d6214 commit 474ddea
Show file tree
Hide file tree
Showing 116 changed files with 1,511 additions and 1,408 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci-test-go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4

- name: Set up Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5
with:
go-version: '${{ inputs.go-version }}'
cache-dependency-path: '${{ inputs.project-directory }}/go.sum'
Expand Down Expand Up @@ -123,7 +123,7 @@ jobs:
./scripts/check_environment.sh
- name: Test Summary
uses: test-summary/action@62bc5c68de2a6a0d02039763b8c754569df99e3f # v2
uses: test-summary/action@fee35d7df20790255fe6aa92cf0f6d28092ecf2f # v2
with:
paths: "**/${{ inputs.project-directory }}/TEST-unit*.xml"
if: always()
2 changes: 1 addition & 1 deletion .github/workflows/ci-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
ref: ${{ github.event.client_payload.pull_request.head.ref }}

- name: Set up Go
uses: actions/setup-go@v4
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5
with:
go-version-file: go.mod
id: go
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 # v3.23.0
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
Expand All @@ -64,7 +64,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2
uses: github/codeql-action/autobuild@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 # v3.23.0

# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
Expand All @@ -77,6 +77,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@49abf0ba24d0b7953cb586944e918a0b92074c80 # v2
uses: github/codeql-action/analyze@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 # v3.23.0
with:
category: "/language:${{matrix.language}}"
2 changes: 1 addition & 1 deletion .github/workflows/scorecards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@ jobs:

# required for Code scanning alerts
- name: "Upload SARIF results to code scanning"
uses: github/codeql-action/upload-sarif@83f0fe6c4988d98a455712a27f0255212bba9bd4 # v2.3.6
uses: github/codeql-action/upload-sarif@1500a131381b66de0c52ac28abb13cd79f4b7ecc # v2.22.12
with:
sarif_file: results.sarif
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
include ./commons-test.mk

.PHONY: test-all
test-all: tools test-unit
test-all: tools test-tools test-unit

.PHONY: test-examples
test-examples:
Expand All @@ -11,4 +11,4 @@ test-examples:
.PHONY: tidy-all
tidy-all:
make -C examples tidy-examples
make -C modules tidy-modules
make -C modules tidy-modules
4 changes: 4 additions & 0 deletions commons-test.mk
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ test-%:
tools:
go mod download

.PHONY: test-tools
test-tools:
go install gotest.tools/gotestsum@latest

.PHONY: tools-tidy
tools-tidy:
go mod tidy
7 changes: 4 additions & 3 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
"github.com/moby/patternmatcher/ignorefile"

tcexec "github.com/testcontainers/testcontainers-go/exec"
"github.com/testcontainers/testcontainers-go/internal/testcontainersdocker"
"github.com/testcontainers/testcontainers-go/internal/core"
"github.com/testcontainers/testcontainers-go/wait"
)

Expand Down Expand Up @@ -48,7 +48,7 @@ type Container interface {
Terminate(context.Context) error // terminate the container
Logs(context.Context) (io.ReadCloser, error) // Get logs of the container
FollowOutput(LogConsumer)
StartLogProducer(context.Context) error
StartLogProducer(context.Context, ...LogProducerOption) error
StopLogProducer() error
Name(context.Context) (string, error) // get container name
State(context.Context) (*types.ContainerState, error) // returns container's running state
Expand All @@ -61,6 +61,7 @@ type Container interface {
CopyDirToContainer(ctx context.Context, hostDirPath string, containerParentPath string, fileMode int64) error
CopyFileToContainer(ctx context.Context, hostFilePath string, containerFilePath string, fileMode int64) error
CopyFileFromContainer(ctx context.Context, filePath string) (io.ReadCloser, error)
GetLogProducerErrorChannel() <-chan error
}

// ImageBuildInfo defines what is needed to build an image
Expand Down Expand Up @@ -272,7 +273,7 @@ func (c *ContainerRequest) GetAuthConfigs() map[string]registry.AuthConfig {

// getAuthConfigsFromDockerfile returns the auth configs to be able to pull from an authenticated docker registry
func getAuthConfigsFromDockerfile(c *ContainerRequest) map[string]registry.AuthConfig {
images, err := testcontainersdocker.ExtractImagesFromDockerfile(filepath.Join(c.Context, c.GetDockerfile()), c.GetBuildArgs())
images, err := core.ExtractImagesFromDockerfile(filepath.Join(c.Context, c.GetDockerfile()), c.GetBuildArgs())
if err != nil {
return map[string]registry.AuthConfig{}
}
Expand Down
118 changes: 87 additions & 31 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"time"

"github.com/cenkalti/backoff/v4"
Expand All @@ -32,8 +33,7 @@ import (

tcexec "github.com/testcontainers/testcontainers-go/exec"
"github.com/testcontainers/testcontainers-go/internal/config"
"github.com/testcontainers/testcontainers-go/internal/testcontainersdocker"
"github.com/testcontainers/testcontainers-go/internal/testcontainerssession"
"github.com/testcontainers/testcontainers-go/internal/core"
"github.com/testcontainers/testcontainers-go/wait"
)

Expand Down Expand Up @@ -67,6 +67,9 @@ type DockerContainer struct {
raw *types.ContainerJSON
stopProducer chan bool
producerDone chan bool
producerError chan error
producerMutex sync.Mutex
producerTimeout *time.Duration
logger Logging
lifecycleHooks []ContainerLifecycleHooks
}
Expand Down Expand Up @@ -632,19 +635,68 @@ func (c *DockerContainer) CopyToContainer(ctx context.Context, fileContent []byt
return nil
}

type LogProducerOption func(*DockerContainer)

// WithLogProducerTimeout is a functional option that sets the timeout for the log producer.
// If the timeout is lower than 5s or greater than 60s it will be set to 5s or 60s respectively.
func WithLogProducerTimeout(timeout time.Duration) LogProducerOption {
return func(c *DockerContainer) {
c.producerTimeout = &timeout
}
}

// StartLogProducer will start a concurrent process that will continuously read logs
// from the container and will send them to each added LogConsumer
func (c *DockerContainer) StartLogProducer(ctx context.Context) error {
if c.stopProducer != nil {
return errors.New("log producer already started")
// from the container and will send them to each added LogConsumer.
// Default log producer timeout is 5s. It is used to set the context timeout
// which means that each log-reading loop will last at least the specified timeout
// and that it cannot be cancelled earlier.
// Use functional option WithLogProducerTimeout() to override default timeout. If it's
// lower than 5s and greater than 60s it will be set to 5s or 60s respectively.
func (c *DockerContainer) StartLogProducer(ctx context.Context, opts ...LogProducerOption) error {
{
c.producerMutex.Lock()
defer c.producerMutex.Unlock()

if c.stopProducer != nil {
return errors.New("log producer already started")
}
}

for _, opt := range opts {
opt(c)
}

minProducerTimeout := time.Duration(5 * time.Second)
maxProducerTimeout := time.Duration(60 * time.Second)

if c.producerTimeout == nil {
c.producerTimeout = &minProducerTimeout
}

if *c.producerTimeout < minProducerTimeout {
c.producerTimeout = &minProducerTimeout
}

if *c.producerTimeout > maxProducerTimeout {
c.producerTimeout = &maxProducerTimeout
}

c.stopProducer = make(chan bool)
c.producerDone = make(chan bool)
c.producerError = make(chan error, 1)

go func(stop <-chan bool, done chan<- bool) {
go func(stop <-chan bool, done chan<- bool, errorCh chan error) {
// signal the producer is done once go routine exits, this prevents race conditions around start/stop
defer close(done)
// set c.stopProducer to nil so that it can be started again
defer func() {
defer c.producerMutex.Unlock()
close(done)
close(errorCh)
{
c.producerMutex.Lock()
c.stopProducer = nil
}
}()

since := ""
// if the socket is closed we will make additional logs request with updated Since timestamp
Expand All @@ -656,25 +708,20 @@ func (c *DockerContainer) StartLogProducer(ctx context.Context) error {
Since: since,
}

ctx, cancel := context.WithTimeout(ctx, time.Second*5)
ctx, cancel := context.WithTimeout(ctx, *c.producerTimeout)
defer cancel()

r, err := c.provider.client.ContainerLogs(ctx, c.GetContainerID(), options)
if err != nil {
// if we can't get the logs, panic, we can't return an error to anything
// from within this goroutine
panic(err)
errorCh <- err
return
}
defer c.provider.Close()

for {
select {
case <-stop:
err := r.Close()
if err != nil {
// we can't close the read closer, this should never happen
panic(err)
}
errorCh <- r.Close()
return
default:
h := make([]byte, 8)
Expand Down Expand Up @@ -730,24 +777,33 @@ func (c *DockerContainer) StartLogProducer(ctx context.Context) error {
}
}
}
}(c.stopProducer, c.producerDone)
}(c.stopProducer, c.producerDone, c.producerError)

return nil
}

// StopLogProducer will stop the concurrent process that is reading logs
// and sending them to each added LogConsumer
func (c *DockerContainer) StopLogProducer() error {
c.producerMutex.Lock()
defer c.producerMutex.Unlock()
if c.stopProducer != nil {
c.stopProducer <- true
// block until the producer is actually done in order to avoid strange races
<-c.producerDone
c.stopProducer = nil
c.producerDone = nil
return <-c.producerError
}
return nil
}

// GetLogProducerErrorChannel exposes the only way for the consumer
// to be able to listen to errors and react to them.
func (c *DockerContainer) GetLogProducerErrorChannel() <-chan error {
return c.producerError
}

// DockerNetwork represents a network started using Docker
type DockerNetwork struct {
ID string // Network ID from Docker
Expand Down Expand Up @@ -897,7 +953,7 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
// the reaper does not need to start a reaper for itself
isReaperContainer := strings.HasSuffix(imageName, config.ReaperDefaultImage)
if !tcConfig.RyukDisabled && !isReaperContainer {
r, err := reuseOrCreateReaper(context.WithValue(ctx, testcontainersdocker.DockerHostContextKey, p.host), testcontainerssession.SessionID(), p)
r, err := reuseOrCreateReaper(context.WithValue(ctx, core.DockerHostContextKey, p.host), core.SessionID(), p)
if err != nil {
return nil, fmt.Errorf("%w: creating reaper failed", err)
}
Expand All @@ -919,7 +975,7 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
}

// always append the hub substitutor after the user-defined ones
req.ImageSubstitutors = append(req.ImageSubstitutors, newPrependHubRegistry())
req.ImageSubstitutors = append(req.ImageSubstitutors, newPrependHubRegistry(tcConfig.HubImageNamePrefix))

for _, is := range req.ImageSubstitutors {
modifiedTag, err := is.Substitute(imageName)
Expand Down Expand Up @@ -993,7 +1049,7 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque

if !isReaperContainer {
// add the labels that the reaper will use to terminate the container to the request
for k, v := range testcontainersdocker.DefaultLabels(testcontainerssession.SessionID()) {
for k, v := range core.DefaultLabels(core.SessionID()) {
req.Labels[k] = v
}
}
Expand Down Expand Up @@ -1060,7 +1116,7 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
Image: imageName,
imageWasBuilt: req.ShouldBuildImage(),
keepBuiltImage: req.ShouldKeepBuiltImage(),
sessionID: testcontainerssession.SessionID(),
sessionID: core.SessionID(),
provider: p,
terminationSignal: termSignal,
stopProducer: nil,
Expand Down Expand Up @@ -1107,13 +1163,13 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain
return p.CreateContainer(ctx, req)
}

sessionID := testcontainerssession.SessionID()
sessionID := core.SessionID()

tcConfig := p.Config().Config

var termSignal chan bool
if !tcConfig.RyukDisabled {
r, err := reuseOrCreateReaper(context.WithValue(ctx, testcontainersdocker.DockerHostContextKey, p.host), sessionID, p)
r, err := reuseOrCreateReaper(context.WithValue(ctx, core.DockerHostContextKey, p.host), sessionID, p)
if err != nil {
return nil, fmt.Errorf("%w: creating reaper failed", err)
}
Expand Down Expand Up @@ -1227,10 +1283,10 @@ func daemonHost(ctx context.Context, p *DockerProvider) (string, error) {
case "http", "https", "tcp":
p.hostCache = url.Hostname()
case "unix", "npipe":
if testcontainersdocker.InAContainer() {
if core.InAContainer() {
ip, err := p.GetGatewayIP(ctx)
if err != nil {
ip, err = testcontainersdocker.DefaultGatewayIP()
ip, err = core.DefaultGatewayIP()
if err != nil {
ip = "localhost"
}
Expand Down Expand Up @@ -1278,11 +1334,11 @@ func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest)
IPAM: req.IPAM,
}

sessionID := testcontainerssession.SessionID()
sessionID := core.SessionID()

var termSignal chan bool
if !tcConfig.RyukDisabled {
r, err := reuseOrCreateReaper(context.WithValue(ctx, testcontainersdocker.DockerHostContextKey, p.host), sessionID, p)
r, err := reuseOrCreateReaper(context.WithValue(ctx, core.DockerHostContextKey, p.host), sessionID, p)
if err != nil {
return nil, fmt.Errorf("%w: creating network reaper failed", err)
}
Expand All @@ -1293,7 +1349,7 @@ func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest)
}

// add the labels that the reaper will use to terminate the network to the request
for k, v := range testcontainersdocker.DefaultLabels(sessionID) {
for k, v := range core.DefaultLabels(sessionID) {
req.Labels[k] = v
}

Expand Down Expand Up @@ -1389,7 +1445,7 @@ func (p *DockerProvider) getDefaultNetwork(ctx context.Context, cli client.APICl
_, err = cli.NetworkCreate(ctx, reaperNetwork, types.NetworkCreate{
Driver: Bridge,
Attachable: true,
Labels: testcontainersdocker.DefaultLabels(testcontainerssession.SessionID()),
Labels: core.DefaultLabels(core.SessionID()),
})

if err != nil {
Expand Down Expand Up @@ -1420,7 +1476,7 @@ func containerFromDockerResponse(ctx context.Context, response types.Container)
}
container.provider = provider

container.sessionID = testcontainerssession.SessionID()
container.sessionID = core.SessionID()
container.consumers = []LogConsumer{}
container.stopProducer = nil
container.isRunning = response.State == "running"
Expand Down
Loading

0 comments on commit 474ddea

Please sign in to comment.