239 lines
5.9 KiB
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
|
|
}
|