Skip to content

Commit

Permalink
feat: define test session semantics (testcontainers#1513)
Browse files Browse the repository at this point in the history
* feat: aggregate test executions at the sessionID level

* chore: initialise ID in a simple manner, using the init function

* fix: update tests

* fix: mod tidy modules

* chore: check if ryuk is running inspecting the container

* chore: remove usage of deprecated labels

* chore: do not pass sessionID everywhere

Instead read the already shared value

* chore: rename ID to SessionID

* chore: define RunID as an identifier of the current test process

* chore: pass RunID to reaper

* feat: use parent pid creation date as part of the hash

* chore: print out the test session and run IDs

* fix: remove unused import

* fix: remove blank line

* chore: rename to processID

* chore: go mod tidy modules

* chore: protect sessionID and processID from undesired changes

* chore: add label to Ryuk

* chore: get reaper instance from the Docker response

* fix: use sessionID as HTTP header

* fix: proper format

* chore: initialise reaper labels in a single manner

* chore: do not add labels twice

* fix: assertion was the opposite

* fix: update test

* chore: mod tidy

* fix: right logic to bootstrap the reaper if it does not exist

* fix: update test

* chore: randomise the lookup of the reaper a little bit

* chore: leverage sync.singleflight for starting the reaper just once

* chore: do not call newReaper but reuseOrRecreate reaper

* chore: back to sync.Once

* fix: make sure there is one Ryuk instance

* fix: properly set initial state for tests

* fix: properly set initial state for tests

* chore: make mockProvider restore the testing state

* fix: mockReaperProvider must reset the state of the reaper

* fix: apply reaper session ID when it exists

* chore: remove unused code

* chore: use reaper's sessionID

* chore: use reaper's sessionID

* chore: skip tests that need the reaper

* chore: mod tidy

* chore: move reaper tests to reaper_test.go

* chore: proper order

* chore: use reaper container in reused log

* chore: do not panic but instead keep returning an error

* chore: consistently read sessionID from session package of the reaper

* fix: bring back check for reaper state if found

* chore: mod tidy kafka module
  • Loading branch information
mdelapenya authored Sep 18, 2023
1 parent fcda830 commit aa3be2d
Show file tree
Hide file tree
Showing 64 changed files with 1,668 additions and 411 deletions.
108 changes: 81 additions & 27 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,10 @@ import (
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/go-connections/nat"
"github.com/google/uuid"
"github.com/moby/term"
specs "github.com/opencontainers/image-spec/specs-go/v1"

tcexec "github.com/testcontainers/testcontainers-go/exec"
"github.com/testcontainers/testcontainers-go/internal"
"github.com/testcontainers/testcontainers-go/internal/testcontainersdocker"
"github.com/testcontainers/testcontainers-go/internal/testcontainerssession"
"github.com/testcontainers/testcontainers-go/wait"
Expand Down Expand Up @@ -64,7 +62,7 @@ type DockerContainer struct {
isRunning bool
imageWasBuilt bool
provider *DockerProvider
sessionID uuid.UUID
sessionID string
terminationSignal chan bool
consumers []LogConsumer
raw *types.ContainerJSON
Expand Down Expand Up @@ -182,7 +180,7 @@ func (c *DockerContainer) Ports(ctx context.Context) (nat.PortMap, error) {

// SessionID gets the current session id
func (c *DockerContainer) SessionID() string {
return c.sessionID.String()
return c.sessionID
}

// Start will start an already created container
Expand Down Expand Up @@ -285,7 +283,7 @@ func (c *DockerContainer) Terminate(ctx context.Context) error {
}
}

c.sessionID = uuid.UUID{}
c.sessionID = ""
c.isRunning = false
return nil
}
Expand Down Expand Up @@ -879,25 +877,25 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
opt(&reaperOpts)
}

sessionID := testcontainerssession.SessionID()
if reaperInstance != nil {
sessionID = reaperInstance.SessionID
}

tcConfig := p.Config().Config

var termSignal chan bool
// the reaper does not need to start a reaper for itself
isReaperContainer := strings.EqualFold(req.Image, reaperImage(reaperOpts.ImageName))
if !tcConfig.RyukDisabled && !isReaperContainer {
r, err := reuseOrCreateReaper(context.WithValue(ctx, testcontainersdocker.DockerHostContextKey, p.host), testcontainerssession.String(), p, req.ReaperOptions...)
r, err := reuseOrCreateReaper(context.WithValue(ctx, testcontainersdocker.DockerHostContextKey, p.host), sessionID, p, req.ReaperOptions...)
if err != nil {
return nil, fmt.Errorf("%w: creating reaper failed", err)
}
termSignal, err = r.Connect()
if err != nil {
return nil, fmt.Errorf("%w: connecting to reaper failed", err)
}
for k, v := range r.Labels() {
if _, ok := req.Labels[k]; !ok {
req.Labels[k] = v
}
}
}

// Cleanup on error, otherwise set termSignal to nil before successful return.
Expand Down Expand Up @@ -972,8 +970,11 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
}
}

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

dockerInput := &container.Config{
Expand Down Expand Up @@ -1076,7 +1077,7 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
WaitingFor: req.WaitingFor,
Image: tag,
imageWasBuilt: req.ShouldBuildImage(),
sessionID: testcontainerssession.ID(),
sessionID: sessionID,
provider: p,
terminationSignal: termSignal,
stopProducer: nil,
Expand Down Expand Up @@ -1123,11 +1124,16 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain
return p.CreateContainer(ctx, req)
}

sessionID := testcontainerssession.SessionID()
if reaperInstance != nil {
sessionID = reaperInstance.SessionID
}

tcConfig := p.Config().Config

var termSignal chan bool
if !tcConfig.RyukDisabled {
r, err := reuseOrCreateReaper(context.WithValue(ctx, testcontainersdocker.DockerHostContextKey, p.host), testcontainerssession.String(), p, req.ReaperOptions...)
r, err := reuseOrCreateReaper(context.WithValue(ctx, testcontainersdocker.DockerHostContextKey, p.host), sessionID, p, req.ReaperOptions...)
if err != nil {
return nil, fmt.Errorf("%w: creating reaper failed", err)
}
Expand All @@ -1141,7 +1147,7 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain
ID: c.ID,
WaitingFor: req.WaitingFor,
Image: c.Image,
sessionID: testcontainerssession.ID(),
sessionID: sessionID,
provider: p,
terminationSignal: termSignal,
stopProducer: nil,
Expand Down Expand Up @@ -1291,21 +1297,26 @@ func (p *DockerProvider) CreateNetwork(ctx context.Context, req NetworkRequest)
IPAM: req.IPAM,
}

sessionID := testcontainerssession.SessionID()
if reaperInstance != nil {
sessionID = reaperInstance.SessionID
}

var termSignal chan bool
if !tcConfig.RyukDisabled {
r, err := reuseOrCreateReaper(context.WithValue(ctx, testcontainersdocker.DockerHostContextKey, p.host), testcontainerssession.String(), p, req.ReaperOptions...)
r, err := reuseOrCreateReaper(context.WithValue(ctx, testcontainersdocker.DockerHostContextKey, p.host), sessionID, p, req.ReaperOptions...)
if err != nil {
return nil, fmt.Errorf("%w: creating network reaper failed", err)
}
termSignal, err = r.Connect()
if err != nil {
return nil, fmt.Errorf("%w: connecting to network reaper failed", err)
}
for k, v := range r.Labels() {
if _, ok := req.Labels[k]; !ok {
req.Labels[k] = v
}
}
}

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

// Cleanup on error, otherwise set termSignal to nil before successful return.
Expand Down Expand Up @@ -1395,16 +1406,17 @@ func (p *DockerProvider) getDefaultNetwork(ctx context.Context, cli client.APICl
}
}

sessionID := testcontainerssession.SessionID()
if reaperInstance != nil {
sessionID = reaperInstance.SessionID
}

// Create a bridge network for the container communications
if !reaperNetworkExists {
_, err = cli.NetworkCreate(ctx, reaperNetwork, types.NetworkCreate{
Driver: Bridge,
Attachable: true,
Labels: map[string]string{
TestcontainerLabel: "true",
testcontainersdocker.LabelLang: "go",
testcontainersdocker.LabelVersion: internal.Version,
},
Labels: testcontainersdocker.DefaultLabels(sessionID),
})

if err != nil {
Expand All @@ -1414,3 +1426,45 @@ func (p *DockerProvider) getDefaultNetwork(ctx context.Context, cli client.APICl

return reaperNetwork, nil
}

// containerFromDockerResponse builds a Docker container struct from the response of the Docker API
func containerFromDockerResponse(ctx context.Context, response types.Container) (*DockerContainer, error) {
provider, err := NewDockerProvider()
if err != nil {
return nil, err
}

container := DockerContainer{}

container.ID = response.ID
container.WaitingFor = nil
container.Image = response.Image
container.imageWasBuilt = false

container.logger = provider.Logger
container.lifecycleHooks = []ContainerLifecycleHooks{
DefaultLoggingHook(container.logger),
}
container.provider = provider

sessionID := testcontainerssession.SessionID()
if reaperInstance != nil {
sessionID = reaperInstance.SessionID
}

container.sessionID = sessionID
container.consumers = []LogConsumer{}
container.stopProducer = nil
container.isRunning = response.State == "running"

// the termination signal should be obtained from the reaper
container.terminationSignal = nil

// populate the raw representation of the container
_, err = container.inspectRawContainer(ctx)
if err != nil {
return nil, err
}

return &container, nil
}
5 changes: 5 additions & 0 deletions docker_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/docker/docker/client"

"github.com/testcontainers/testcontainers-go/internal/testcontainersdocker"
"github.com/testcontainers/testcontainers-go/internal/testcontainerssession"
)

// DockerClient is a wrapper around the docker client that is used by testcontainers-go.
Expand Down Expand Up @@ -51,13 +52,17 @@ func (c *DockerClient) Info(ctx context.Context) (types.Info, error) {
Total Memory: %v MB
Resolved Docker Host: %s
Resolved Docker Socket Path: %s
Test SessionID: %s
Test ProcessID: %s
`

Logger.Printf(infoMessage, packagePath,
dockerInfo.ServerVersion, c.Client.ClientVersion(),
dockerInfo.OperatingSystem, dockerInfo.MemTotal/1024/1024,
testcontainersdocker.ExtractDockerHost(ctx),
testcontainersdocker.ExtractDockerSocket(ctx),
testcontainerssession.SessionID(),
testcontainerssession.ProcessID(),
)
})

Expand Down
Loading

0 comments on commit aa3be2d

Please sign in to comment.