143 lines
3.4 KiB
Go
143 lines
3.4 KiB
Go
package testcontainers
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"github.com/docker/go-connections/nat"
|
|
"github.com/testcontainers/testcontainers-go/wait"
|
|
"net"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const (
|
|
TestcontainerLabel = "org.testcontainers.golang"
|
|
TestcontainerLabelSessionID = TestcontainerLabel + ".sessionId"
|
|
TestcontainerLabelIsReaper = TestcontainerLabel + ".reaper"
|
|
|
|
ReaperDefaultImage = "quay.io/testcontainers/ryuk:0.2.3"
|
|
)
|
|
|
|
var reaper *Reaper // We would like to create reaper only once
|
|
var mutex sync.Mutex
|
|
|
|
// ReaperProvider represents a provider for the reaper to run itself with
|
|
// The ContainerProvider interface should usually satisfy this as well, so it is pluggable
|
|
type ReaperProvider interface {
|
|
RunContainer(ctx context.Context, req ContainerRequest) (Container, error)
|
|
}
|
|
|
|
// Reaper is used to start a sidecar container that cleans up resources
|
|
type Reaper struct {
|
|
Provider ReaperProvider
|
|
SessionID string
|
|
Endpoint string
|
|
}
|
|
|
|
// NewReaper creates a Reaper with a sessionID to identify containers and a provider to use
|
|
func NewReaper(ctx context.Context, sessionID string, provider ReaperProvider, reaperImageName string) (*Reaper, error) {
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
// If reaper already exists re-use it
|
|
if reaper != nil {
|
|
return reaper, nil
|
|
}
|
|
|
|
// Otherwise create a new one
|
|
reaper = &Reaper{
|
|
Provider: provider,
|
|
SessionID: sessionID,
|
|
}
|
|
|
|
listeningPort := nat.Port("8080/tcp")
|
|
|
|
req := ContainerRequest{
|
|
Image: reaperImage(reaperImageName),
|
|
ExposedPorts: []string{string(listeningPort)},
|
|
Labels: map[string]string{
|
|
TestcontainerLabel: "true",
|
|
TestcontainerLabelIsReaper: "true",
|
|
},
|
|
SkipReaper: true,
|
|
BindMounts: map[string]string{
|
|
"/var/run/docker.sock": "/var/run/docker.sock",
|
|
},
|
|
AutoRemove: true,
|
|
WaitingFor: wait.ForListeningPort(listeningPort),
|
|
}
|
|
|
|
c, err := provider.RunContainer(ctx, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
endpoint, err := c.PortEndpoint(ctx, "8080", "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reaper.Endpoint = endpoint
|
|
|
|
return reaper, nil
|
|
}
|
|
|
|
func reaperImage(reaperImageName string) string {
|
|
if reaperImageName == "" {
|
|
return ReaperDefaultImage
|
|
} else {
|
|
return reaperImageName
|
|
}
|
|
}
|
|
|
|
// Connect runs a goroutine which can be terminated by sending true into the returned channel
|
|
func (r *Reaper) Connect() (chan bool, error) {
|
|
conn, err := net.DialTimeout("tcp", r.Endpoint, 10*time.Second)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "Connecting to Ryuk on "+r.Endpoint+" failed")
|
|
}
|
|
|
|
terminationSignal := make(chan bool)
|
|
go func(conn net.Conn) {
|
|
sock := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
|
|
defer conn.Close()
|
|
|
|
labelFilters := []string{}
|
|
for l, v := range r.Labels() {
|
|
labelFilters = append(labelFilters, fmt.Sprintf("label=%s=%s", l, v))
|
|
}
|
|
|
|
retryLimit := 3
|
|
for retryLimit > 0 {
|
|
retryLimit--
|
|
|
|
sock.WriteString(strings.Join(labelFilters, "&"))
|
|
sock.WriteString("\n")
|
|
if err := sock.Flush(); err != nil {
|
|
continue
|
|
}
|
|
|
|
resp, err := sock.ReadString('\n')
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if resp == "ACK\n" {
|
|
break
|
|
}
|
|
}
|
|
|
|
<-terminationSignal
|
|
}(conn)
|
|
return terminationSignal, nil
|
|
}
|
|
|
|
// Labels returns the container labels to use so that this Reaper cleans them up
|
|
func (r *Reaper) Labels() map[string]string {
|
|
return map[string]string{
|
|
TestcontainerLabel: "true",
|
|
TestcontainerLabelSessionID: r.SessionID,
|
|
}
|
|
}
|