consul/test/integration/consul-container/libs/cluster/app.go

143 lines
3.8 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cluster
import (
"context"
"fmt"
"time"
dockercontainer "github.com/docker/docker/api/types/container"
"github.com/docker/go-connections/nat"
"github.com/hashicorp/go-multierror"
"github.com/testcontainers/testcontainers-go"
"github.com/hashicorp/consul/test/integration/consul-container/libs/utils"
)
// LaunchInfo is the resutl of LaunchContainerOnNode.
type LaunchInfo struct {
Container testcontainers.Container
IP string
MappedPorts map[string]nat.Port
}
// LaunchContainerOnNode will run a new container attached to the same network
// namespace as the provided agent, in the same manner in Kubernetes where
// you'd run two containers in the same pod so you can share localhost.
//
// This is supposed to mimic more accurately how consul/CLI/envoy/etc all are
// co-located on localhost with the consul client agent in typical deployment
// topologies.
func LaunchContainerOnNode(
ctx context.Context,
node Agent,
req testcontainers.ContainerRequest,
mapPorts []string,
) (*LaunchInfo, error) {
if req.Name == "" {
return nil, fmt.Errorf("ContainerRequest requires the Name field")
}
if req.NetworkMode != "" {
return nil, fmt.Errorf("caller should not configure ContainerRequest.NetworkMode")
}
req.NetworkMode = dockercontainer.NetworkMode("container:" + node.GetName() + "-pod")
pod := node.GetPod()
if pod == nil {
return nil, fmt.Errorf("node Pod is required")
}
// Do some trickery to ensure that partial completion is correctly torn
// down, but successful execution is not.
var deferClean utils.ResettableDefer
defer deferClean.Execute()
launchCtx, cancel := context.WithTimeout(ctx, time.Second*40)
defer cancel()
container, err := testcontainers.GenericContainer(launchCtx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
fmt.Printf("creating container with image: %s(%s)\n", req.Name, req.Image)
if err != nil {
return nil, fmt.Errorf("creating container: %s(%s), %w", req.Name, req.Image, err)
}
deferClean.Add(func() {
_ = container.Terminate(ctx)
})
ip, err := container.ContainerIP(ctx)
if err != nil {
return nil, fmt.Errorf("fetching container IP: %w", err)
}
if utils.FollowLog {
if err := container.StartLogProducer(ctx); err != nil {
return nil, fmt.Errorf("starting log producer: %w", err)
}
container.FollowOutput(&LogConsumer{
Prefix: req.Name,
})
deferClean.Add(func() {
_ = container.StopLogProducer()
})
}
ports := make(map[string]nat.Port)
for _, portStr := range mapPorts {
mapped, err := pod.MappedPort(ctx, nat.Port(portStr))
if err != nil {
return nil, fmt.Errorf("mapping port %s: %w", portStr, err)
}
ports[portStr] = mapped
}
info := &LaunchInfo{
Container: container,
IP: ip,
MappedPorts: ports,
}
node.RegisterTermination(func() error {
return TerminateContainer(ctx, container, true)
})
// disable cleanup functions now that we have an object with a Terminate() function
deferClean.Reset()
return info, nil
}
// TerminateContainer attempts to terminate the container. On failure, an error
// will be returned and the reaper process (RYUK) will handle cleanup.
func TerminateContainer(ctx context.Context, c testcontainers.Container, stopLogs bool) error {
if c == nil {
return nil
}
var merr error
if utils.FollowLog && stopLogs {
if state, err := c.State(ctx); err == nil && state.Running {
// StopLogProducer can only be called on running containers
if err := c.StopLogProducer(); err != nil {
merr = multierror.Append(merr, err)
}
}
}
if err := c.Stop(ctx, nil); err != nil {
merr = multierror.Append(merr, err)
}
if err := c.Terminate(ctx); err != nil {
merr = multierror.Append(merr, err)
}
return merr
}