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

387 lines
11 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cluster
import (
"encoding/json"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"golang.org/x/mod/semver"
"github.com/hashicorp/consul/test/integration/consul-container/libs/utils"
)
// TODO: switch from semver to go-version
const (
remoteCertDirectory = "/consul/config/certs"
ConsulCACertPEM = "consul-agent-ca.pem"
ConsulCACertKey = "consul-agent-ca-key.pem"
)
type LogStore string
const (
LogStore_WAL LogStore = "wal"
LogStore_BoltDB LogStore = "boltdb"
)
// BuildContext provides a reusable object meant to share common configuration settings
// between agent configuration builders.
type BuildContext struct {
datacenter string
consulImageName string
consulVersion string
injectGossipEncryption bool // setup the agents to use a gossip encryption key
encryptKey string
injectCerts bool // initializes the built-in CA and distributes client certificates to agents
injectAutoEncryption bool // initialize the built-in CA and set up agents to use auto-encrpt
allowHTTPAnyway bool
useAPIWithTLS bool
useGRPCWithTLS bool
certVolume string
caCert string
tlsCertIndex int // keeps track of the certificates issued for naming purposes
aclEnabled bool
logStore LogStore
}
func (c *BuildContext) DockerImage() string {
return utils.DockerImage(c.consulImageName, c.consulVersion)
}
// BuildOptions define the desired automated test setup overrides that are
// applied across agents in the cluster
type BuildOptions struct {
// Datacenter is the override datacenter for agents.
Datacenter string
// ConsulImageName is the default Consul image name for agents in the
// cluster when none is specified.
ConsulImageName string
// ConsulVersion is the default Consul version for agents in the cluster
// when none is specified.
ConsulVersion string
// InjectGossipEncryption provides a gossip encryption key for all agents.
InjectGossipEncryption bool
// InjectCerts provides a CA for all agents and (future) agent certs.
//
// It also disables the HTTP API unless AllowHTTPAnyway is enabled.
InjectCerts bool
// InjectAutoEncryption configures auto-encrypt for TLS and sets up certs.
// Overrides InjectCerts.
//
// It also disables the HTTP API unless AllowHTTPAnyway is enabled.
InjectAutoEncryption bool
// AllowHTTPAnyway ensures that the HTTP API is enabled even when
// InjectCerts or InjectAutoEncryption are enabled.
AllowHTTPAnyway bool
// UseAPIWithTLS ensures that any accesses for the JSON API use the https
// port. By default it will not.
UseAPIWithTLS bool
// UseGRPCWithTLS ensures that any accesses for external gRPC use the
// grpc_tls port. By default it will not.
UseGRPCWithTLS bool
// ACLEnabled configures acl in agent configuration
ACLEnabled bool
//StoreLog define which LogStore to use
LogStore LogStore
}
func NewBuildContext(t *testing.T, opts BuildOptions) *BuildContext {
ctx := &BuildContext{
datacenter: opts.Datacenter,
consulImageName: opts.ConsulImageName,
consulVersion: opts.ConsulVersion,
injectGossipEncryption: opts.InjectGossipEncryption,
injectCerts: opts.InjectCerts,
injectAutoEncryption: opts.InjectAutoEncryption,
allowHTTPAnyway: opts.AllowHTTPAnyway,
useAPIWithTLS: opts.UseAPIWithTLS,
useGRPCWithTLS: opts.UseGRPCWithTLS,
aclEnabled: opts.ACLEnabled,
logStore: opts.LogStore,
}
if ctx.consulImageName == "" {
ctx.consulImageName = utils.GetTargetImageName()
}
if ctx.consulVersion == "" {
ctx.consulVersion = utils.TargetVersion
}
if opts.InjectGossipEncryption {
serfKey, err := newSerfEncryptionKey()
require.NoError(t, err, "could not generate serf encryption key")
ctx.encryptKey = serfKey
}
if opts.InjectAutoEncryption {
if opts.UseAPIWithTLS {
// TODO: we should improve this
t.Fatalf("Cannot use TLS with the API in conjunction with Auto Encrypt because you would need to use the Connect CA Cert for verification")
}
if opts.UseGRPCWithTLS {
// TODO: we should improve this
t.Fatalf("Cannot use TLS with gRPC in conjunction with Auto Encrypt because you would need to use the Connect CA Cert for verification")
}
}
if opts.InjectAutoEncryption || opts.InjectCerts {
ctx.createTLSCAFiles(t)
} else {
if opts.UseAPIWithTLS {
t.Fatalf("UseAPIWithTLS requires one of InjectAutoEncryption or InjectCerts to be set")
}
if opts.UseGRPCWithTLS {
t.Fatalf("UseGRPCWithTLS requires one of InjectAutoEncryption or InjectCerts to be set")
}
}
return ctx
}
type Builder struct {
context *BuildContext // this is non-nil
conf *ConfigBuilder
}
// NewConfigBuilder instantiates a builder object with sensible defaults for a single consul instance
// This includes the following:
// * default ports with no plaintext options
// * debug logging
// * single server with bootstrap
// * bind to all interfaces, advertise on 'eth0'
// * connect enabled
func NewConfigBuilder(ctx *BuildContext) *Builder {
if ctx == nil {
panic("BuildContext is a required argument")
}
b := &Builder{
conf: &ConfigBuilder{},
context: ctx,
}
b.conf.Set("advertise_addr", `{{ GetInterfaceIP "eth0" }}`)
b.conf.Set("bind_addr", "0.0.0.0")
b.conf.Set("data_dir", "/consul/data")
b.conf.Set("bootstrap", true)
b.conf.Set("client_addr", "0.0.0.0")
b.conf.Set("connect.enabled", true)
b.conf.Set("log_level", "debug")
b.conf.Set("server", true)
b.conf.Set("ui_config.enabled", true)
// These are the default ports, disabling plaintext transport
b.conf.Set("ports.dns", 8600)
//nolint:staticcheck
if ctx.certVolume == "" {
b.conf.Set("ports.http", 8500)
b.conf.Set("ports.https", -1)
} else {
b.conf.Set("ports.http", -1)
b.conf.Set("ports.https", 8501)
}
b.conf.Set("ports.grpc", 8502)
b.conf.Set("ports.serf_lan", 8301)
b.conf.Set("ports.serf_wan", 8302)
b.conf.Set("ports.server", 8300)
if ctx.allowHTTPAnyway {
b.conf.Set("ports.http", 8500)
}
if ctx.consulVersion == "local" || semver.Compare("v"+ctx.consulVersion, "v1.14.0") >= 0 {
// Enable GRPCTLS for version after v1.14.0
b.conf.Set("ports.grpc_tls", 8503)
}
if ctx.aclEnabled {
b.conf.Set("acl.enabled", true)
b.conf.Set("acl.default_policy", "deny")
b.conf.Set("acl.enable_token_persistence", true)
}
ls := string(ctx.logStore)
if ls != "" && (ctx.consulVersion == "local" ||
semver.Compare("v"+ctx.consulVersion, "v1.15.0") >= 0) {
// Enable logstore backend for version after v1.15.0
if ls != string(LogStore_WAL) && ls != string(LogStore_BoltDB) {
ls = string(LogStore_BoltDB)
}
b.conf.Set("raft_logstore.backend", ls)
} else {
b.conf.Unset("raft_logstore.backend")
}
return b
}
// Advanced lets you directly manipulate specific config settings.
func (b *Builder) Advanced(fn func(*ConfigBuilder)) *Builder {
if fn != nil {
fn(b.conf)
}
return b
}
func (b *Builder) Bootstrap(servers int) *Builder {
if servers < 1 {
b.conf.Unset("bootstrap")
b.conf.Unset("bootstrap_expect")
} else if servers == 1 {
b.conf.Set("bootstrap", true)
b.conf.Unset("bootstrap_expect")
} else {
b.conf.Unset("bootstrap")
b.conf.Set("bootstrap_expect", servers)
}
return b
}
func (b *Builder) Client() *Builder {
b.conf.Unset("ports.server")
b.conf.Unset("server")
b.conf.Unset("bootstrap")
b.conf.Unset("bootstrap_expect")
return b
}
func (b *Builder) Datacenter(name string) *Builder {
b.conf.Set("datacenter", name)
return b
}
func (b *Builder) Peering(enable bool) *Builder {
b.conf.Set("peering.enabled", enable)
return b
}
func (b *Builder) NodeID(nodeID string) *Builder {
b.conf.Set("node_id", nodeID)
return b
}
func (b *Builder) Partition(name string) *Builder {
b.conf.Set("partition", name)
return b
}
func (b *Builder) RetryJoin(names ...string) *Builder {
b.conf.Set("retry_join", names)
return b
}
func (b *Builder) EnableACL() *Builder {
b.conf.Set("acl.enabled", true)
b.conf.Set("acl.default_policy", "deny")
b.conf.Set("acl.enable_token_persistence", true)
return b
}
func (b *Builder) Telemetry(statSite string) *Builder {
b.conf.Set("telemetry.statsite_address", statSite)
return b
}
// ToAgentConfig renders the builders configuration into a string
// representation of the json config file for agents.
func (b *Builder) ToAgentConfig(t *testing.T) *Config {
b.injectContextOptions(t)
out, err := json.MarshalIndent(b.conf, "", " ")
require.NoError(t, err, "could not generate json config")
confCopy, err := b.conf.Clone()
require.NoError(t, err)
cmd := []string{"agent"}
if utils.Debug {
cmd = []string{"/root/go/bin/dlv", "exec", "/bin/consul", "--listen=:4000", "--headless=true", "", "--accept-multiclient", "--continue", "--api-version=2", "--", "agent", "--config-file=/consul/config/config.json"}
}
return &Config{
JSON: string(out),
ConfigBuilder: confCopy,
Cmd: cmd,
Image: b.context.consulImageName,
Version: b.context.consulVersion,
CertVolume: b.context.certVolume,
CACert: b.context.caCert,
UseAPIWithTLS: b.context.useAPIWithTLS,
UseGRPCWithTLS: b.context.useGRPCWithTLS,
ACLEnabled: b.context.aclEnabled,
}
}
func (b *Builder) injectContextOptions(t *testing.T) {
var dc string
if b.context.datacenter != "" {
b.conf.Set("datacenter", b.context.datacenter)
dc = b.context.datacenter
}
if val, _ := b.conf.GetString("datacenter"); val == "" {
dc = "dc1"
}
b.conf.Set("datacenter", dc)
server, _ := b.conf.GetBool("server")
if b.context.encryptKey != "" {
b.conf.Set("encrypt", b.context.encryptKey)
}
// For any TLS setup, we add the CA to agent conf
if b.context.certVolume != "" {
b.conf.Set("tls.defaults.ca_file", filepath.Join(remoteCertDirectory, ConsulCACertPEM))
b.conf.Set("tls.defaults.verify_outgoing", true) // Secure settings
b.conf.Set("tls.internal_rpc.verify_server_hostname", true)
}
// Also for any TLS setup, generate server key pairs from the CA
if b.context.certVolume != "" && server {
keyFileName, certFileName := b.context.createTLSCertFiles(t, dc)
b.context.tlsCertIndex++
b.conf.Set("tls.defaults.cert_file", filepath.Join(remoteCertDirectory, certFileName))
b.conf.Set("tls.defaults.key_file", filepath.Join(remoteCertDirectory, keyFileName))
b.conf.Set("tls.internal_rpc.verify_incoming", true) // Only applies to servers for auto-encrypt
}
// This assumes we've already gone through the CA/Cert setup in the previous conditional
if b.context.injectAutoEncryption {
if server {
b.conf.Set("auto_encrypt.allow_tls", true) // This setting is different between client and servers
b.conf.Set("tls.grpc.use_auto_cert", true) // This is required for peering to work over the non-GRPC_TLS port
// VerifyIncoming does not apply to client agents for auto-encrypt
} else {
b.conf.Set("auto_encrypt.tls", true) // This setting is different between client and servers
b.conf.Set("tls.grpc.use_auto_cert", true) // This is required for peering to work over the non-GRPC_TLS port
}
}
if b.context.injectCerts && !b.context.injectAutoEncryption {
panic("client certificate distribution not implemented")
}
}