open-consul/test/integration/consul-container/libs/node/consul-container.go

239 lines
5.9 KiB
Go

package node
import (
"context"
"fmt"
"os"
"strconv"
"time"
"github.com/docker/docker/pkg/ioutils"
"github.com/hashicorp/consul/api"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"github.com/hashicorp/consul/integration/consul-container/libs/utils"
)
const bootLogLine = "Consul agent running"
const disableRYUKEnv = "TESTCONTAINERS_RYUK_DISABLED"
// consulContainerNode implements the Node interface by running a Consul node
// in a container.
type consulContainerNode struct {
ctx context.Context
client *api.Client
container testcontainers.Container
ip string
port int
config Config
req testcontainers.ContainerRequest
dataDir string
}
func (c *consulContainerNode) GetConfig() Config {
return c.config
}
func newConsulContainerWithReq(ctx context.Context, req testcontainers.ContainerRequest) (testcontainers.Container, error) {
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
return nil, err
}
return container, nil
}
// NewConsulContainer starts a Consul node in a container with the given config.
func NewConsulContainer(ctx context.Context, config Config) (Node, error) {
license, err := readLicense()
if err != nil {
return nil, err
}
name := utils.RandName("consul-")
tmpDirData, err := ioutils.TempDir("", name)
if err != nil {
return nil, err
}
err = os.Chmod(tmpDirData, 0777)
if err != nil {
return nil, err
}
configFile, err := createConfigFile(config.HCL)
if err != nil {
return nil, err
}
skipReaper := isRYUKDisabled()
req := testcontainers.ContainerRequest{
Image: consulImage + ":" + config.Version,
ExposedPorts: []string{"8500/tcp"},
WaitingFor: wait.ForLog(bootLogLine).WithStartupTimeout(10 * time.Second),
AutoRemove: false,
Name: name,
Mounts: testcontainers.ContainerMounts{
testcontainers.ContainerMount{Source: testcontainers.DockerBindMountSource{HostPath: configFile}, Target: "/consul/config/config.hcl"},
testcontainers.ContainerMount{Source: testcontainers.DockerBindMountSource{HostPath: tmpDirData}, Target: "/consul/data"},
},
Cmd: config.Cmd,
SkipReaper: skipReaper,
Env: map[string]string{"CONSUL_LICENSE": license},
}
container, err := newConsulContainerWithReq(ctx, req)
if err != nil {
return nil, err
}
localIP, err := container.Host(ctx)
if err != nil {
return nil, err
}
mappedPort, err := container.MappedPort(ctx, "8500")
if err != nil {
return nil, err
}
ip, err := container.ContainerIP(ctx)
if err != nil {
return nil, err
}
uri := fmt.Sprintf("http://%s:%s", localIP, mappedPort.Port())
c := new(consulContainerNode)
c.config = config
c.container = container
c.ip = ip
c.port = mappedPort.Int()
apiConfig := api.DefaultConfig()
apiConfig.Address = uri
c.client, err = api.NewClient(apiConfig)
c.ctx = ctx
c.req = req
c.dataDir = tmpDirData
if err != nil {
return nil, err
}
return c, nil
}
// GetClient returns an API client that can be used to communicate with the Node.
func (c *consulContainerNode) GetClient() *api.Client {
return c.client
}
// GetAddr return the network address associated with the Node.
func (c *consulContainerNode) GetAddr() (string, int) {
return c.ip, c.port
}
func (c *consulContainerNode) Upgrade(ctx context.Context, config Config) error {
file, err := createConfigFile(config.HCL)
if err != nil {
return err
}
c.req.Cmd = config.Cmd
c.req.Mounts = testcontainers.ContainerMounts{
testcontainers.ContainerMount{Source: testcontainers.DockerBindMountSource{HostPath: file}, Target: "/consul/config/config.hcl"},
testcontainers.ContainerMount{Source: testcontainers.DockerBindMountSource{HostPath: c.dataDir}, Target: "/consul/data"},
}
c.req.Image = consulImage + ":" + config.Version
err = c.container.Terminate(ctx)
if err != nil {
return err
}
container, err := newConsulContainerWithReq(ctx, c.req)
if err != nil {
return err
}
c.container = container
localIP, err := container.Host(ctx)
if err != nil {
return err
}
mappedPort, err := container.MappedPort(ctx, "8500")
if err != nil {
return err
}
ip, err := container.ContainerIP(ctx)
if err != nil {
return err
}
uri := fmt.Sprintf("http://%s:%s", localIP, mappedPort.Port())
c.ip = ip
c.port = mappedPort.Int()
apiConfig := api.DefaultConfig()
apiConfig.Address = uri
c.client, err = api.NewClient(apiConfig)
c.ctx = ctx
c.config = config
if err != nil {
return err
}
return nil
}
// Terminate attempts to terminate the container. On failure, an error will be
// returned and the reaper process (RYUK) will handle cleanup.
func (c *consulContainerNode) Terminate() error {
return c.container.Terminate(c.ctx)
}
// isRYUKDisabled returns whether the reaper process (RYUK) has been disabled
// by an environment variable.
//
// https://github.com/testcontainers/moby-ryuk
func isRYUKDisabled() bool {
skipReaperStr := os.Getenv(disableRYUKEnv)
skipReaper, err := strconv.ParseBool(skipReaperStr)
if err != nil {
return false
}
return skipReaper
}
func readLicense() (string, error) {
license := os.Getenv("CONSUL_LICENSE")
if license == "" {
licensePath := os.Getenv("CONSUL_LICENSE_PATH")
if licensePath != "" {
licenseBytes, err := os.ReadFile(licensePath)
if err != nil {
return "", err
}
license = string(licenseBytes)
}
}
return license, nil
}
func createConfigFile(HCL string) (string, error) {
tmpDir, err := ioutils.TempDir("", "consul-container-test-config")
if err != nil {
return "", err
}
err = os.Chmod(tmpDir, 0777)
if err != nil {
return "", err
}
err = os.Mkdir(tmpDir+"/config", 0777)
if err != nil {
return "", err
}
configFile := tmpDir + "/config/config.hcl"
err = os.WriteFile(configFile, []byte(HCL), 0644)
if err != nil {
return "", err
}
return configFile, nil
}