package agent import ( "encoding/json" "path/filepath" "github.com/pkg/errors" "golang.org/x/mod/semver" agentconfig "github.com/hashicorp/consul/agent/config" "github.com/hashicorp/consul/test/integration/consul-container/libs/utils" "github.com/hashicorp/consul/tlsutil" ) const ( remoteCertDirectory = "/consul/config/certs" ) // BuildContext provides a reusable object meant to share common configuration settings // between agent configuration builders. type BuildContext struct { datacenter string encryptKey string caCert string caKey string index int // keeps track of the certificates issued for naming purposes injectAutoEncryption bool // initialize the built-in CA and set up agents to use auto-encrpt injectCerts bool // initializes the built-in CA and distributes client certificates to agents injectGossipEncryption bool // setup the agents to use a gossip encryption key consulVersion string } // BuildOptions define the desired automated test setup overrides that are // applied across agents in the cluster type BuildOptions struct { Datacenter string // Override datacenter for agents InjectCerts bool // Provides a CA for all agents and (future) agent certs. InjectAutoEncryption bool // Configures auto-encrypt for TLS and sets up certs. Overrides InjectCerts. InjectGossipEncryption bool // Provides a gossip encryption key for all agents. ConsulVersion string // The default Consul version for agents in the cluster when none is specified. } func NewBuildContext(opts BuildOptions) (*BuildContext, error) { ctx := &BuildContext{ datacenter: opts.Datacenter, injectAutoEncryption: opts.InjectAutoEncryption, injectCerts: opts.InjectCerts, injectGossipEncryption: opts.InjectGossipEncryption, consulVersion: opts.ConsulVersion, } if opts.ConsulVersion == "" { ctx.consulVersion = *utils.TargetVersion } if opts.InjectGossipEncryption { serfKey, err := newSerfEncryptionKey() if err != nil { return nil, errors.Wrap(err, "could not generate serf encryption key") } ctx.encryptKey = serfKey } if opts.InjectAutoEncryption || opts.InjectCerts { // This is the same call that 'consul tls ca create` will run caCert, caKey, err := tlsutil.GenerateCA(tlsutil.CAOpts{Domain: "consul", PermittedDNSDomains: []string{"consul", "localhost"}}) if err != nil { return nil, errors.Wrap(err, "could not generate built-in CA root pair") } ctx.caCert = caCert ctx.caKey = caKey } return ctx, nil } func (c *BuildContext) GetCerts() (cert string, key string) { return c.caCert, c.caKey } type Builder struct { conf *agentconfig.Config certs map[string]string context *BuildContext } // 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 { b := &Builder{ certs: map[string]string{}, conf: &agentconfig.Config{ AdvertiseAddrLAN: utils.StringToPointer(`{{ GetInterfaceIP "eth0" }}`), BindAddr: utils.StringToPointer("0.0.0.0"), Bootstrap: utils.BoolToPointer(true), ClientAddr: utils.StringToPointer("0.0.0.0"), Connect: agentconfig.Connect{ Enabled: utils.BoolToPointer(true), }, LogLevel: utils.StringToPointer("DEBUG"), ServerMode: utils.BoolToPointer(true), }, context: ctx, } // These are the default ports, disabling plaintext transport b.conf.Ports = agentconfig.Ports{ DNS: utils.IntToPointer(8600), HTTP: nil, HTTPS: utils.IntToPointer(8501), GRPC: utils.IntToPointer(8502), SerfLAN: utils.IntToPointer(8301), SerfWAN: utils.IntToPointer(8302), Server: utils.IntToPointer(8300), } if ctx != nil && (ctx.consulVersion == "local" || semver.Compare("v"+ctx.consulVersion, "v1.14.0") >= 0) { // Enable GRPCTLS for version after v1.14.0 b.conf.Ports.GRPCTLS = utils.IntToPointer(8503) } return b } func (b *Builder) Bootstrap(servers int) *Builder { if servers < 1 { b.conf.Bootstrap = nil b.conf.BootstrapExpect = nil } else if servers == 1 { b.conf.Bootstrap = utils.BoolToPointer(true) b.conf.BootstrapExpect = nil } else { b.conf.Bootstrap = nil b.conf.BootstrapExpect = utils.IntToPointer(servers) } return b } func (b *Builder) Client() *Builder { b.conf.Ports.Server = nil b.conf.ServerMode = nil b.conf.Bootstrap = nil b.conf.BootstrapExpect = nil return b } func (b *Builder) Datacenter(name string) *Builder { b.conf.Datacenter = utils.StringToPointer(name) return b } func (b *Builder) Peering(enable bool) *Builder { b.conf.Peering = agentconfig.Peering{ Enabled: utils.BoolToPointer(enable), } return b } func (b *Builder) RetryJoin(names ...string) *Builder { b.conf.RetryJoinLAN = names return b } func (b *Builder) Telemetry(statSite string) *Builder { b.conf.Telemetry = agentconfig.Telemetry{ StatsiteAddr: utils.StringToPointer(statSite), } return b } // ToAgentConfig renders the builders configuration into a string // representation of the json config file for agents. // DANGER! Some fields may not have json tags in the Agent Config. // You may need to add these yourself. func (b *Builder) ToAgentConfig() (*Config, error) { b.injectContextOptions() out, err := json.MarshalIndent(b.conf, "", " ") if err != nil { return nil, errors.Wrap(err, "could not marshall builder") } conf := &Config{ Certs: b.certs, Cmd: []string{"agent"}, Image: *utils.TargetImage, JSON: string(out), Version: *utils.TargetVersion, } // Override the default version if b.context != nil && b.context.consulVersion != "" { conf.Version = b.context.consulVersion } return conf, nil } func (b *Builder) injectContextOptions() { if b.context == nil { return } var dc string if b.context.datacenter != "" { b.conf.Datacenter = utils.StringToPointer(b.context.datacenter) dc = b.context.datacenter } if b.conf.Datacenter == nil || *b.conf.Datacenter == "" { dc = "dc1" } server := b.conf.ServerMode != nil && *b.conf.ServerMode if b.context.encryptKey != "" { b.conf.EncryptKey = utils.StringToPointer(b.context.encryptKey) } // For any TLS setup, we add the CA to agent conf if b.context.caCert != "" { // Add the ca file to the list of certs that will be mounted to consul filename := filepath.Join(remoteCertDirectory, "consul-agent-ca.pem") b.certs[filename] = b.context.caCert b.conf.TLS = agentconfig.TLS{ Defaults: agentconfig.TLSProtocolConfig{ CAFile: utils.StringToPointer(filename), VerifyOutgoing: utils.BoolToPointer(true), // Secure settings }, InternalRPC: agentconfig.TLSProtocolConfig{ VerifyServerHostname: utils.BoolToPointer(true), }, } } // Also for any TLS setup, generate server key pairs from the CA if b.context.caCert != "" && server { keyFileName, priv, certFileName, pub := newServerTLSKeyPair(dc, b.context) // Add the key pair to the list that will be mounted to consul certFileName = filepath.Join(remoteCertDirectory, certFileName) keyFileName = filepath.Join(remoteCertDirectory, keyFileName) b.certs[certFileName] = pub b.certs[keyFileName] = priv b.conf.TLS.Defaults.CertFile = utils.StringToPointer(certFileName) b.conf.TLS.Defaults.KeyFile = utils.StringToPointer(keyFileName) b.conf.TLS.Defaults.VerifyIncoming = utils.BoolToPointer(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 && server { b.conf.AutoEncrypt = agentconfig.AutoEncrypt{ AllowTLS: utils.BoolToPointer(true), // This setting is different between client and servers } b.conf.TLS.GRPC = agentconfig.TLSProtocolConfig{ UseAutoCert: utils.BoolToPointer(true), // This is required for peering to work over the non-GRPC_TLS port } // VerifyIncoming does not apply to client agents for auto-encrypt } if b.context.injectAutoEncryption && !server { b.conf.AutoEncrypt = agentconfig.AutoEncrypt{ TLS: utils.BoolToPointer(true), // This setting is different between client and servers } b.conf.TLS.GRPC = agentconfig.TLSProtocolConfig{ UseAutoCert: utils.BoolToPointer(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") } b.context.index++ }