Skip to content

Commit

Permalink
feat: add keep alive flag to dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
Mathew-Estafanous committed Apr 22, 2024
1 parent 4e016aa commit a2f0c45
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 20 deletions.
4 changes: 2 additions & 2 deletions container.go
Expand Up @@ -132,8 +132,8 @@ type ContainerRequest struct {
Tmpfs map[string]string
RegistryCred string // Deprecated: Testcontainers will detect registry credentials automatically
WaitingFor wait.Strategy
DependsOn []*ContainerDependency // specify other containers that must be running before starting this container.
Name string // for specifying container name
DependsOn []ContainerDependency // specify other containers that must be running before starting this container.
Name string // for specifying container name
Hostname string
WorkingDir string // specify the working directory of the container
ExtraHosts []string // Deprecated: Use HostConfigModifier instead
Expand Down
31 changes: 23 additions & 8 deletions dependency.go
Expand Up @@ -15,24 +15,32 @@ type ContainerDependency struct {
EnvKey string
// CallbackFunc is called after the dependency container is started.
CallbackFunc func(Container)
// KeepAlive determines whether the dependency should be kept alive after the parent container is terminated.
KeepAlive bool
}

// NewContainerDependency can be used to define a dependency and the environment variable that
// will be used to pass the DNS name to the parent container.
func NewContainerDependency(containerRequest ContainerRequest, envKey string) *ContainerDependency {
return &ContainerDependency{
func NewContainerDependency(containerRequest ContainerRequest, envKey string) ContainerDependency {
return ContainerDependency{
Request: containerRequest,
EnvKey: envKey,
CallbackFunc: func(c Container) {},
KeepAlive: true,
}
}

func (c *ContainerDependency) WithCallback(callbackFunc func(Container)) *ContainerDependency {
func (c ContainerDependency) WithKeepAlive(keepAlive bool) ContainerDependency {
c.KeepAlive = keepAlive
return c
}

func (c ContainerDependency) WithCallback(callbackFunc func(Container)) ContainerDependency {
c.CallbackFunc = callbackFunc
return c
}

func (c *ContainerDependency) StartDependency(ctx context.Context, network string) (Container, error) {
func (c ContainerDependency) StartDependency(ctx context.Context, network string) (Container, error) {
c.Request.Networks = append(c.Request.Networks, network)
dependency, err := GenericContainer(ctx, GenericContainerRequest{
ContainerRequest: c.Request,
Expand Down Expand Up @@ -72,24 +80,31 @@ func resolveDNSName(ctx context.Context, container Container, network *DockerNet
return aliases[0], nil
}

func cleanupDependencyNetwork(ctx context.Context, dependencies []Container, network *DockerNetwork) error {
func cleanupDependencyNetwork(ctx context.Context, dependencies map[Container]bool, network *DockerNetwork) error {
if network == nil {
return nil
}

for _, dependency := range dependencies {
for dependency, keepAlive := range dependencies {
err := network.provider.client.NetworkDisconnect(ctx, network.ID, dependency.GetContainerID(), true)
if err != nil {
return err
}

if !keepAlive {
if err := dependency.Terminate(ctx); err != nil {
return err
}
}

}
defer network.provider.Close()
return network.Remove(ctx)
}

var defaultDependencyHook = func(dockerInput *container.Config) ContainerLifecycleHooks {
var depNetwork *DockerNetwork
depContainers := make([]Container, 0)
depContainers := make(map[Container]bool)
return ContainerLifecycleHooks{
PreCreates: []ContainerRequestHook{
func(ctx context.Context, req ContainerRequest) (err error) {
Expand Down Expand Up @@ -127,7 +142,7 @@ var defaultDependencyHook = func(dockerInput *container.Config) ContainerLifecyc
if err != nil {
return err
}
depContainers = append(depContainers, container)
depContainers[container] = dep.KeepAlive
name, err := resolveDNSName(ctx, container, depNetwork)
if err != nil {
return err
Expand Down
72 changes: 63 additions & 9 deletions dependency_test.go
Expand Up @@ -13,15 +13,15 @@ import (
func Test_ContainerDependency(t *testing.T) {
type TestCase struct {
name string
configureDependants func(ctx context.Context, t *testing.T) []*ContainerDependency
configureDependants func(ctx context.Context, t *testing.T) []ContainerDependency
containerRequest ContainerRequest
expectedEnv []string
expectedError string
}
testCases := []TestCase{
{
name: "dependency's dns name is passed as an environment variable to parent container",
configureDependants: func(ctx context.Context, t *testing.T) []*ContainerDependency {
configureDependants: func(ctx context.Context, t *testing.T) []ContainerDependency {
nginxReq := ContainerRequest{
Image: nginxAlpineImage,
ExposedPorts: []string{nginxDefaultPort},
Expand All @@ -32,7 +32,7 @@ func Test_ContainerDependency(t *testing.T) {
terminateContainerOnEnd(t, ctx, container)
}

return []*ContainerDependency{
return []ContainerDependency{
NewContainerDependency(nginxReq, "FIRST_DEPENDENCY").WithCallback(terminateFn),
NewContainerDependency(nginxReq, "SECOND_DEPENDENCY").WithCallback(terminateFn),
}
Expand All @@ -45,13 +45,13 @@ func Test_ContainerDependency(t *testing.T) {
},
{
name: "container fails to start when dependency fails to start",
configureDependants: func(ctx context.Context, t *testing.T) []*ContainerDependency {
configureDependants: func(ctx context.Context, t *testing.T) []ContainerDependency {
badReq := ContainerRequest{
Image: "bad image name",
ExposedPorts: []string{"80/tcp"},
}

return []*ContainerDependency{
return []ContainerDependency{
NewContainerDependency(badReq, "FIRST_DEPENDENCY"),
}
},
Expand All @@ -63,15 +63,15 @@ func Test_ContainerDependency(t *testing.T) {
},
{
name: "fails to start dependency when key is empty",
configureDependants: func(ctx context.Context, t *testing.T) []*ContainerDependency {
configureDependants: func(ctx context.Context, t *testing.T) []ContainerDependency {
nginxReq := ContainerRequest{
Image: nginxAlpineImage,
ExposedPorts: []string{nginxDefaultPort},
WaitingFor: wait.ForListeningPort(nginxDefaultPort),
}

dependency := NewContainerDependency(nginxReq, "")
return []*ContainerDependency{dependency}
return []ContainerDependency{dependency}
},
containerRequest: ContainerRequest{
Image: nginxAlpineImage,
Expand Down Expand Up @@ -136,7 +136,7 @@ func Test_ContainerDependency_CallbackFunc(t *testing.T) {
req := ContainerRequest{
Image: nginxAlpineImage,
Entrypoint: []string{"tail", "-f", "/dev/null"},
DependsOn: []*ContainerDependency{
DependsOn: []ContainerDependency{
NewContainerDependency(nginxReq, "MY_DEPENDENCY").
WithCallback(func(c Container) {
terminateContainerOnEnd(t, ctx, c)
Expand Down Expand Up @@ -182,7 +182,7 @@ func Test_ContainerDependency_ReuseRunningContainer(t *testing.T) {
req := ContainerRequest{
Image: nginxAlpineImage,
Entrypoint: []string{"tail", "-f", "/dev/null"},
DependsOn: []*ContainerDependency{
DependsOn: []ContainerDependency{
NewContainerDependency(nginxReq, "MY_DEPENDENCY").
WithCallback(func(c Container) {
dependencyContainer <- c
Expand All @@ -204,3 +204,57 @@ func Test_ContainerDependency_ReuseRunningContainer(t *testing.T) {
require.Equal(t, depContainer.GetContainerID(), dependency.GetContainerID())
}
}

func Test_ContainerDependency_KeepAlive(t *testing.T) {
ctx := context.Background()

nginxReq := ContainerRequest{
Image: nginxAlpineImage,
Name: "my-nginx-container",
ExposedPorts: []string{nginxDefaultPort},
WaitingFor: wait.ForListeningPort(nginxDefaultPort),
}

testCases := []struct {
name string
keepAlive bool
expectRunning bool
}{
{"dependency is terminated when KeepAlive is set to false", false, false},
{"dependency is still running when KeepAlive is set to true", true, true},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var depContainer Container
req := ContainerRequest{
Image: nginxAlpineImage,
Entrypoint: []string{"tail", "-f", "/dev/null"},
DependsOn: []ContainerDependency{
NewContainerDependency(nginxReq, "MY_DEPENDENCY").
WithCallback(func(c Container) {
depContainer = c
}).
WithKeepAlive(tc.keepAlive),
},
}

c, err := GenericContainer(ctx, GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
require.NoError(t, err)
require.True(t, c.IsRunning())
require.True(t, depContainer.IsRunning())
if tc.keepAlive {
terminateContainerOnEnd(t, ctx, depContainer)
}

err = c.Terminate(ctx)
require.NoError(t, err)

// Check the expected state of the dependency after the parent container is terminated.
require.Equal(t, tc.expectRunning, depContainer.IsRunning())
})
}
}
2 changes: 1 addition & 1 deletion docker.go
Expand Up @@ -58,7 +58,7 @@ type DockerContainer struct {
// Container ID from Docker
ID string
WaitingFor wait.Strategy
DependsOn []*ContainerDependency
DependsOn []ContainerDependency
Image string

isRunning bool
Expand Down

0 comments on commit a2f0c45

Please sign in to comment.