package config import ( "bytes" "crypto/tls" "encoding/base64" "encoding/json" "errors" "flag" "fmt" "io/ioutil" "net" "os" "path/filepath" "reflect" "strconv" "strings" "testing" "time" "github.com/armon/go-metrics/prometheus" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/require" "github.com/hashicorp/consul/agent/cache" "github.com/hashicorp/consul/agent/checks" "github.com/hashicorp/consul/agent/consul" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/token" "github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/logging" "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/types" ) // testCase used to test different config loading and flag parsing scenarios. type testCase struct { desc string args []string setup func() // TODO: accept a testing.T instead of panic expected func(rt *RuntimeConfig) expectedErr string expectedWarnings []string opts LoadOpts json []string hcl []string } func (tc testCase) source(format string) []string { if format == "hcl" { return tc.hcl } return tc.json } // TestConfigFlagsAndEdgecases tests the command line flags and // edgecases for the config parsing. It provides a test structure which // checks for warnings on deprecated fields and flags. These tests // should check one option at a time if possible func TestLoad_IntegrationWithFlags(t *testing.T) { dataDir := testutil.TempDir(t, "config") run := func(t *testing.T, tc testCase) { t.Helper() if len(tc.json) == 0 && len(tc.hcl) == 0 { runCase(t, tc.desc, tc.run("", dataDir)) return } for _, format := range []string{"json", "hcl"} { name := fmt.Sprintf("%v_%v", tc.desc, format) runCase(t, name, tc.run(format, dataDir)) } } defaultEntMeta := structs.DefaultEnterpriseMeta() // ------------------------------------------------------------ // cmd line flags // run(t, testCase{ desc: "-advertise", args: []string{ `-advertise=1.2.3.4`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.AdvertiseAddrLAN = ipAddr("1.2.3.4") rt.AdvertiseAddrWAN = ipAddr("1.2.3.4") rt.RPCAdvertiseAddr = tcpAddr("1.2.3.4:8300") rt.SerfAdvertiseAddrLAN = tcpAddr("1.2.3.4:8301") rt.SerfAdvertiseAddrWAN = tcpAddr("1.2.3.4:8302") rt.TaggedAddresses = map[string]string{ "lan": "1.2.3.4", "lan_ipv4": "1.2.3.4", "wan": "1.2.3.4", "wan_ipv4": "1.2.3.4", } rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-advertise-wan", args: []string{ `-advertise-wan=1.2.3.4`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.AdvertiseAddrWAN = ipAddr("1.2.3.4") rt.SerfAdvertiseAddrWAN = tcpAddr("1.2.3.4:8302") rt.TaggedAddresses = map[string]string{ "lan": "10.0.0.1", "lan_ipv4": "10.0.0.1", "wan": "1.2.3.4", "wan_ipv4": "1.2.3.4", } rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-advertise and -advertise-wan", args: []string{ `-advertise=1.2.3.4`, `-advertise-wan=5.6.7.8`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.AdvertiseAddrLAN = ipAddr("1.2.3.4") rt.AdvertiseAddrWAN = ipAddr("5.6.7.8") rt.RPCAdvertiseAddr = tcpAddr("1.2.3.4:8300") rt.SerfAdvertiseAddrLAN = tcpAddr("1.2.3.4:8301") rt.SerfAdvertiseAddrWAN = tcpAddr("5.6.7.8:8302") rt.TaggedAddresses = map[string]string{ "lan": "1.2.3.4", "lan_ipv4": "1.2.3.4", "wan": "5.6.7.8", "wan_ipv4": "5.6.7.8", } rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-bind", args: []string{ `-bind=1.2.3.4`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.BindAddr = ipAddr("1.2.3.4") rt.AdvertiseAddrLAN = ipAddr("1.2.3.4") rt.AdvertiseAddrWAN = ipAddr("1.2.3.4") rt.RPCAdvertiseAddr = tcpAddr("1.2.3.4:8300") rt.RPCBindAddr = tcpAddr("1.2.3.4:8300") rt.SerfAdvertiseAddrLAN = tcpAddr("1.2.3.4:8301") rt.SerfAdvertiseAddrWAN = tcpAddr("1.2.3.4:8302") rt.SerfBindAddrLAN = tcpAddr("1.2.3.4:8301") rt.SerfBindAddrWAN = tcpAddr("1.2.3.4:8302") rt.TaggedAddresses = map[string]string{ "lan": "1.2.3.4", "lan_ipv4": "1.2.3.4", "wan": "1.2.3.4", "wan_ipv4": "1.2.3.4", } rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-bootstrap", args: []string{ `-bootstrap`, `-server`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.Bootstrap = true rt.ServerMode = true rt.LeaveOnTerm = false rt.SkipLeaveOnInt = true rt.DataDir = dataDir }, expectedWarnings: []string{"bootstrap = true: do not enable unless necessary"}, }) run(t, testCase{ desc: "-bootstrap-expect", args: []string{ `-bootstrap-expect=3`, `-server`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.BootstrapExpect = 3 rt.ServerMode = true rt.LeaveOnTerm = false rt.SkipLeaveOnInt = true rt.DataDir = dataDir }, expectedWarnings: []string{"bootstrap_expect > 0: expecting 3 servers"}, }) run(t, testCase{ desc: "-client", args: []string{ `-client=1.2.3.4`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.ClientAddrs = []*net.IPAddr{ipAddr("1.2.3.4")} rt.DNSAddrs = []net.Addr{tcpAddr("1.2.3.4:8600"), udpAddr("1.2.3.4:8600")} rt.HTTPAddrs = []net.Addr{tcpAddr("1.2.3.4:8500")} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-config-dir", args: []string{ `-data-dir=` + dataDir, `-config-dir`, filepath.Join(dataDir, "conf.d"), }, expected: func(rt *RuntimeConfig) { rt.Datacenter = "a" rt.ACLDatacenter = "a" rt.PrimaryDatacenter = "a" rt.DataDir = dataDir }, setup: func() { writeFile(filepath.Join(dataDir, "conf.d/conf.json"), []byte(`{"datacenter":"a"}`)) }, }) run(t, testCase{ desc: "-config-file json", args: []string{ `-data-dir=` + dataDir, `-config-file`, filepath.Join(dataDir, "conf.json"), }, expected: func(rt *RuntimeConfig) { rt.Datacenter = "a" rt.ACLDatacenter = "a" rt.PrimaryDatacenter = "a" rt.DataDir = dataDir }, setup: func() { writeFile(filepath.Join(dataDir, "conf.json"), []byte(`{"datacenter":"a"}`)) }, }) run(t, testCase{ desc: "-config-file hcl and json", args: []string{ `-data-dir=` + dataDir, `-config-file`, filepath.Join(dataDir, "conf.hcl"), `-config-file`, filepath.Join(dataDir, "conf.json"), }, expected: func(rt *RuntimeConfig) { rt.Datacenter = "b" rt.ACLDatacenter = "b" rt.PrimaryDatacenter = "b" rt.DataDir = dataDir }, setup: func() { writeFile(filepath.Join(dataDir, "conf.hcl"), []byte(`datacenter = "a"`)) writeFile(filepath.Join(dataDir, "conf.json"), []byte(`{"datacenter":"b"}`)) }, }) run(t, testCase{ desc: "-data-dir empty", args: []string{ `-data-dir=`, }, expectedErr: "data_dir cannot be empty", }) run(t, testCase{ desc: "-data-dir non-directory", args: []string{ `-data-dir=runtime_test.go`, }, expectedErr: `data_dir "runtime_test.go" is not a directory`, }) run(t, testCase{ desc: "-datacenter", args: []string{ `-datacenter=a`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.Datacenter = "a" rt.ACLDatacenter = "a" rt.PrimaryDatacenter = "a" rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-datacenter empty", args: []string{ `-datacenter=`, `-data-dir=` + dataDir, }, expectedErr: "datacenter cannot be empty", }) run(t, testCase{ desc: "-dev", args: []string{ `-dev`, }, expected: func(rt *RuntimeConfig) { rt.AdvertiseAddrLAN = ipAddr("127.0.0.1") rt.AdvertiseAddrWAN = ipAddr("127.0.0.1") rt.BindAddr = ipAddr("127.0.0.1") rt.ConnectEnabled = true rt.DevMode = true rt.DisableAnonymousSignature = true rt.DisableKeyringFile = true rt.EnableDebug = true rt.UIConfig.Enabled = true rt.LeaveOnTerm = false rt.Logging.LogLevel = "DEBUG" rt.RPCAdvertiseAddr = tcpAddr("127.0.0.1:8300") rt.RPCBindAddr = tcpAddr("127.0.0.1:8300") rt.SerfAdvertiseAddrLAN = tcpAddr("127.0.0.1:8301") rt.SerfAdvertiseAddrWAN = tcpAddr("127.0.0.1:8302") rt.SerfBindAddrLAN = tcpAddr("127.0.0.1:8301") rt.SerfBindAddrWAN = tcpAddr("127.0.0.1:8302") rt.ServerMode = true rt.SkipLeaveOnInt = true rt.TaggedAddresses = map[string]string{ "lan": "127.0.0.1", "lan_ipv4": "127.0.0.1", "wan": "127.0.0.1", "wan_ipv4": "127.0.0.1", } rt.ConsulCoordinateUpdatePeriod = 100 * time.Millisecond rt.ConsulRaftElectionTimeout = 52 * time.Millisecond rt.ConsulRaftHeartbeatTimeout = 35 * time.Millisecond rt.ConsulRaftLeaderLeaseTimeout = 20 * time.Millisecond rt.GossipLANGossipInterval = 100 * time.Millisecond rt.GossipLANProbeInterval = 100 * time.Millisecond rt.GossipLANProbeTimeout = 100 * time.Millisecond rt.GossipLANSuspicionMult = 3 rt.GossipWANGossipInterval = 100 * time.Millisecond rt.GossipWANProbeInterval = 100 * time.Millisecond rt.GossipWANProbeTimeout = 100 * time.Millisecond rt.GossipWANSuspicionMult = 3 rt.ConsulServerHealthInterval = 10 * time.Millisecond rt.GRPCPort = 8502 rt.GRPCAddrs = []net.Addr{tcpAddr("127.0.0.1:8502")} }, }) run(t, testCase{ desc: "-disable-host-node-id", args: []string{ `-disable-host-node-id`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.DisableHostNodeID = true rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-disable-keyring-file", args: []string{ `-disable-keyring-file`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.DisableKeyringFile = true rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-dns-port", args: []string{ `-dns-port=123`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.DNSPort = 123 rt.DNSAddrs = []net.Addr{tcpAddr("127.0.0.1:123"), udpAddr("127.0.0.1:123")} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-domain", args: []string{ `-domain=a`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.DNSDomain = "a" rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-alt-domain", args: []string{ `-alt-domain=alt`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.DNSAltDomain = "alt" rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-alt-domain can't be prefixed by DC", args: []string{ `-datacenter=a`, `-alt-domain=a.alt`, `-data-dir=` + dataDir, }, expectedErr: "alt_domain cannot start with {service,connect,node,query,addr,a}", }) run(t, testCase{ desc: "-alt-domain can't be prefixed by service", args: []string{ `-alt-domain=service.alt`, `-data-dir=` + dataDir, }, expectedErr: "alt_domain cannot start with {service,connect,node,query,addr,dc1}", }) run(t, testCase{ desc: "-alt-domain can be prefixed by non-keywords", args: []string{ `-alt-domain=mydomain.alt`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.DNSAltDomain = "mydomain.alt" rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-alt-domain can't be prefixed by DC", args: []string{ `-alt-domain=dc1.alt`, `-data-dir=` + dataDir, }, expectedErr: "alt_domain cannot start with {service,connect,node,query,addr,dc1}", }) run(t, testCase{ desc: "-enable-script-checks", args: []string{ `-enable-script-checks`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.EnableLocalScriptChecks = true rt.EnableRemoteScriptChecks = true rt.DataDir = dataDir }, expectedWarnings: []string{remoteScriptCheckSecurityWarning}, }) run(t, testCase{ desc: "-encrypt", args: []string{ `-encrypt=pUqJrVyVRj5jsiYEkM/tFQYfWyJIv4s3XkvDwy7Cu5s=`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.EncryptKey = "pUqJrVyVRj5jsiYEkM/tFQYfWyJIv4s3XkvDwy7Cu5s=" rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-config-format disabled, skip unknown files", args: []string{ `-data-dir=` + dataDir, `-config-dir`, filepath.Join(dataDir, "conf"), }, expected: func(rt *RuntimeConfig) { rt.Datacenter = "a" rt.ACLDatacenter = "a" rt.PrimaryDatacenter = "a" rt.DataDir = dataDir }, setup: func() { writeFile(filepath.Join(dataDir, "conf", "valid.json"), []byte(`{"datacenter":"a"}`)) writeFile(filepath.Join(dataDir, "conf", "invalid.skip"), []byte(`NOPE`)) }, expectedWarnings: []string{ "skipping file " + filepath.Join(dataDir, "conf", "invalid.skip") + ", extension must be .hcl or .json, or config format must be set", }, }) run(t, testCase{ desc: "-config-format=json", args: []string{ `-data-dir=` + dataDir, `-config-format=json`, `-config-file`, filepath.Join(dataDir, "conf"), }, expected: func(rt *RuntimeConfig) { rt.Datacenter = "a" rt.ACLDatacenter = "a" rt.PrimaryDatacenter = "a" rt.DataDir = dataDir }, setup: func() { writeFile(filepath.Join(dataDir, "conf"), []byte(`{"datacenter":"a"}`)) }, }) run(t, testCase{ desc: "-config-format=hcl", args: []string{ `-data-dir=` + dataDir, `-config-format=hcl`, `-config-file`, filepath.Join(dataDir, "conf"), }, expected: func(rt *RuntimeConfig) { rt.Datacenter = "a" rt.ACLDatacenter = "a" rt.PrimaryDatacenter = "a" rt.DataDir = dataDir }, setup: func() { writeFile(filepath.Join(dataDir, "conf"), []byte(`datacenter = "a"`)) }, }) run(t, testCase{ desc: "-http-port", args: []string{ `-http-port=123`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.HTTPPort = 123 rt.HTTPAddrs = []net.Addr{tcpAddr("127.0.0.1:123")} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-join", args: []string{ `-join=a`, `-join=b`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.StartJoinAddrsLAN = []string{"a", "b"} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-join-wan", args: []string{ `-join-wan=a`, `-join-wan=b`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.StartJoinAddrsWAN = []string{"a", "b"} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-log-level", args: []string{ `-log-level=a`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.Logging.LogLevel = "a" rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-log-json", args: []string{ `-log-json`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.Logging.LogJSON = true rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-log-rotate-max-files", args: []string{ `-log-rotate-max-files=2`, `-data-dir=` + dataDir, }, json: []string{`{ "log_rotate_max_files": 2 }`}, hcl: []string{`log_rotate_max_files = 2`}, expected: func(rt *RuntimeConfig) { rt.Logging.LogRotateMaxFiles = 2 rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-node", args: []string{ `-node=a`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.NodeName = "a" rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-node-id", args: []string{ `-node-id=a`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.NodeID = "a" rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-node-meta", args: []string{ `-node-meta=a:b`, `-node-meta=c:d`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.NodeMeta = map[string]string{"a": "b", "c": "d"} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-non-voting-server", args: []string{ `-non-voting-server`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.ReadReplica = true rt.DataDir = dataDir }, expectedWarnings: enterpriseReadReplicaWarnings, }) run(t, testCase{ desc: "-pid-file", args: []string{ `-pid-file=a`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.PidFile = "a" rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-primary-gateway", args: []string{ `-server`, `-datacenter=dc2`, `-primary-gateway=a`, `-primary-gateway=b`, `-data-dir=` + dataDir, }, json: []string{`{ "primary_datacenter": "dc1" }`}, hcl: []string{`primary_datacenter = "dc1"`}, expected: func(rt *RuntimeConfig) { rt.Datacenter = "dc2" rt.PrimaryDatacenter = "dc1" rt.ACLDatacenter = "dc1" rt.PrimaryGateways = []string{"a", "b"} rt.DataDir = dataDir // server things rt.ServerMode = true rt.LeaveOnTerm = false rt.SkipLeaveOnInt = true }, }) run(t, testCase{ desc: "-protocol", args: []string{ `-protocol=1`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.RPCProtocol = 1 rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-raft-protocol", args: []string{ `-raft-protocol=3`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.RaftProtocol = 3 rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-raft-protocol unsupported", args: []string{ `-raft-protocol=2`, `-data-dir=` + dataDir, }, expectedErr: "raft_protocol version 2 is not supported by this version of Consul", }) run(t, testCase{ desc: "-recursor", args: []string{ `-recursor=1.2.3.4`, `-recursor=5.6.7.8`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.DNSRecursors = []string{"1.2.3.4", "5.6.7.8"} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-rejoin", args: []string{ `-rejoin`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.RejoinAfterLeave = true rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-retry-interval", args: []string{ `-retry-interval=5s`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.RetryJoinIntervalLAN = 5 * time.Second rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-retry-interval-wan", args: []string{ `-retry-interval-wan=5s`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.RetryJoinIntervalWAN = 5 * time.Second rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-retry-join", args: []string{ `-retry-join=a`, `-retry-join=b`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.RetryJoinLAN = []string{"a", "b"} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-retry-join-wan", args: []string{ `-retry-join-wan=a`, `-retry-join-wan=b`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.RetryJoinWAN = []string{"a", "b"} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-retry-max", args: []string{ `-retry-max=1`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.RetryJoinMaxAttemptsLAN = 1 rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-retry-max-wan", args: []string{ `-retry-max-wan=1`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.RetryJoinMaxAttemptsWAN = 1 rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-serf-lan-bind", args: []string{ `-serf-lan-bind=1.2.3.4`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.SerfBindAddrLAN = tcpAddr("1.2.3.4:8301") rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-serf-lan-port", args: []string{ `-serf-lan-port=123`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.SerfPortLAN = 123 rt.SerfAdvertiseAddrLAN = tcpAddr("10.0.0.1:123") rt.SerfBindAddrLAN = tcpAddr("0.0.0.0:123") rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-serf-wan-bind", args: []string{ `-serf-wan-bind=1.2.3.4`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.SerfBindAddrWAN = tcpAddr("1.2.3.4:8302") rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-serf-wan-port", args: []string{ `-serf-wan-port=123`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.SerfPortWAN = 123 rt.SerfAdvertiseAddrWAN = tcpAddr("10.0.0.1:123") rt.SerfBindAddrWAN = tcpAddr("0.0.0.0:123") rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-server", args: []string{ `-server`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.ServerMode = true rt.LeaveOnTerm = false rt.SkipLeaveOnInt = true rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-server-port", args: []string{ `-server-port=123`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.ServerPort = 123 rt.RPCAdvertiseAddr = tcpAddr("10.0.0.1:123") rt.RPCBindAddr = tcpAddr("0.0.0.0:123") rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-syslog", args: []string{ `-syslog`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.Logging.EnableSyslog = true rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-ui", args: []string{ `-ui`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.UIConfig.Enabled = true rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-ui-dir", args: []string{ `-ui-dir=a`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.UIConfig.Dir = "a" rt.DataDir = dataDir }, }) run(t, testCase{ desc: "-ui-content-path", args: []string{ `-ui-content-path=/a/b`, `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.UIConfig.ContentPath = "/a/b/" rt.DataDir = dataDir }, }) // ------------------------------------------------------------ // ports and addresses // run(t, testCase{ desc: "bind addr any v4", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "bind_addr":"0.0.0.0" }`}, hcl: []string{`bind_addr = "0.0.0.0"`}, expected: func(rt *RuntimeConfig) { rt.AdvertiseAddrLAN = ipAddr("10.0.0.1") rt.AdvertiseAddrWAN = ipAddr("10.0.0.1") rt.BindAddr = ipAddr("0.0.0.0") rt.RPCAdvertiseAddr = tcpAddr("10.0.0.1:8300") rt.RPCBindAddr = tcpAddr("0.0.0.0:8300") rt.SerfAdvertiseAddrLAN = tcpAddr("10.0.0.1:8301") rt.SerfAdvertiseAddrWAN = tcpAddr("10.0.0.1:8302") rt.SerfBindAddrLAN = tcpAddr("0.0.0.0:8301") rt.SerfBindAddrWAN = tcpAddr("0.0.0.0:8302") rt.TaggedAddresses = map[string]string{ "lan": "10.0.0.1", "lan_ipv4": "10.0.0.1", "wan": "10.0.0.1", "wan_ipv4": "10.0.0.1", } rt.DataDir = dataDir }, }) run(t, testCase{ desc: "bind addr any v6", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "bind_addr":"::" }`}, hcl: []string{`bind_addr = "::"`}, expected: func(rt *RuntimeConfig) { rt.AdvertiseAddrLAN = ipAddr("dead:beef::1") rt.AdvertiseAddrWAN = ipAddr("dead:beef::1") rt.BindAddr = ipAddr("::") rt.RPCAdvertiseAddr = tcpAddr("[dead:beef::1]:8300") rt.RPCBindAddr = tcpAddr("[::]:8300") rt.SerfAdvertiseAddrLAN = tcpAddr("[dead:beef::1]:8301") rt.SerfAdvertiseAddrWAN = tcpAddr("[dead:beef::1]:8302") rt.SerfBindAddrLAN = tcpAddr("[::]:8301") rt.SerfBindAddrWAN = tcpAddr("[::]:8302") rt.TaggedAddresses = map[string]string{ "lan": "dead:beef::1", "lan_ipv6": "dead:beef::1", "wan": "dead:beef::1", "wan_ipv6": "dead:beef::1", } rt.DataDir = dataDir }, opts: LoadOpts{ getPublicIPv6: func() ([]*net.IPAddr, error) { return []*net.IPAddr{ipAddr("dead:beef::1")}, nil }, }, }) run(t, testCase{ desc: "bind addr any and advertise set should not detect", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "bind_addr":"0.0.0.0", "advertise_addr": "1.2.3.4" }`}, hcl: []string{`bind_addr = "0.0.0.0" advertise_addr = "1.2.3.4"`}, expected: func(rt *RuntimeConfig) { rt.AdvertiseAddrLAN = ipAddr("1.2.3.4") rt.AdvertiseAddrWAN = ipAddr("1.2.3.4") rt.BindAddr = ipAddr("0.0.0.0") rt.RPCAdvertiseAddr = tcpAddr("1.2.3.4:8300") rt.RPCBindAddr = tcpAddr("0.0.0.0:8300") rt.SerfAdvertiseAddrLAN = tcpAddr("1.2.3.4:8301") rt.SerfAdvertiseAddrWAN = tcpAddr("1.2.3.4:8302") rt.SerfBindAddrLAN = tcpAddr("0.0.0.0:8301") rt.SerfBindAddrWAN = tcpAddr("0.0.0.0:8302") rt.TaggedAddresses = map[string]string{ "lan": "1.2.3.4", "lan_ipv4": "1.2.3.4", "wan": "1.2.3.4", "wan_ipv4": "1.2.3.4", } rt.DataDir = dataDir }, opts: LoadOpts{ getPrivateIPv4: func() ([]*net.IPAddr, error) { return nil, fmt.Errorf("should not detect advertise_addr") }, }, }) run(t, testCase{ desc: "client addr and ports == 0", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "client_addr":"0.0.0.0", "ports":{} }`}, hcl: []string{` client_addr = "0.0.0.0" ports {} `}, expected: func(rt *RuntimeConfig) { rt.ClientAddrs = []*net.IPAddr{ipAddr("0.0.0.0")} rt.DNSAddrs = []net.Addr{tcpAddr("0.0.0.0:8600"), udpAddr("0.0.0.0:8600")} rt.HTTPAddrs = []net.Addr{tcpAddr("0.0.0.0:8500")} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "client addr and ports < 0", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "client_addr":"0.0.0.0", "ports": { "dns":-1, "http":-2, "https":-3, "grpc":-4 } }`}, hcl: []string{` client_addr = "0.0.0.0" ports { dns = -1 http = -2 https = -3 grpc = -4 } `}, expected: func(rt *RuntimeConfig) { rt.ClientAddrs = []*net.IPAddr{ipAddr("0.0.0.0")} rt.DNSPort = -1 rt.DNSAddrs = nil rt.HTTPPort = -1 rt.HTTPAddrs = nil // HTTPS and gRPC default to disabled so shouldn't be different from // default rt. rt.DataDir = dataDir }, }) run(t, testCase{ desc: "client addr and ports > 0", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "client_addr":"0.0.0.0", "ports":{ "dns": 1, "http": 2, "https": 3, "grpc": 4 } }`}, hcl: []string{` client_addr = "0.0.0.0" ports { dns = 1 http = 2 https = 3 grpc = 4 } `}, expected: func(rt *RuntimeConfig) { rt.ClientAddrs = []*net.IPAddr{ipAddr("0.0.0.0")} rt.DNSPort = 1 rt.DNSAddrs = []net.Addr{tcpAddr("0.0.0.0:1"), udpAddr("0.0.0.0:1")} rt.HTTPPort = 2 rt.HTTPAddrs = []net.Addr{tcpAddr("0.0.0.0:2")} rt.HTTPSPort = 3 rt.HTTPSAddrs = []net.Addr{tcpAddr("0.0.0.0:3")} rt.GRPCPort = 4 rt.GRPCAddrs = []net.Addr{tcpAddr("0.0.0.0:4")} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "client addr, addresses and ports == 0", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "client_addr":"0.0.0.0", "addresses": { "dns": "1.1.1.1", "http": "2.2.2.2", "https": "3.3.3.3", "grpc": "4.4.4.4" }, "ports":{} }`}, hcl: []string{` client_addr = "0.0.0.0" addresses = { dns = "1.1.1.1" http = "2.2.2.2" https = "3.3.3.3" grpc = "4.4.4.4" } ports {} `}, expected: func(rt *RuntimeConfig) { rt.ClientAddrs = []*net.IPAddr{ipAddr("0.0.0.0")} rt.DNSAddrs = []net.Addr{tcpAddr("1.1.1.1:8600"), udpAddr("1.1.1.1:8600")} rt.HTTPAddrs = []net.Addr{tcpAddr("2.2.2.2:8500")} // HTTPS and gRPC default to disabled so shouldn't be different from // default rt. rt.DataDir = dataDir }, }) run(t, testCase{ desc: "client addr, addresses and ports < 0", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "client_addr":"0.0.0.0", "addresses": { "dns": "1.1.1.1", "http": "2.2.2.2", "https": "3.3.3.3", "grpc": "4.4.4.4" }, "ports": { "dns":-1, "http":-2, "https":-3, "grpc":-4 } }`}, hcl: []string{` client_addr = "0.0.0.0" addresses = { dns = "1.1.1.1" http = "2.2.2.2" https = "3.3.3.3" grpc = "4.4.4.4" } ports { dns = -1 http = -2 https = -3 grpc = -4 } `}, expected: func(rt *RuntimeConfig) { rt.ClientAddrs = []*net.IPAddr{ipAddr("0.0.0.0")} rt.DNSPort = -1 rt.DNSAddrs = nil rt.HTTPPort = -1 rt.HTTPAddrs = nil // HTTPS and gRPC default to disabled so shouldn't be different from // default rt. rt.DataDir = dataDir }, }) run(t, testCase{ desc: "client addr, addresses and ports", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "client_addr": "0.0.0.0", "addresses": { "dns": "1.1.1.1", "http": "2.2.2.2", "https": "3.3.3.3", "grpc": "4.4.4.4" }, "ports":{ "dns":1, "http":2, "https":3, "grpc":4 } }`}, hcl: []string{` client_addr = "0.0.0.0" addresses = { dns = "1.1.1.1" http = "2.2.2.2" https = "3.3.3.3" grpc = "4.4.4.4" } ports { dns = 1 http = 2 https = 3 grpc = 4 } `}, expected: func(rt *RuntimeConfig) { rt.ClientAddrs = []*net.IPAddr{ipAddr("0.0.0.0")} rt.DNSPort = 1 rt.DNSAddrs = []net.Addr{tcpAddr("1.1.1.1:1"), udpAddr("1.1.1.1:1")} rt.HTTPPort = 2 rt.HTTPAddrs = []net.Addr{tcpAddr("2.2.2.2:2")} rt.HTTPSPort = 3 rt.HTTPSAddrs = []net.Addr{tcpAddr("3.3.3.3:3")} rt.GRPCPort = 4 rt.GRPCAddrs = []net.Addr{tcpAddr("4.4.4.4:4")} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "client template and ports", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "client_addr": "{{ printf \"1.2.3.4 2001:db8::1\" }}", "ports":{ "dns":1, "http":2, "https":3, "grpc":4 } }`}, hcl: []string{` client_addr = "{{ printf \"1.2.3.4 2001:db8::1\" }}" ports { dns = 1 http = 2 https = 3 grpc = 4 } `}, expected: func(rt *RuntimeConfig) { rt.ClientAddrs = []*net.IPAddr{ipAddr("1.2.3.4"), ipAddr("2001:db8::1")} rt.DNSPort = 1 rt.DNSAddrs = []net.Addr{tcpAddr("1.2.3.4:1"), tcpAddr("[2001:db8::1]:1"), udpAddr("1.2.3.4:1"), udpAddr("[2001:db8::1]:1")} rt.HTTPPort = 2 rt.HTTPAddrs = []net.Addr{tcpAddr("1.2.3.4:2"), tcpAddr("[2001:db8::1]:2")} rt.HTTPSPort = 3 rt.HTTPSAddrs = []net.Addr{tcpAddr("1.2.3.4:3"), tcpAddr("[2001:db8::1]:3")} rt.GRPCPort = 4 rt.GRPCAddrs = []net.Addr{tcpAddr("1.2.3.4:4"), tcpAddr("[2001:db8::1]:4")} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "client, address template and ports", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "client_addr": "{{ printf \"1.2.3.4 2001:db8::1\" }}", "addresses": { "dns": "{{ printf \"1.1.1.1 2001:db8::10 \" }}", "http": "{{ printf \"2.2.2.2 unix://http 2001:db8::20 \" }}", "https": "{{ printf \"3.3.3.3 unix://https 2001:db8::30 \" }}", "grpc": "{{ printf \"4.4.4.4 unix://grpc 2001:db8::40 \" }}" }, "ports":{ "dns":1, "http":2, "https":3, "grpc":4 } }`}, hcl: []string{` client_addr = "{{ printf \"1.2.3.4 2001:db8::1\" }}" addresses = { dns = "{{ printf \"1.1.1.1 2001:db8::10 \" }}" http = "{{ printf \"2.2.2.2 unix://http 2001:db8::20 \" }}" https = "{{ printf \"3.3.3.3 unix://https 2001:db8::30 \" }}" grpc = "{{ printf \"4.4.4.4 unix://grpc 2001:db8::40 \" }}" } ports { dns = 1 http = 2 https = 3 grpc = 4 } `}, expected: func(rt *RuntimeConfig) { rt.ClientAddrs = []*net.IPAddr{ipAddr("1.2.3.4"), ipAddr("2001:db8::1")} rt.DNSPort = 1 rt.DNSAddrs = []net.Addr{tcpAddr("1.1.1.1:1"), tcpAddr("[2001:db8::10]:1"), udpAddr("1.1.1.1:1"), udpAddr("[2001:db8::10]:1")} rt.HTTPPort = 2 rt.HTTPAddrs = []net.Addr{tcpAddr("2.2.2.2:2"), unixAddr("unix://http"), tcpAddr("[2001:db8::20]:2")} rt.HTTPSPort = 3 rt.HTTPSAddrs = []net.Addr{tcpAddr("3.3.3.3:3"), unixAddr("unix://https"), tcpAddr("[2001:db8::30]:3")} rt.GRPCPort = 4 rt.GRPCAddrs = []net.Addr{tcpAddr("4.4.4.4:4"), unixAddr("unix://grpc"), tcpAddr("[2001:db8::40]:4")} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "advertise address lan template", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "advertise_addr": "{{ printf \"1.2.3.4\" }}" }`}, hcl: []string{`advertise_addr = "{{ printf \"1.2.3.4\" }}"`}, expected: func(rt *RuntimeConfig) { rt.AdvertiseAddrLAN = ipAddr("1.2.3.4") rt.AdvertiseAddrWAN = ipAddr("1.2.3.4") rt.RPCAdvertiseAddr = tcpAddr("1.2.3.4:8300") rt.SerfAdvertiseAddrLAN = tcpAddr("1.2.3.4:8301") rt.SerfAdvertiseAddrWAN = tcpAddr("1.2.3.4:8302") rt.TaggedAddresses = map[string]string{ "lan": "1.2.3.4", "lan_ipv4": "1.2.3.4", "wan": "1.2.3.4", "wan_ipv4": "1.2.3.4", } rt.DataDir = dataDir }, }) run(t, testCase{ desc: "advertise address wan template", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "advertise_addr_wan": "{{ printf \"1.2.3.4\" }}" }`}, hcl: []string{`advertise_addr_wan = "{{ printf \"1.2.3.4\" }}"`}, expected: func(rt *RuntimeConfig) { rt.AdvertiseAddrWAN = ipAddr("1.2.3.4") rt.SerfAdvertiseAddrWAN = tcpAddr("1.2.3.4:8302") rt.TaggedAddresses = map[string]string{ "lan": "10.0.0.1", "lan_ipv4": "10.0.0.1", "wan": "1.2.3.4", "wan_ipv4": "1.2.3.4", } rt.DataDir = dataDir }, }) run(t, testCase{ desc: "advertise address lan with ports", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ports": { "server": 1000, "serf_lan": 2000, "serf_wan": 3000 }, "advertise_addr": "1.2.3.4" }`}, hcl: []string{` ports { server = 1000 serf_lan = 2000 serf_wan = 3000 } advertise_addr = "1.2.3.4" `}, expected: func(rt *RuntimeConfig) { rt.AdvertiseAddrLAN = ipAddr("1.2.3.4") rt.AdvertiseAddrWAN = ipAddr("1.2.3.4") rt.RPCAdvertiseAddr = tcpAddr("1.2.3.4:1000") rt.RPCBindAddr = tcpAddr("0.0.0.0:1000") rt.SerfAdvertiseAddrLAN = tcpAddr("1.2.3.4:2000") rt.SerfAdvertiseAddrWAN = tcpAddr("1.2.3.4:3000") rt.SerfBindAddrLAN = tcpAddr("0.0.0.0:2000") rt.SerfBindAddrWAN = tcpAddr("0.0.0.0:3000") rt.SerfPortLAN = 2000 rt.SerfPortWAN = 3000 rt.ServerPort = 1000 rt.TaggedAddresses = map[string]string{ "lan": "1.2.3.4", "lan_ipv4": "1.2.3.4", "wan": "1.2.3.4", "wan_ipv4": "1.2.3.4", } rt.DataDir = dataDir }, }) run(t, testCase{ desc: "advertise address wan with ports", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ports": { "server": 1000, "serf_lan": 2000, "serf_wan": 3000 }, "advertise_addr_wan": "1.2.3.4" }`}, hcl: []string{` ports { server = 1000 serf_lan = 2000 serf_wan = 3000 } advertise_addr_wan = "1.2.3.4" `}, expected: func(rt *RuntimeConfig) { rt.AdvertiseAddrLAN = ipAddr("10.0.0.1") rt.AdvertiseAddrWAN = ipAddr("1.2.3.4") rt.RPCAdvertiseAddr = tcpAddr("10.0.0.1:1000") rt.RPCBindAddr = tcpAddr("0.0.0.0:1000") rt.SerfAdvertiseAddrLAN = tcpAddr("10.0.0.1:2000") rt.SerfAdvertiseAddrWAN = tcpAddr("1.2.3.4:3000") rt.SerfBindAddrLAN = tcpAddr("0.0.0.0:2000") rt.SerfBindAddrWAN = tcpAddr("0.0.0.0:3000") rt.SerfPortLAN = 2000 rt.SerfPortWAN = 3000 rt.ServerPort = 1000 rt.TaggedAddresses = map[string]string{ "lan": "10.0.0.1", "lan_ipv4": "10.0.0.1", "wan": "1.2.3.4", "wan_ipv4": "1.2.3.4", } rt.DataDir = dataDir }, }) run(t, testCase{ desc: "allow disabling serf wan port", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ports": { "serf_wan": -1 }, "advertise_addr_wan": "1.2.3.4" }`}, hcl: []string{` ports { serf_wan = -1 } advertise_addr_wan = "1.2.3.4" `}, expected: func(rt *RuntimeConfig) { rt.AdvertiseAddrWAN = ipAddr("1.2.3.4") rt.SerfAdvertiseAddrWAN = nil rt.SerfBindAddrWAN = nil rt.TaggedAddresses = map[string]string{ "lan": "10.0.0.1", "lan_ipv4": "10.0.0.1", "wan": "1.2.3.4", "wan_ipv4": "1.2.3.4", } rt.DataDir = dataDir rt.SerfPortWAN = -1 }, }) run(t, testCase{ desc: "serf bind address lan template", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "serf_lan": "{{ printf \"1.2.3.4\" }}" }`}, hcl: []string{`serf_lan = "{{ printf \"1.2.3.4\" }}"`}, expected: func(rt *RuntimeConfig) { rt.SerfBindAddrLAN = tcpAddr("1.2.3.4:8301") rt.DataDir = dataDir }, }) run(t, testCase{ desc: "serf bind address wan template", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "serf_wan": "{{ printf \"1.2.3.4\" }}" }`}, hcl: []string{`serf_wan = "{{ printf \"1.2.3.4\" }}"`}, expected: func(rt *RuntimeConfig) { rt.SerfBindAddrWAN = tcpAddr("1.2.3.4:8302") rt.DataDir = dataDir }, }) run(t, testCase{ desc: "dns recursor templates with deduplication", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "recursors": [ "{{ printf \"5.6.7.8:9999\" }}", "{{ printf \"1.2.3.4\" }}", "{{ printf \"5.6.7.8:9999\" }}" ] }`}, hcl: []string{`recursors = [ "{{ printf \"5.6.7.8:9999\" }}", "{{ printf \"1.2.3.4\" }}", "{{ printf \"5.6.7.8:9999\" }}" ] `}, expected: func(rt *RuntimeConfig) { rt.DNSRecursors = []string{"5.6.7.8:9999", "1.2.3.4"} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "start_join address template", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "start_join": ["{{ printf \"1.2.3.4 4.3.2.1\" }}"] }`}, hcl: []string{`start_join = ["{{ printf \"1.2.3.4 4.3.2.1\" }}"]`}, expected: func(rt *RuntimeConfig) { rt.StartJoinAddrsLAN = []string{"1.2.3.4", "4.3.2.1"} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "start_join_wan address template", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "start_join_wan": ["{{ printf \"1.2.3.4 4.3.2.1\" }}"] }`}, hcl: []string{`start_join_wan = ["{{ printf \"1.2.3.4 4.3.2.1\" }}"]`}, expected: func(rt *RuntimeConfig) { rt.StartJoinAddrsWAN = []string{"1.2.3.4", "4.3.2.1"} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "retry_join address template", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "retry_join": ["{{ printf \"1.2.3.4 4.3.2.1\" }}"] }`}, hcl: []string{`retry_join = ["{{ printf \"1.2.3.4 4.3.2.1\" }}"]`}, expected: func(rt *RuntimeConfig) { rt.RetryJoinLAN = []string{"1.2.3.4", "4.3.2.1"} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "retry_join_wan address template", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "retry_join_wan": ["{{ printf \"1.2.3.4 4.3.2.1\" }}"] }`}, hcl: []string{`retry_join_wan = ["{{ printf \"1.2.3.4 4.3.2.1\" }}"]`}, expected: func(rt *RuntimeConfig) { rt.RetryJoinWAN = []string{"1.2.3.4", "4.3.2.1"} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "min/max ports for dynamic exposed listeners", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ports": { "expose_min_port": 1234, "expose_max_port": 5678 } }`}, hcl: []string{` ports { expose_min_port = 1234 expose_max_port = 5678 } `}, expected: func(rt *RuntimeConfig) { rt.ExposeMinPort = 1234 rt.ExposeMaxPort = 5678 rt.DataDir = dataDir }, }) run(t, testCase{ desc: "defaults for dynamic exposed listeners", args: []string{`-data-dir=` + dataDir}, expected: func(rt *RuntimeConfig) { rt.ExposeMinPort = 21500 rt.ExposeMaxPort = 21755 rt.DataDir = dataDir }, }) // ------------------------------------------------------------ // precedence rules // run(t, testCase{ desc: "precedence: merge order", args: []string{`-data-dir=` + dataDir}, json: []string{ `{ "bootstrap": true, "bootstrap_expect": 1, "datacenter": "a", "start_join": ["a", "b"], "node_meta": {"a":"b"} }`, `{ "bootstrap": false, "bootstrap_expect": 0, "datacenter":"b", "start_join": ["c", "d"], "node_meta": {"a":"c"} }`, }, hcl: []string{ ` bootstrap = true bootstrap_expect = 1 datacenter = "a" start_join = ["a", "b"] node_meta = { "a" = "b" } `, ` bootstrap = false bootstrap_expect = 0 datacenter = "b" start_join = ["c", "d"] node_meta = { "a" = "c" } `, }, expected: func(rt *RuntimeConfig) { rt.Bootstrap = false rt.BootstrapExpect = 0 rt.Datacenter = "b" rt.ACLDatacenter = "b" rt.PrimaryDatacenter = "b" rt.StartJoinAddrsLAN = []string{"a", "b", "c", "d"} rt.NodeMeta = map[string]string{"a": "c"} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "precedence: flag before file", json: []string{ `{ "advertise_addr": "1.2.3.4", "advertise_addr_wan": "5.6.7.8", "bootstrap":true, "bootstrap_expect": 3, "datacenter":"a", "node_meta": {"a":"b"}, "recursors":["1.2.3.5", "5.6.7.9"], "serf_lan": "a", "serf_wan": "a", "start_join":["a", "b"] }`, }, hcl: []string{ ` advertise_addr = "1.2.3.4" advertise_addr_wan = "5.6.7.8" bootstrap = true bootstrap_expect = 3 datacenter = "a" node_meta = { "a" = "b" } recursors = ["1.2.3.5", "5.6.7.9"] serf_lan = "a" serf_wan = "a" start_join = ["a", "b"] `, }, args: []string{ `-advertise=1.1.1.1`, `-advertise-wan=2.2.2.2`, `-bootstrap=false`, `-bootstrap-expect=0`, `-datacenter=b`, `-data-dir=` + dataDir, `-join`, `c`, `-join=d`, `-node-meta=a:c`, `-recursor`, `1.2.3.6`, `-recursor=5.6.7.10`, `-serf-lan-bind=3.3.3.3`, `-serf-wan-bind=4.4.4.4`, }, expected: func(rt *RuntimeConfig) { rt.AdvertiseAddrLAN = ipAddr("1.1.1.1") rt.AdvertiseAddrWAN = ipAddr("2.2.2.2") rt.RPCAdvertiseAddr = tcpAddr("1.1.1.1:8300") rt.SerfAdvertiseAddrLAN = tcpAddr("1.1.1.1:8301") rt.SerfAdvertiseAddrWAN = tcpAddr("2.2.2.2:8302") rt.Datacenter = "b" rt.ACLDatacenter = "b" rt.PrimaryDatacenter = "b" rt.DNSRecursors = []string{"1.2.3.6", "5.6.7.10", "1.2.3.5", "5.6.7.9"} rt.NodeMeta = map[string]string{"a": "c"} rt.SerfBindAddrLAN = tcpAddr("3.3.3.3:8301") rt.SerfBindAddrWAN = tcpAddr("4.4.4.4:8302") rt.StartJoinAddrsLAN = []string{"c", "d", "a", "b"} rt.TaggedAddresses = map[string]string{ "lan": "1.1.1.1", "lan_ipv4": "1.1.1.1", "wan": "2.2.2.2", "wan_ipv4": "2.2.2.2", } rt.DataDir = dataDir }, }) // ------------------------------------------------------------ // transformations // run(t, testCase{ desc: "raft performance scaling", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "performance": { "raft_multiplier": 9} }`}, hcl: []string{`performance = { raft_multiplier=9 }`}, expected: func(rt *RuntimeConfig) { rt.ConsulRaftElectionTimeout = 9 * 1000 * time.Millisecond rt.ConsulRaftHeartbeatTimeout = 9 * 1000 * time.Millisecond rt.ConsulRaftLeaderLeaseTimeout = 9 * 500 * time.Millisecond rt.DataDir = dataDir }, }) run(t, testCase{ desc: "Serf Allowed CIDRS LAN, multiple values from flags", args: []string{`-data-dir=` + dataDir, `-serf-lan-allowed-cidrs=127.0.0.0/4`, `-serf-lan-allowed-cidrs=192.168.0.0/24`}, json: []string{}, hcl: []string{}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.SerfAllowedCIDRsLAN = []net.IPNet{*(parseCIDR(t, "127.0.0.0/4")), *(parseCIDR(t, "192.168.0.0/24"))} }, }) run(t, testCase{ desc: "Serf Allowed CIDRS LAN/WAN, multiple values from HCL/JSON", args: []string{`-data-dir=` + dataDir}, json: []string{`{"serf_lan_allowed_cidrs": ["127.0.0.0/4", "192.168.0.0/24"]}`, `{"serf_wan_allowed_cidrs": ["10.228.85.46/25"]}`}, hcl: []string{`serf_lan_allowed_cidrs=["127.0.0.0/4", "192.168.0.0/24"]`, `serf_wan_allowed_cidrs=["10.228.85.46/25"]`}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.SerfAllowedCIDRsLAN = []net.IPNet{*(parseCIDR(t, "127.0.0.0/4")), *(parseCIDR(t, "192.168.0.0/24"))} rt.SerfAllowedCIDRsWAN = []net.IPNet{*(parseCIDR(t, "10.228.85.46/25"))} }, }) run(t, testCase{ desc: "Serf Allowed CIDRS WAN, multiple values from flags", args: []string{`-data-dir=` + dataDir, `-serf-wan-allowed-cidrs=192.168.4.0/24`, `-serf-wan-allowed-cidrs=192.168.3.0/24`}, json: []string{}, hcl: []string{}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.SerfAllowedCIDRsWAN = []net.IPNet{*(parseCIDR(t, "192.168.4.0/24")), *(parseCIDR(t, "192.168.3.0/24"))} }, }) // ------------------------------------------------------------ // validations // run(t, testCase{ desc: "invalid input", args: []string{`-data-dir=` + dataDir}, json: []string{`this is not JSON`}, hcl: []string{`*** 0123 this is not HCL`}, expectedErr: "failed to parse", }) run(t, testCase{ desc: "datacenter is lower-cased", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "datacenter": "A" }`}, hcl: []string{`datacenter = "A"`}, expected: func(rt *RuntimeConfig) { rt.Datacenter = "a" rt.ACLDatacenter = "a" rt.PrimaryDatacenter = "a" rt.DataDir = dataDir }, }) run(t, testCase{ desc: "acl_datacenter is lower-cased", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "acl_datacenter": "A" }`}, hcl: []string{`acl_datacenter = "A"`}, expected: func(rt *RuntimeConfig) { rt.ACLsEnabled = true rt.ACLDatacenter = "a" rt.DataDir = dataDir rt.PrimaryDatacenter = "a" }, expectedWarnings: []string{`The 'acl_datacenter' field is deprecated. Use the 'primary_datacenter' field instead.`}, }) run(t, testCase{ desc: "acl_replication_token enables acl replication", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "acl_replication_token": "a" }`}, hcl: []string{`acl_replication_token = "a"`}, expected: func(rt *RuntimeConfig) { rt.ACLTokens.ACLReplicationToken = "a" rt.ACLTokenReplication = true rt.DataDir = dataDir }, }) run(t, testCase{ desc: "acl_enforce_version_8 is deprecated", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "acl_enforce_version_8": true }`}, hcl: []string{`acl_enforce_version_8 = true`}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir }, expectedWarnings: []string{`config key "acl_enforce_version_8" is deprecated and should be removed`}, }) run(t, testCase{ desc: "advertise address detect fails v4", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "bind_addr": "0.0.0.0"}`}, hcl: []string{`bind_addr = "0.0.0.0"`}, opts: LoadOpts{ getPrivateIPv4: func() ([]*net.IPAddr, error) { return nil, errors.New("some error") }, }, expectedErr: "Error detecting private IPv4 address: some error", }) run(t, testCase{ desc: "advertise address detect none v4", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "bind_addr": "0.0.0.0"}`}, hcl: []string{`bind_addr = "0.0.0.0"`}, opts: LoadOpts{ getPrivateIPv4: func() ([]*net.IPAddr, error) { return nil, nil }, }, expectedErr: "No private IPv4 address found", }) run(t, testCase{ desc: "advertise address detect multiple v4", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "bind_addr": "0.0.0.0"}`}, hcl: []string{`bind_addr = "0.0.0.0"`}, opts: LoadOpts{ getPrivateIPv4: func() ([]*net.IPAddr, error) { return []*net.IPAddr{ipAddr("1.1.1.1"), ipAddr("2.2.2.2")}, nil }, }, expectedErr: "Multiple private IPv4 addresses found. Please configure one", }) run(t, testCase{ desc: "advertise address detect fails v6", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "bind_addr": "::"}`}, hcl: []string{`bind_addr = "::"`}, opts: LoadOpts{ getPublicIPv6: func() ([]*net.IPAddr, error) { return nil, errors.New("some error") }, }, expectedErr: "Error detecting public IPv6 address: some error", }) run(t, testCase{ desc: "advertise address detect none v6", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "bind_addr": "::"}`}, hcl: []string{`bind_addr = "::"`}, opts: LoadOpts{ getPublicIPv6: func() ([]*net.IPAddr, error) { return nil, nil }, }, expectedErr: "No public IPv6 address found", }) run(t, testCase{ desc: "advertise address detect multiple v6", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "bind_addr": "::"}`}, hcl: []string{`bind_addr = "::"`}, opts: LoadOpts{ getPublicIPv6: func() ([]*net.IPAddr, error) { return []*net.IPAddr{ipAddr("dead:beef::1"), ipAddr("dead:beef::2")}, nil }, }, expectedErr: "Multiple public IPv6 addresses found. Please configure one", }) run(t, testCase{ desc: "ae_interval is overridden by NonUserSource", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ae_interval": "-1s" }`}, hcl: []string{`ae_interval = "-1s"`}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.AEInterval = time.Minute }, }) run(t, testCase{ desc: "acl_datacenter invalid", args: []string{ `-datacenter=a`, `-data-dir=` + dataDir, }, json: []string{`{ "acl_datacenter": "%" }`}, hcl: []string{`acl_datacenter = "%"`}, expectedErr: `acl_datacenter can only contain lowercase alphanumeric, - or _ characters.`, expectedWarnings: []string{`The 'acl_datacenter' field is deprecated. Use the 'primary_datacenter' field instead.`}, }) run(t, testCase{ desc: "autopilot.max_trailing_logs invalid", args: []string{ `-datacenter=a`, `-data-dir=` + dataDir, }, json: []string{`{ "autopilot": { "max_trailing_logs": -1 } }`}, hcl: []string{`autopilot = { max_trailing_logs = -1 }`}, expectedErr: "autopilot.max_trailing_logs cannot be -1. Must be greater than or equal to zero", }) run(t, testCase{ desc: "bind_addr cannot be empty", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "bind_addr": "" }`}, hcl: []string{`bind_addr = ""`}, expectedErr: "bind_addr cannot be empty", }) run(t, testCase{ desc: "bind_addr does not allow multiple addresses", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "bind_addr": "1.1.1.1 2.2.2.2" }`}, hcl: []string{`bind_addr = "1.1.1.1 2.2.2.2"`}, expectedErr: "bind_addr cannot contain multiple addresses", }) run(t, testCase{ desc: "bind_addr cannot be a unix socket", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "bind_addr": "unix:///foo" }`}, hcl: []string{`bind_addr = "unix:///foo"`}, expectedErr: "bind_addr cannot be a unix socket", }) run(t, testCase{ desc: "bootstrap without server", args: []string{ `-datacenter=a`, `-data-dir=` + dataDir, }, json: []string{`{ "bootstrap": true }`}, hcl: []string{`bootstrap = true`}, expectedErr: "'bootstrap = true' requires 'server = true'", }) run(t, testCase{ desc: "bootstrap-expect without server", args: []string{ `-datacenter=a`, `-data-dir=` + dataDir, }, json: []string{`{ "bootstrap_expect": 3 }`}, hcl: []string{`bootstrap_expect = 3`}, expectedErr: "'bootstrap_expect > 0' requires 'server = true'", }) run(t, testCase{ desc: "bootstrap-expect invalid", args: []string{ `-datacenter=a`, `-data-dir=` + dataDir, }, json: []string{`{ "bootstrap_expect": -1 }`}, hcl: []string{`bootstrap_expect = -1`}, expectedErr: "bootstrap_expect cannot be -1. Must be greater than or equal to zero", }) run(t, testCase{ desc: "bootstrap-expect and dev mode", args: []string{ `-dev`, `-datacenter=a`, `-data-dir=` + dataDir, }, json: []string{`{ "bootstrap_expect": 3, "server": true }`}, hcl: []string{`bootstrap_expect = 3 server = true`}, expectedErr: "'bootstrap_expect > 0' not allowed in dev mode", }) run(t, testCase{ desc: "bootstrap-expect and bootstrap", args: []string{ `-datacenter=a`, `-data-dir=` + dataDir, }, json: []string{`{ "bootstrap": true, "bootstrap_expect": 3, "server": true }`}, hcl: []string{`bootstrap = true bootstrap_expect = 3 server = true`}, expectedErr: "'bootstrap_expect > 0' and 'bootstrap = true' are mutually exclusive", }) run(t, testCase{ desc: "bootstrap-expect=1 equals bootstrap", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "bootstrap_expect": 1, "server": true }`}, hcl: []string{`bootstrap_expect = 1 server = true`}, expected: func(rt *RuntimeConfig) { rt.Bootstrap = true rt.BootstrapExpect = 0 rt.LeaveOnTerm = false rt.ServerMode = true rt.SkipLeaveOnInt = true rt.DataDir = dataDir }, expectedWarnings: []string{"BootstrapExpect is set to 1; this is the same as Bootstrap mode.", "bootstrap = true: do not enable unless necessary"}, }) run(t, testCase{ desc: "bootstrap-expect=2 warning", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "bootstrap_expect": 2, "server": true }`}, hcl: []string{`bootstrap_expect = 2 server = true`}, expected: func(rt *RuntimeConfig) { rt.BootstrapExpect = 2 rt.LeaveOnTerm = false rt.ServerMode = true rt.SkipLeaveOnInt = true rt.DataDir = dataDir }, expectedWarnings: []string{ `bootstrap_expect = 2: A cluster with 2 servers will provide no failure tolerance. See https://www.consul.io/docs/internals/consensus.html#deployment-table`, `bootstrap_expect > 0: expecting 2 servers`, }, }) run(t, testCase{ desc: "bootstrap-expect > 2 but even warning", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "bootstrap_expect": 4, "server": true }`}, hcl: []string{`bootstrap_expect = 4 server = true`}, expected: func(rt *RuntimeConfig) { rt.BootstrapExpect = 4 rt.LeaveOnTerm = false rt.ServerMode = true rt.SkipLeaveOnInt = true rt.DataDir = dataDir }, expectedWarnings: []string{ `bootstrap_expect is even number: A cluster with an even number of servers does not achieve optimum fault tolerance. See https://www.consul.io/docs/internals/consensus.html#deployment-table`, `bootstrap_expect > 0: expecting 4 servers`, }, }) run(t, testCase{ desc: "client mode sets LeaveOnTerm and SkipLeaveOnInt correctly", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "server": false }`}, hcl: []string{` server = false`}, expected: func(rt *RuntimeConfig) { rt.LeaveOnTerm = true rt.ServerMode = false rt.SkipLeaveOnInt = false rt.DataDir = dataDir }, }) run(t, testCase{ desc: "client does not allow socket", args: []string{ `-datacenter=a`, `-data-dir=` + dataDir, }, json: []string{`{ "client_addr": "unix:///foo" }`}, hcl: []string{`client_addr = "unix:///foo"`}, expectedErr: "client_addr cannot be a unix socket", }) run(t, testCase{ desc: "datacenter invalid", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "datacenter": "%" }`}, hcl: []string{`datacenter = "%"`}, expectedErr: `datacenter can only contain lowercase alphanumeric, - or _ characters.`, }) run(t, testCase{ desc: "dns does not allow socket", args: []string{ `-datacenter=a`, `-data-dir=` + dataDir, }, json: []string{`{ "addresses": {"dns": "unix:///foo" } }`}, hcl: []string{`addresses = { dns = "unix:///foo" }`}, expectedErr: "DNS address cannot be a unix socket", }) run(t, testCase{ desc: "ui enabled and dir specified", args: []string{ `-datacenter=a`, `-data-dir=` + dataDir, }, json: []string{`{ "ui_config": { "enabled": true, "dir": "a" } }`}, hcl: []string{`ui_config { enabled = true dir = "a"}`}, expectedErr: "Both the ui_config.enabled and ui_config.dir (or -ui and -ui-dir) were specified, please provide only one.\n" + "If trying to use your own web UI resources, use ui_config.dir or the -ui-dir flag.\n" + "The web UI is included in the binary so use ui_config.enabled or the -ui flag to enable it", }) // test ANY address failures // to avoid combinatory explosion for tests use 0.0.0.0, :: or [::] but not all of them run(t, testCase{ desc: "advertise_addr any", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "advertise_addr": "0.0.0.0" }`}, hcl: []string{`advertise_addr = "0.0.0.0"`}, expectedErr: "Advertise address cannot be 0.0.0.0, :: or [::]", }) run(t, testCase{ desc: "advertise_addr_wan any", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "advertise_addr_wan": "::" }`}, hcl: []string{`advertise_addr_wan = "::"`}, expectedErr: "Advertise WAN address cannot be 0.0.0.0, :: or [::]", }) run(t, testCase{ desc: "recursors any", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "recursors": ["::"] }`}, hcl: []string{`recursors = ["::"]`}, expectedErr: "DNS recursor address cannot be 0.0.0.0, :: or [::]", }) run(t, testCase{ desc: "dns_config.udp_answer_limit invalid", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "dns_config": { "udp_answer_limit": -1 } }`}, hcl: []string{`dns_config = { udp_answer_limit = -1 }`}, expectedErr: "dns_config.udp_answer_limit cannot be -1. Must be greater than or equal to zero", }) run(t, testCase{ desc: "dns_config.a_record_limit invalid", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "dns_config": { "a_record_limit": -1 } }`}, hcl: []string{`dns_config = { a_record_limit = -1 }`}, expectedErr: "dns_config.a_record_limit cannot be -1. Must be greater than or equal to zero", }) run(t, testCase{ desc: "performance.raft_multiplier < 0", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "performance": { "raft_multiplier": -1 } }`}, hcl: []string{`performance = { raft_multiplier = -1 }`}, expectedErr: `performance.raft_multiplier cannot be -1. Must be between 1 and 10`, }) run(t, testCase{ desc: "performance.raft_multiplier == 0", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "performance": { "raft_multiplier": 0 } }`}, hcl: []string{`performance = { raft_multiplier = 0 }`}, expectedErr: `performance.raft_multiplier cannot be 0. Must be between 1 and 10`, }) run(t, testCase{ desc: "performance.raft_multiplier > 10", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "performance": { "raft_multiplier": 20 } }`}, hcl: []string{`performance = { raft_multiplier = 20 }`}, expectedErr: `performance.raft_multiplier cannot be 20. Must be between 1 and 10`, }) run(t, testCase{ desc: "node_name invalid", args: []string{ `-data-dir=` + dataDir, `-node=`, }, opts: LoadOpts{ hostname: func() (string, error) { return "", nil }, }, expectedErr: "node_name cannot be empty", }) run(t, testCase{ desc: "node_meta key too long", args: []string{ `-data-dir=` + dataDir, }, json: []string{ `{ "dns_config": { "udp_answer_limit": 1 } }`, `{ "node_meta": { "` + randomString(130) + `": "a" } }`, }, hcl: []string{ `dns_config = { udp_answer_limit = 1 }`, `node_meta = { "` + randomString(130) + `" = "a" }`, }, expectedErr: "Key is too long (limit: 128 characters)", }) run(t, testCase{ desc: "node_meta value too long", args: []string{ `-data-dir=` + dataDir, }, json: []string{ `{ "dns_config": { "udp_answer_limit": 1 } }`, `{ "node_meta": { "a": "` + randomString(520) + `" } }`, }, hcl: []string{ `dns_config = { udp_answer_limit = 1 }`, `node_meta = { "a" = "` + randomString(520) + `" }`, }, expectedErr: "Value is too long (limit: 512 characters)", }) run(t, testCase{ desc: "node_meta too many keys", args: []string{ `-data-dir=` + dataDir, }, json: []string{ `{ "dns_config": { "udp_answer_limit": 1 } }`, `{ "node_meta": {` + metaPairs(70, "json") + `} }`, }, hcl: []string{ `dns_config = { udp_answer_limit = 1 }`, `node_meta = {` + metaPairs(70, "hcl") + ` }`, }, expectedErr: "Node metadata cannot contain more than 64 key/value pairs", }) run(t, testCase{ desc: "unique listeners dns vs http", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "client_addr": "1.2.3.4", "ports": { "dns": 1000, "http": 1000 } }`}, hcl: []string{` client_addr = "1.2.3.4" ports = { dns = 1000 http = 1000 } `}, expectedErr: "HTTP address 1.2.3.4:1000 already configured for DNS", }) run(t, testCase{ desc: "unique listeners dns vs https", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "client_addr": "1.2.3.4", "ports": { "dns": 1000, "https": 1000 } }`}, hcl: []string{` client_addr = "1.2.3.4" ports = { dns = 1000 https = 1000 } `}, expectedErr: "HTTPS address 1.2.3.4:1000 already configured for DNS", }) run(t, testCase{ desc: "unique listeners http vs https", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "client_addr": "1.2.3.4", "ports": { "http": 1000, "https": 1000 } }`}, hcl: []string{` client_addr = "1.2.3.4" ports = { http = 1000 https = 1000 } `}, expectedErr: "HTTPS address 1.2.3.4:1000 already configured for HTTP", }) run(t, testCase{ desc: "unique advertise addresses HTTP vs RPC", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "addresses": { "http": "10.0.0.1" }, "ports": { "http": 1000, "server": 1000 } }`}, hcl: []string{` addresses = { http = "10.0.0.1" } ports = { http = 1000 server = 1000 } `}, expectedErr: "RPC Advertise address 10.0.0.1:1000 already configured for HTTP", }) run(t, testCase{ desc: "unique advertise addresses RPC vs Serf LAN", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "ports": { "server": 1000, "serf_lan": 1000 } }`}, hcl: []string{` ports = { server = 1000 serf_lan = 1000 } `}, expectedErr: "Serf Advertise LAN address 10.0.0.1:1000 already configured for RPC Advertise", }) run(t, testCase{ desc: "unique advertise addresses RPC vs Serf WAN", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "ports": { "server": 1000, "serf_wan": 1000 } }`}, hcl: []string{` ports = { server = 1000 serf_wan = 1000 } `}, expectedErr: "Serf Advertise WAN address 10.0.0.1:1000 already configured for RPC Advertise", }) run(t, testCase{ desc: "http use_cache defaults to true", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "http_config": {} }`}, hcl: []string{` http_config = {} `}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.HTTPUseCache = true }, }) run(t, testCase{ desc: "http use_cache is enabled when true", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "http_config": { "use_cache": true } }`}, hcl: []string{` http_config = { use_cache = true } `}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.HTTPUseCache = true }, }) run(t, testCase{ desc: "http use_cache is disabled when false", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "http_config": { "use_cache": false } }`}, hcl: []string{` http_config = { use_cache = false } `}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.HTTPUseCache = false }, }) run(t, testCase{ desc: "sidecar_service can't have ID", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "service": { "name": "web", "port": 1234, "connect": { "sidecar_service": { "ID": "random-sidecar-id" } } } }`}, hcl: []string{` service { name = "web" port = 1234 connect { sidecar_service { ID = "random-sidecar-id" } } } `}, expectedErr: "sidecar_service can't specify an ID", }) run(t, testCase{ desc: "sidecar_service can't have nested sidecar", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "service": { "name": "web", "port": 1234, "connect": { "sidecar_service": { "connect": { "sidecar_service": {} } } } } }`}, hcl: []string{` service { name = "web" port = 1234 connect { sidecar_service { connect { sidecar_service { } } } } } `}, expectedErr: "sidecar_service can't have a nested sidecar_service", }) run(t, testCase{ desc: "telemetry.prefix_filter cannot be empty", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "telemetry": { "prefix_filter": [""] } }`}, hcl: []string{` telemetry = { prefix_filter = [""] } `}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir }, expectedWarnings: []string{"Cannot have empty filter rule in prefix_filter"}, }) run(t, testCase{ desc: "telemetry.prefix_filter must start with + or -", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "telemetry": { "prefix_filter": ["+foo", "-bar", "nix"] } }`}, hcl: []string{` telemetry = { prefix_filter = ["+foo", "-bar", "nix"] } `}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.Telemetry.AllowedPrefixes = []string{"foo"} rt.Telemetry.BlockedPrefixes = []string{"bar"} }, expectedWarnings: []string{`Filter rule must begin with either '+' or '-': "nix"`}, }) run(t, testCase{ desc: "encrypt has invalid key", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "encrypt": "this is not a valid key" }`}, hcl: []string{` encrypt = "this is not a valid key" `}, expectedErr: "encrypt has invalid key: illegal base64 data at input byte 4", }) run(t, testCase{ desc: "multiple check files", args: []string{ `-data-dir=` + dataDir, }, json: []string{ `{ "check": { "name": "a", "args": ["/bin/true"] } }`, `{ "check": { "name": "b", "args": ["/bin/false"] } }`, }, hcl: []string{ `check = { name = "a" args = ["/bin/true"] }`, `check = { name = "b" args = ["/bin/false"] }`, }, expected: func(rt *RuntimeConfig) { rt.Checks = []*structs.CheckDefinition{ {Name: "a", ScriptArgs: []string{"/bin/true"}, OutputMaxSize: checks.DefaultBufSize}, {Name: "b", ScriptArgs: []string{"/bin/false"}, OutputMaxSize: checks.DefaultBufSize}, } rt.DataDir = dataDir }, }) run(t, testCase{ desc: "grpc check", args: []string{ `-data-dir=` + dataDir, }, json: []string{ `{ "check": { "name": "a", "grpc": "localhost:12345/foo", "grpc_use_tls": true } }`, }, hcl: []string{ `check = { name = "a" grpc = "localhost:12345/foo", grpc_use_tls = true }`, }, expected: func(rt *RuntimeConfig) { rt.Checks = []*structs.CheckDefinition{ {Name: "a", GRPC: "localhost:12345/foo", GRPCUseTLS: true, OutputMaxSize: checks.DefaultBufSize}, } rt.DataDir = dataDir }, }) run(t, testCase{ desc: "alias check with no node", args: []string{ `-data-dir=` + dataDir, }, json: []string{ `{ "check": { "name": "a", "alias_service": "foo" } }`, }, hcl: []string{ `check = { name = "a", alias_service = "foo" }`, }, expected: func(rt *RuntimeConfig) { rt.Checks = []*structs.CheckDefinition{ {Name: "a", AliasService: "foo", OutputMaxSize: checks.DefaultBufSize}, } rt.DataDir = dataDir }, }) run(t, testCase{ desc: "multiple service files", args: []string{ `-data-dir=` + dataDir, }, json: []string{ `{ "service": { "name": "a", "port": 80 } }`, `{ "service": { "name": "b", "port": 90, "meta": {"my": "value"}, "weights": {"passing": 13} } }`, }, hcl: []string{ `service = { name = "a" port = 80 }`, `service = { name = "b" port = 90 meta={my="value"}, weights={passing=13}}`, }, expected: func(rt *RuntimeConfig) { rt.Services = []*structs.ServiceDefinition{ { Name: "a", Port: 80, Weights: &structs.Weights{ Passing: 1, Warning: 1, }, }, { Name: "b", Port: 90, Meta: map[string]string{"my": "value"}, Weights: &structs.Weights{ Passing: 13, Warning: 1, }, }, } rt.DataDir = dataDir }, }) run(t, testCase{ desc: "service with wrong meta: too long key", args: []string{ `-data-dir=` + dataDir, }, json: []string{ `{ "service": { "name": "a", "port": 80, "meta": { "` + randomString(520) + `": "metaValue" } } }`, }, hcl: []string{ `service = { name = "a" port = 80, meta={` + randomString(520) + `="metaValue"} }`, }, expectedErr: `Key is too long`, }) run(t, testCase{ desc: "service with wrong meta: too long value", args: []string{ `-data-dir=` + dataDir, }, json: []string{ `{ "service": { "name": "a", "port": 80, "meta": { "a": "` + randomString(520) + `" } } }`, }, hcl: []string{ `service = { name = "a" port = 80, meta={a="` + randomString(520) + `"} }`, }, expectedErr: `Value is too long`, }) run(t, testCase{ desc: "service with wrong meta: too many meta", args: []string{ `-data-dir=` + dataDir, }, json: []string{ `{ "service": { "name": "a", "port": 80, "meta": { ` + metaPairs(70, "json") + `} } }`, }, hcl: []string{ `service = { name = "a" port = 80 meta={` + metaPairs(70, "hcl") + `} }`, }, expectedErr: `invalid meta for service a: Node metadata cannot contain more than 64 key`, }) run(t, testCase{ desc: "translated keys", args: []string{ `-data-dir=` + dataDir, }, json: []string{ `{ "service": { "name": "a", "port": 80, "tagged_addresses": { "wan": { "address": "198.18.3.4", "port": 443 } }, "enable_tag_override": true, "check": { "id": "x", "name": "y", "DockerContainerID": "z", "DeregisterCriticalServiceAfter": "10s", "ScriptArgs": ["a", "b"] } } }`, }, hcl: []string{ `service = { name = "a" port = 80 enable_tag_override = true tagged_addresses = { wan = { address = "198.18.3.4" port = 443 } } check = { id = "x" name = "y" DockerContainerID = "z" DeregisterCriticalServiceAfter = "10s" ScriptArgs = ["a", "b"] } }`, }, expected: func(rt *RuntimeConfig) { rt.Services = []*structs.ServiceDefinition{ { Name: "a", Port: 80, TaggedAddresses: map[string]structs.ServiceAddress{ "wan": { Address: "198.18.3.4", Port: 443, }, }, EnableTagOverride: true, Checks: []*structs.CheckType{ { CheckID: types.CheckID("x"), Name: "y", DockerContainerID: "z", DeregisterCriticalServiceAfter: 10 * time.Second, ScriptArgs: []string{"a", "b"}, OutputMaxSize: checks.DefaultBufSize, }, }, Weights: &structs.Weights{ Passing: 1, Warning: 1, }, }, } rt.DataDir = dataDir }, }) run(t, testCase{ desc: "ignore snapshot_agent sub-object", args: []string{ `-data-dir=` + dataDir, }, json: []string{ `{ "snapshot_agent": { "dont": "care" } }`, }, hcl: []string{ `snapshot_agent = { dont = "care" }`, }, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir }, }) run(t, testCase{ // Test that slices in structured config are preserved by // decode.HookWeakDecodeFromSlice. desc: "service.connectsidecar_service with checks and upstreams", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "service": { "name": "web", "port": 1234, "connect": { "sidecar_service": { "port": 2345, "checks": [ { "TCP": "127.0.0.1:2345", "Interval": "10s" } ], "proxy": { "expose": { "checks": true, "paths": [ { "path": "/health", "local_path_port": 8080, "listener_port": 21500, "protocol": "http" } ] }, "upstreams": [ { "destination_name": "db", "local_bind_port": 7000 } ] } } } } }`}, hcl: []string{` service { name = "web" port = 1234 connect { sidecar_service { port = 2345 checks = [ { tcp = "127.0.0.1:2345" interval = "10s" } ] proxy { expose { checks = true paths = [ { path = "/health" local_path_port = 8080 listener_port = 21500 protocol = "http" } ] }, upstreams = [ { destination_name = "db" local_bind_port = 7000 }, ] } } } } `}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.Services = []*structs.ServiceDefinition{ { Name: "web", Port: 1234, Connect: &structs.ServiceConnect{ SidecarService: &structs.ServiceDefinition{ Port: 2345, Checks: structs.CheckTypes{ { TCP: "127.0.0.1:2345", Interval: 10 * time.Second, OutputMaxSize: checks.DefaultBufSize, }, }, Proxy: &structs.ConnectProxyConfig{ Expose: structs.ExposeConfig{ Checks: true, Paths: []structs.ExposePath{ { Path: "/health", LocalPathPort: 8080, ListenerPort: 21500, Protocol: "http", }, }, }, Upstreams: structs.Upstreams{ structs.Upstream{ DestinationType: "service", DestinationName: "db", LocalBindPort: 7000, }, }, }, Weights: &structs.Weights{ Passing: 1, Warning: 1, }, }, }, Weights: &structs.Weights{ Passing: 1, Warning: 1, }, }, } }, }) run(t, testCase{ // Test that slices in structured config are preserved by // decode.HookWeakDecodeFromSlice. desc: "services.connect.sidecar_service with checks and upstreams", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "services": [{ "name": "web", "port": 1234, "connect": { "sidecar_service": { "port": 2345, "checks": [ { "TCP": "127.0.0.1:2345", "Interval": "10s" } ], "proxy": { "expose": { "checks": true, "paths": [ { "path": "/health", "local_path_port": 8080, "listener_port": 21500, "protocol": "http" } ] }, "upstreams": [ { "destination_name": "db", "local_bind_port": 7000 } ] } } } }] }`}, hcl: []string{` services = [{ name = "web" port = 1234 connect { sidecar_service { port = 2345 checks = [ { tcp = "127.0.0.1:2345" interval = "10s" } ] proxy { expose { checks = true paths = [ { path = "/health" local_path_port = 8080 listener_port = 21500 protocol = "http" } ] }, upstreams = [ { destination_name = "db" local_bind_port = 7000 }, ] } } } }] `}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.Services = []*structs.ServiceDefinition{ { Name: "web", Port: 1234, Connect: &structs.ServiceConnect{ SidecarService: &structs.ServiceDefinition{ Port: 2345, Checks: structs.CheckTypes{ { TCP: "127.0.0.1:2345", Interval: 10 * time.Second, OutputMaxSize: checks.DefaultBufSize, }, }, Proxy: &structs.ConnectProxyConfig{ Expose: structs.ExposeConfig{ Checks: true, Paths: []structs.ExposePath{ { Path: "/health", LocalPathPort: 8080, ListenerPort: 21500, Protocol: "http", }, }, }, Upstreams: structs.Upstreams{ structs.Upstream{ DestinationType: "service", DestinationName: "db", LocalBindPort: 7000, }, }, }, Weights: &structs.Weights{ Passing: 1, Warning: 1, }, }, }, Weights: &structs.Weights{ Passing: 1, Warning: 1, }, }, } }, }) run(t, testCase{ // This tests checks that VerifyServerHostname implies VerifyOutgoing desc: "verify_server_hostname implies verify_outgoing", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "verify_server_hostname": true }`}, hcl: []string{` verify_server_hostname = true `}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.VerifyServerHostname = true rt.VerifyOutgoing = true }, }) run(t, testCase{ desc: "auto_encrypt.allow_tls works implies connect", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "verify_incoming": true, "auto_encrypt": { "allow_tls": true }, "server": true }`}, hcl: []string{` verify_incoming = true auto_encrypt { allow_tls = true } server = true `}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.VerifyIncoming = true rt.AutoEncryptAllowTLS = true rt.ConnectEnabled = true // server things rt.ServerMode = true rt.LeaveOnTerm = false rt.SkipLeaveOnInt = true }, }) run(t, testCase{ desc: "auto_encrypt.allow_tls works with verify_incoming", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "verify_incoming": true, "auto_encrypt": { "allow_tls": true }, "server": true }`}, hcl: []string{` verify_incoming = true auto_encrypt { allow_tls = true } server = true `}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.VerifyIncoming = true rt.AutoEncryptAllowTLS = true rt.ConnectEnabled = true // server things rt.ServerMode = true rt.LeaveOnTerm = false rt.SkipLeaveOnInt = true }, }) run(t, testCase{ desc: "auto_encrypt.allow_tls works with verify_incoming_rpc", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "verify_incoming_rpc": true, "auto_encrypt": { "allow_tls": true }, "server": true }`}, hcl: []string{` verify_incoming_rpc = true auto_encrypt { allow_tls = true } server = true `}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.VerifyIncomingRPC = true rt.AutoEncryptAllowTLS = true rt.ConnectEnabled = true // server things rt.ServerMode = true rt.LeaveOnTerm = false rt.SkipLeaveOnInt = true }, }) run(t, testCase{ desc: "auto_encrypt.allow_tls warns without verify_incoming or verify_incoming_rpc", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "auto_encrypt": { "allow_tls": true }, "server": true }`}, hcl: []string{` auto_encrypt { allow_tls = true } server = true `}, expectedWarnings: []string{"if auto_encrypt.allow_tls is turned on, either verify_incoming or verify_incoming_rpc should be enabled. It is necessary to turn it off during a migration to TLS, but it should definitely be turned on afterwards."}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.AutoEncryptAllowTLS = true rt.ConnectEnabled = true // server things rt.ServerMode = true rt.LeaveOnTerm = false rt.SkipLeaveOnInt = true }, }) run(t, testCase{ desc: "rpc.enable_streaming = true has no effect when not running in server mode", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "rpc": { "enable_streaming": true } }`}, hcl: []string{` rpc { enable_streaming = true } `}, expectedWarnings: []string{"rpc.enable_streaming = true has no effect when not running in server mode"}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir // rpc.enable_streaming make no sense in not-server mode rt.RPCConfig.EnableStreaming = true rt.ServerMode = false }, }) run(t, testCase{ desc: "use_streaming_backend = true requires rpc.enable_streaming on servers to work properly", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "use_streaming_backend": true, "server": true }`}, hcl: []string{` use_streaming_backend = true server = true `}, expectedWarnings: []string{"use_streaming_backend = true requires rpc.enable_streaming on servers to work properly"}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.UseStreamingBackend = true // server things rt.ServerMode = true rt.LeaveOnTerm = false rt.SkipLeaveOnInt = true }, }) run(t, testCase{ desc: "auto_encrypt.allow_tls errors in client mode", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "auto_encrypt": { "allow_tls": true }, "server": false }`}, hcl: []string{` auto_encrypt { allow_tls = true } server = false `}, expectedErr: "auto_encrypt.allow_tls can only be used on a server.", }) run(t, testCase{ desc: "auto_encrypt.tls errors in server mode", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "auto_encrypt": { "tls": true }, "server": true }`}, hcl: []string{` auto_encrypt { tls = true } server = true `}, expectedErr: "auto_encrypt.tls can only be used on a client.", }) run(t, testCase{ desc: "test connect vault provider configuration", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "connect": { "enabled": true, "ca_provider": "vault", "ca_config": { "ca_file": "/capath/ca.pem", "ca_path": "/capath/", "cert_file": "/certpath/cert.pem", "key_file": "/certpath/key.pem", "tls_server_name": "server.name", "tls_skip_verify": true, "token": "abc", "root_pki_path": "consul-vault", "intermediate_pki_path": "connect-intermediate" } } }`}, hcl: []string{` connect { enabled = true ca_provider = "vault" ca_config { ca_file = "/capath/ca.pem" ca_path = "/capath/" cert_file = "/certpath/cert.pem" key_file = "/certpath/key.pem" tls_server_name = "server.name" tls_skip_verify = true token = "abc" root_pki_path = "consul-vault" intermediate_pki_path = "connect-intermediate" } } `}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.ConnectEnabled = true rt.ConnectCAProvider = "vault" rt.ConnectCAConfig = map[string]interface{}{ "CAFile": "/capath/ca.pem", "CAPath": "/capath/", "CertFile": "/certpath/cert.pem", "KeyFile": "/certpath/key.pem", "TLSServerName": "server.name", "TLSSkipVerify": true, "Token": "abc", "RootPKIPath": "consul-vault", "IntermediatePKIPath": "connect-intermediate", } }, }) run(t, testCase{ desc: "Connect AWS CA provider configuration", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "connect": { "enabled": true, "ca_provider": "aws-pca", "ca_config": { "existing_arn": "foo", "delete_on_exit": true } } }`}, hcl: []string{` connect { enabled = true ca_provider = "aws-pca" ca_config { existing_arn = "foo" delete_on_exit = true } } `}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.ConnectEnabled = true rt.ConnectCAProvider = "aws-pca" rt.ConnectCAConfig = map[string]interface{}{ "ExistingARN": "foo", "DeleteOnExit": true, } }, }) run(t, testCase{ desc: "Connect AWS CA provider TTL validation", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "connect": { "enabled": true, "ca_provider": "aws-pca", "ca_config": { "leaf_cert_ttl": "1h" } } }`}, hcl: []string{` connect { enabled = true ca_provider = "aws-pca" ca_config { leaf_cert_ttl = "1h" } } `}, expectedErr: "AWS PCA doesn't support certificates that are valid for less than 24 hours", }) run(t, testCase{ desc: "Connect AWS CA provider EC key length validation", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "connect": { "enabled": true, "ca_provider": "aws-pca", "ca_config": { "private_key_bits": 521 } } }`}, hcl: []string{` connect { enabled = true ca_provider = "aws-pca" ca_config { private_key_bits = 521 } } `}, expectedErr: "AWS PCA only supports P256 EC curve", }) run(t, testCase{ desc: "connect.enable_mesh_gateway_wan_federation requires connect.enabled", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "connect": { "enabled": false, "enable_mesh_gateway_wan_federation": true } }`}, hcl: []string{` connect { enabled = false enable_mesh_gateway_wan_federation = true } `}, expectedErr: "'connect.enable_mesh_gateway_wan_federation=true' requires 'connect.enabled=true'", }) run(t, testCase{ desc: "connect.enable_mesh_gateway_wan_federation cannot use -join-wan", args: []string{ `-data-dir=` + dataDir, `-join-wan=1.2.3.4`, }, json: []string{`{ "server": true, "primary_datacenter": "one", "datacenter": "one", "connect": { "enabled": true, "enable_mesh_gateway_wan_federation": true } }`}, hcl: []string{` server = true primary_datacenter = "one" datacenter = "one" connect { enabled = true enable_mesh_gateway_wan_federation = true } `}, expectedErr: "'start_join_wan' is incompatible with 'connect.enable_mesh_gateway_wan_federation = true'", }) run(t, testCase{ desc: "connect.enable_mesh_gateway_wan_federation cannot use -retry-join-wan", args: []string{ `-data-dir=` + dataDir, `-retry-join-wan=1.2.3.4`, }, json: []string{`{ "server": true, "primary_datacenter": "one", "datacenter": "one", "connect": { "enabled": true, "enable_mesh_gateway_wan_federation": true } }`}, hcl: []string{` server = true primary_datacenter = "one" datacenter = "one" connect { enabled = true enable_mesh_gateway_wan_federation = true } `}, expectedErr: "'retry_join_wan' is incompatible with 'connect.enable_mesh_gateway_wan_federation = true'", }) run(t, testCase{ desc: "connect.enable_mesh_gateway_wan_federation requires server mode", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "server": false, "connect": { "enabled": true, "enable_mesh_gateway_wan_federation": true } }`}, hcl: []string{` server = false connect { enabled = true enable_mesh_gateway_wan_federation = true } `}, expectedErr: "'connect.enable_mesh_gateway_wan_federation = true' requires 'server = true'", }) run(t, testCase{ desc: "connect.enable_mesh_gateway_wan_federation requires no slashes in node names", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "server": true, "node_name": "really/why", "connect": { "enabled": true, "enable_mesh_gateway_wan_federation": true } }`}, hcl: []string{` server = true node_name = "really/why" connect { enabled = true enable_mesh_gateway_wan_federation = true } `}, expectedErr: "'connect.enable_mesh_gateway_wan_federation = true' requires that 'node_name' not contain '/' characters", }) run(t, testCase{ desc: "primary_gateways requires server mode", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "server": false, "primary_gateways": [ "foo.local", "bar.local" ] }`}, hcl: []string{` server = false primary_gateways = [ "foo.local", "bar.local" ] `}, expectedErr: "'primary_gateways' requires 'server = true'", }) run(t, testCase{ desc: "primary_gateways only works in a secondary datacenter", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "server": true, "primary_datacenter": "one", "datacenter": "one", "primary_gateways": [ "foo.local", "bar.local" ] }`}, hcl: []string{` server = true primary_datacenter = "one" datacenter = "one" primary_gateways = [ "foo.local", "bar.local" ] `}, expectedErr: "'primary_gateways' should only be configured in a secondary datacenter", }) run(t, testCase{ desc: "connect.enable_mesh_gateway_wan_federation in secondary with primary_gateways configured", args: []string{ `-data-dir=` + dataDir, }, json: []string{`{ "server": true, "primary_datacenter": "one", "datacenter": "two", "primary_gateways": [ "foo.local", "bar.local" ], "connect": { "enabled": true, "enable_mesh_gateway_wan_federation": true } }`}, hcl: []string{` server = true primary_datacenter = "one" datacenter = "two" primary_gateways = [ "foo.local", "bar.local" ] connect { enabled = true enable_mesh_gateway_wan_federation = true } `}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.Datacenter = "two" rt.PrimaryDatacenter = "one" rt.ACLDatacenter = "one" rt.PrimaryGateways = []string{"foo.local", "bar.local"} rt.ConnectEnabled = true rt.ConnectMeshGatewayWANFederationEnabled = true // server things rt.ServerMode = true rt.LeaveOnTerm = false rt.SkipLeaveOnInt = true }, }) // ------------------------------------------------------------ // ConfigEntry Handling // run(t, testCase{ desc: "ConfigEntry bootstrap doesn't parse", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "config_entries": { "bootstrap": [ { "foo": "bar" } ] } }`}, hcl: []string{` config_entries { bootstrap { foo = "bar" } }`}, expectedErr: "config_entries.bootstrap[0]: Payload does not contain a kind/Kind", }) run(t, testCase{ desc: "ConfigEntry bootstrap unknown kind", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "config_entries": { "bootstrap": [ { "kind": "foo", "name": "bar", "baz": 1 } ] } }`}, hcl: []string{` config_entries { bootstrap { kind = "foo" name = "bar" baz = 1 } }`}, expectedErr: "config_entries.bootstrap[0]: invalid config entry kind: foo", }) run(t, testCase{ desc: "ConfigEntry bootstrap invalid service-defaults", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "config_entries": { "bootstrap": [ { "kind": "service-defaults", "name": "web", "made_up_key": "blah" } ] } }`}, hcl: []string{` config_entries { bootstrap { kind = "service-defaults" name = "web" made_up_key = "blah" } }`}, expectedErr: "config_entries.bootstrap[0]: 1 error occurred:\n\t* invalid config key \"made_up_key\"\n\n", }) run(t, testCase{ desc: "ConfigEntry bootstrap proxy-defaults (snake-case)", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "config_entries": { "bootstrap": [ { "kind": "proxy-defaults", "name": "global", "config": { "bar": "abc", "moreconfig": { "moar": "config" } }, "mesh_gateway": { "mode": "remote" } } ] } }`}, hcl: []string{` config_entries { bootstrap { kind = "proxy-defaults" name = "global" config { "bar" = "abc" "moreconfig" { "moar" = "config" } } mesh_gateway { mode = "remote" } } }`}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.ConfigEntryBootstrap = []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, EnterpriseMeta: *defaultEntMeta, Config: map[string]interface{}{ "bar": "abc", "moreconfig": map[string]interface{}{ "moar": "config", }, }, MeshGateway: structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, }, }, } }, }) run(t, testCase{ desc: "ConfigEntry bootstrap proxy-defaults (camel-case)", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "config_entries": { "bootstrap": [ { "Kind": "proxy-defaults", "Name": "global", "Config": { "bar": "abc", "moreconfig": { "moar": "config" } }, "MeshGateway": { "Mode": "remote" } } ] } }`}, hcl: []string{` config_entries { bootstrap { Kind = "proxy-defaults" Name = "global" Config { "bar" = "abc" "moreconfig" { "moar" = "config" } } MeshGateway { Mode = "remote" } } }`}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.ConfigEntryBootstrap = []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, EnterpriseMeta: *defaultEntMeta, Config: map[string]interface{}{ "bar": "abc", "moreconfig": map[string]interface{}{ "moar": "config", }, }, MeshGateway: structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, }, }, } }, }) run(t, testCase{ desc: "ConfigEntry bootstrap service-defaults (snake-case)", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "config_entries": { "bootstrap": [ { "kind": "service-defaults", "name": "web", "meta" : { "foo": "bar", "gir": "zim" }, "protocol": "http", "external_sni": "abc-123", "mesh_gateway": { "mode": "remote" } } ] } }`}, hcl: []string{` config_entries { bootstrap { kind = "service-defaults" name = "web" meta { "foo" = "bar" "gir" = "zim" } protocol = "http" external_sni = "abc-123" mesh_gateway { mode = "remote" } } }`}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.ConfigEntryBootstrap = []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "web", Meta: map[string]string{ "foo": "bar", "gir": "zim", }, EnterpriseMeta: *defaultEntMeta, Protocol: "http", ExternalSNI: "abc-123", MeshGateway: structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, }, }, } }, }) run(t, testCase{ desc: "ConfigEntry bootstrap service-defaults (camel-case)", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "config_entries": { "bootstrap": [ { "Kind": "service-defaults", "Name": "web", "Meta" : { "foo": "bar", "gir": "zim" }, "Protocol": "http", "ExternalSNI": "abc-123", "MeshGateway": { "Mode": "remote" } } ] } }`}, hcl: []string{` config_entries { bootstrap { Kind = "service-defaults" Name = "web" Meta { "foo" = "bar" "gir" = "zim" } Protocol = "http" ExternalSNI = "abc-123" MeshGateway { Mode = "remote" } } }`}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.ConfigEntryBootstrap = []structs.ConfigEntry{ &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "web", Meta: map[string]string{ "foo": "bar", "gir": "zim", }, EnterpriseMeta: *defaultEntMeta, Protocol: "http", ExternalSNI: "abc-123", MeshGateway: structs.MeshGatewayConfig{ Mode: structs.MeshGatewayModeRemote, }, }, } }, }) run(t, testCase{ desc: "ConfigEntry bootstrap service-router (snake-case)", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "config_entries": { "bootstrap": [ { "kind": "service-router", "name": "main", "meta" : { "foo": "bar", "gir": "zim" }, "routes": [ { "match": { "http": { "path_exact": "/foo", "header": [ { "name": "debug1", "present": true }, { "name": "debug2", "present": true, "invert": true }, { "name": "debug3", "exact": "1" }, { "name": "debug4", "prefix": "aaa" }, { "name": "debug5", "suffix": "bbb" }, { "name": "debug6", "regex": "a.*z" } ] } }, "destination": { "service" : "carrot", "service_subset" : "kale", "namespace" : "leek", "prefix_rewrite" : "/alternate", "request_timeout" : "99s", "num_retries" : 12345, "retry_on_connect_failure": true, "retry_on_status_codes" : [401, 209] } }, { "match": { "http": { "path_prefix": "/foo", "methods": [ "GET", "DELETE" ], "query_param": [ { "name": "hack1", "present": true }, { "name": "hack2", "exact": "1" }, { "name": "hack3", "regex": "a.*z" } ] } } }, { "match": { "http": { "path_regex": "/foo" } } } ] } ] } }`}, hcl: []string{` config_entries { bootstrap { kind = "service-router" name = "main" meta { "foo" = "bar" "gir" = "zim" } routes = [ { match { http { path_exact = "/foo" header = [ { name = "debug1" present = true }, { name = "debug2" present = true invert = true }, { name = "debug3" exact = "1" }, { name = "debug4" prefix = "aaa" }, { name = "debug5" suffix = "bbb" }, { name = "debug6" regex = "a.*z" }, ] } } destination { service = "carrot" service_subset = "kale" namespace = "leek" prefix_rewrite = "/alternate" request_timeout = "99s" num_retries = 12345 retry_on_connect_failure = true retry_on_status_codes = [401, 209] } }, { match { http { path_prefix = "/foo" methods = [ "GET", "DELETE" ] query_param = [ { name = "hack1" present = true }, { name = "hack2" exact = "1" }, { name = "hack3" regex = "a.*z" }, ] } } }, { match { http { path_regex = "/foo" } } }, ] } }`}, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.ConfigEntryBootstrap = []structs.ConfigEntry{ &structs.ServiceRouterConfigEntry{ Kind: structs.ServiceRouter, Name: "main", Meta: map[string]string{ "foo": "bar", "gir": "zim", }, EnterpriseMeta: *defaultEntMeta, Routes: []structs.ServiceRoute{ { Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathExact: "/foo", Header: []structs.ServiceRouteHTTPMatchHeader{ { Name: "debug1", Present: true, }, { Name: "debug2", Present: true, Invert: true, }, { Name: "debug3", Exact: "1", }, { Name: "debug4", Prefix: "aaa", }, { Name: "debug5", Suffix: "bbb", }, { Name: "debug6", Regex: "a.*z", }, }, }, }, Destination: &structs.ServiceRouteDestination{ Service: "carrot", ServiceSubset: "kale", Namespace: "leek", PrefixRewrite: "/alternate", RequestTimeout: 99 * time.Second, NumRetries: 12345, RetryOnConnectFailure: true, RetryOnStatusCodes: []uint32{401, 209}, }, }, { Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathPrefix: "/foo", Methods: []string{"GET", "DELETE"}, QueryParam: []structs.ServiceRouteHTTPMatchQueryParam{ { Name: "hack1", Present: true, }, { Name: "hack2", Exact: "1", }, { Name: "hack3", Regex: "a.*z", }, }, }, }, }, { Match: &structs.ServiceRouteMatch{ HTTP: &structs.ServiceRouteHTTPMatch{ PathRegex: "/foo", }, }, }, }, }, } }, }) // TODO(rb): add in missing tests for ingress-gateway (snake + camel) // TODO(rb): add in missing tests for terminating-gateway (snake + camel) run(t, testCase{ desc: "ConfigEntry bootstrap service-intentions (snake-case)", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "config_entries": { "bootstrap": [ { "kind": "service-intentions", "name": "web", "meta" : { "foo": "bar", "gir": "zim" }, "sources": [ { "name": "foo", "action": "deny", "type": "consul", "description": "foo desc" }, { "name": "bar", "action": "allow", "description": "bar desc" }, { "name": "*", "action": "deny", "description": "wild desc" } ] } ] } }`, }, hcl: []string{` config_entries { bootstrap { kind = "service-intentions" name = "web" meta { "foo" = "bar" "gir" = "zim" } sources = [ { name = "foo" action = "deny" type = "consul" description = "foo desc" }, { name = "bar" action = "allow" description = "bar desc" } ] sources { name = "*" action = "deny" description = "wild desc" } } } `, }, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.ConfigEntryBootstrap = []structs.ConfigEntry{ &structs.ServiceIntentionsConfigEntry{ Kind: "service-intentions", Name: "web", Meta: map[string]string{ "foo": "bar", "gir": "zim", }, EnterpriseMeta: *defaultEntMeta, Sources: []*structs.SourceIntention{ { Name: "foo", Action: "deny", Type: "consul", Description: "foo desc", Precedence: 9, EnterpriseMeta: *defaultEntMeta, }, { Name: "bar", Action: "allow", Type: "consul", Description: "bar desc", Precedence: 9, EnterpriseMeta: *defaultEntMeta, }, { Name: "*", Action: "deny", Type: "consul", Description: "wild desc", Precedence: 8, EnterpriseMeta: *defaultEntMeta, }, }, }, } }, }) run(t, testCase{ desc: "ConfigEntry bootstrap service-intentions wildcard destination (snake-case)", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "config_entries": { "bootstrap": [ { "kind": "service-intentions", "name": "*", "sources": [ { "name": "foo", "action": "deny", "precedence": 6 } ] } ] } }`, }, hcl: []string{` config_entries { bootstrap { kind = "service-intentions" name = "*" sources { name = "foo" action = "deny" # should be parsed, but we'll ignore it later precedence = 6 } } } `, }, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir rt.ConfigEntryBootstrap = []structs.ConfigEntry{ &structs.ServiceIntentionsConfigEntry{ Kind: "service-intentions", Name: "*", EnterpriseMeta: *defaultEntMeta, Sources: []*structs.SourceIntention{ { Name: "foo", Action: "deny", Type: "consul", Precedence: 6, EnterpriseMeta: *defaultEntMeta, }, }, }, } }, }) /////////////////////////////////// // Defaults sanity checks run(t, testCase{ desc: "default limits", args: []string{ `-data-dir=` + dataDir, }, expected: func(rt *RuntimeConfig) { rt.DataDir = dataDir // Note that in the happy case this test will pass even if you comment // out all the stuff below since rt is also initialized from the // defaults. But it's still valuable as it will fail as soon as the // defaults are changed from these values forcing that change to be // intentional. rt.RPCHandshakeTimeout = 5 * time.Second rt.HTTPSHandshakeTimeout = 5 * time.Second rt.HTTPMaxConnsPerClient = 200 rt.RPCMaxConnsPerClient = 100 }, }) /////////////////////////////////// // Auto Config related tests run(t, testCase{ desc: "auto config and auto encrypt error", args: []string{ `-data-dir=` + dataDir, }, hcl: []string{` auto_config { enabled = true intro_token = "blah" server_addresses = ["198.18.0.1"] } auto_encrypt { tls = true } verify_outgoing = true `}, json: []string{`{ "auto_config": { "enabled": true, "intro_token": "blah", "server_addresses": ["198.18.0.1"] }, "auto_encrypt": { "tls": true }, "verify_outgoing": true }`}, expectedErr: "both auto_encrypt.tls and auto_config.enabled cannot be set to true.", }) run(t, testCase{ desc: "auto config not allowed for servers", args: []string{ `-data-dir=` + dataDir, }, hcl: []string{` server = true auto_config { enabled = true intro_token = "blah" server_addresses = ["198.18.0.1"] } verify_outgoing = true `}, json: []string{` { "server": true, "auto_config": { "enabled": true, "intro_token": "blah", "server_addresses": ["198.18.0.1"] }, "verify_outgoing": true }`}, expectedErr: "auto_config.enabled cannot be set to true for server agents", }) run(t, testCase{ desc: "auto config tls not enabled", args: []string{ `-data-dir=` + dataDir, }, hcl: []string{` auto_config { enabled = true server_addresses = ["198.18.0.1"] intro_token = "foo" } `}, json: []string{` { "auto_config": { "enabled": true, "server_addresses": ["198.18.0.1"], "intro_token": "foo" } }`}, expectedErr: "auto_config.enabled cannot be set without configuring TLS for server communications", }) run(t, testCase{ desc: "auto config server tls not enabled", args: []string{ `-data-dir=` + dataDir, }, hcl: []string{` server = true auto_config { authorization { enabled = true } } `}, json: []string{` { "server": true, "auto_config": { "authorization": { "enabled": true } } }`}, expectedErr: "auto_config.authorization.enabled cannot be set without providing a TLS certificate for the server", }) run(t, testCase{ desc: "auto config no intro token", args: []string{ `-data-dir=` + dataDir, }, hcl: []string{` auto_config { enabled = true server_addresses = ["198.18.0.1"] } verify_outgoing = true `}, json: []string{` { "auto_config": { "enabled": true, "server_addresses": ["198.18.0.1"] }, "verify_outgoing": true }`}, expectedErr: "One of auto_config.intro_token, auto_config.intro_token_file or the CONSUL_INTRO_TOKEN environment variable must be set to enable auto_config", }) run(t, testCase{ desc: "auto config no server addresses", args: []string{ `-data-dir=` + dataDir, }, hcl: []string{` auto_config { enabled = true intro_token = "blah" } verify_outgoing = true `}, json: []string{` { "auto_config": { "enabled": true, "intro_token": "blah" }, "verify_outgoing": true }`}, expectedErr: "auto_config.enabled is set without providing a list of addresses", }) run(t, testCase{ desc: "auto config client", args: []string{ `-data-dir=` + dataDir, }, hcl: []string{` auto_config { enabled = true intro_token = "blah" intro_token_file = "blah" server_addresses = ["198.18.0.1"] dns_sans = ["foo"] ip_sans = ["invalid", "127.0.0.1"] } verify_outgoing = true `}, json: []string{` { "auto_config": { "enabled": true, "intro_token": "blah", "intro_token_file": "blah", "server_addresses": ["198.18.0.1"], "dns_sans": ["foo"], "ip_sans": ["invalid", "127.0.0.1"] }, "verify_outgoing": true }`}, expectedWarnings: []string{ "Cannot parse ip \"invalid\" from auto_config.ip_sans", "Both an intro token and intro token file are set. The intro token will be used instead of the file", }, expected: func(rt *RuntimeConfig) { rt.ConnectEnabled = true rt.AutoConfig.Enabled = true rt.AutoConfig.IntroToken = "blah" rt.AutoConfig.IntroTokenFile = "blah" rt.AutoConfig.ServerAddresses = []string{"198.18.0.1"} rt.AutoConfig.DNSSANs = []string{"foo"} rt.AutoConfig.IPSANs = []net.IP{net.IPv4(127, 0, 0, 1)} rt.DataDir = dataDir rt.VerifyOutgoing = true }, }) run(t, testCase{ desc: "auto config authorizer client not allowed", args: []string{ `-data-dir=` + dataDir, }, hcl: []string{` auto_config { authorization { enabled = true } } `}, json: []string{` { "auto_config": { "authorization": { "enabled": true } } }`}, expectedErr: "auto_config.authorization.enabled cannot be set to true for client agents", }) run(t, testCase{ desc: "auto config authorizer invalid config", args: []string{ `-data-dir=` + dataDir, `-server`, }, hcl: []string{` auto_config { authorization { enabled = true } } cert_file = "foo" `}, json: []string{` { "auto_config": { "authorization": { "enabled": true } }, "cert_file": "foo" }`}, expectedErr: `auto_config.authorization.static has invalid configuration: exactly one of 'JWTValidationPubKeys', 'JWKSURL', or 'OIDCDiscoveryURL' must be set for type "jwt"`, }) run(t, testCase{ desc: "auto config authorizer invalid config 2", args: []string{ `-data-dir=` + dataDir, `-server`, }, hcl: []string{` auto_config { authorization { enabled = true static { jwks_url = "https://fake.uri.local" oidc_discovery_url = "https://fake.uri.local" } } } cert_file = "foo" `}, json: []string{` { "auto_config": { "authorization": { "enabled": true, "static": { "jwks_url": "https://fake.uri.local", "oidc_discovery_url": "https://fake.uri.local" } } }, "cert_file": "foo" }`}, expectedErr: `auto_config.authorization.static has invalid configuration: exactly one of 'JWTValidationPubKeys', 'JWKSURL', or 'OIDCDiscoveryURL' must be set for type "jwt"`, }) run(t, testCase{ desc: "auto config authorizer require token replication in secondary", args: []string{ `-data-dir=` + dataDir, `-server`, }, hcl: []string{` primary_datacenter = "otherdc" acl { enabled = true } auto_config { authorization { enabled = true static { jwks_url = "https://fake.uri.local" oidc_discovery_url = "https://fake.uri.local" } } } cert_file = "foo" `}, json: []string{` { "primary_datacenter": "otherdc", "acl": { "enabled": true }, "auto_config": { "authorization": { "enabled": true, "static": { "jwks_url": "https://fake.uri.local", "oidc_discovery_url": "https://fake.uri.local" } } }, "cert_file": "foo" }`}, expectedErr: `Enabling auto-config authorization (auto_config.authorization.enabled) in non primary datacenters with ACLs enabled (acl.enabled) requires also enabling ACL token replication (acl.enable_token_replication)`, }) run(t, testCase{ desc: "auto config authorizer invalid claim assertion", args: []string{ `-data-dir=` + dataDir, `-server`, }, hcl: []string{` auto_config { authorization { enabled = true static { jwt_validation_pub_keys = ["-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"] claim_assertions = [ "values.node == ${node}" ] } } } cert_file = "foo" `}, json: []string{` { "auto_config": { "authorization": { "enabled": true, "static": { "jwt_validation_pub_keys": ["-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"], "claim_assertions": [ "values.node == ${node}" ] } } }, "cert_file": "foo" }`}, expectedErr: `auto_config.authorization.static.claim_assertion "values.node == ${node}" is invalid: Selector "values" is not valid`, }) run(t, testCase{ desc: "auto config authorizer ok", args: []string{ `-data-dir=` + dataDir, `-server`, }, hcl: []string{` auto_config { authorization { enabled = true static { jwt_validation_pub_keys = ["-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"] claim_assertions = [ "value.node == ${node}" ] claim_mappings = { node = "node" } } } } cert_file = "foo" `}, json: []string{` { "auto_config": { "authorization": { "enabled": true, "static": { "jwt_validation_pub_keys": ["-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"], "claim_assertions": [ "value.node == ${node}" ], "claim_mappings": { "node": "node" } } } }, "cert_file": "foo" }`}, expected: func(rt *RuntimeConfig) { rt.AutoConfig.Authorizer.Enabled = true rt.AutoConfig.Authorizer.AuthMethod.Config["ClaimMappings"] = map[string]string{ "node": "node", } rt.AutoConfig.Authorizer.AuthMethod.Config["JWTValidationPubKeys"] = []string{"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"} rt.AutoConfig.Authorizer.ClaimAssertions = []string{"value.node == ${node}"} rt.DataDir = dataDir rt.LeaveOnTerm = false rt.ServerMode = true rt.SkipLeaveOnInt = true rt.CertFile = "foo" }, }) // UI Config tests run(t, testCase{ desc: "ui config deprecated", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ui": true, "ui_content_path": "/bar" }`}, hcl: []string{` ui = true ui_content_path = "/bar" `}, expectedWarnings: []string{ `The 'ui' field is deprecated. Use the 'ui_config.enabled' field instead.`, `The 'ui_content_path' field is deprecated. Use the 'ui_config.content_path' field instead.`, }, expected: func(rt *RuntimeConfig) { // Should still work! rt.UIConfig.Enabled = true rt.UIConfig.ContentPath = "/bar/" rt.DataDir = dataDir }, }) run(t, testCase{ desc: "ui-dir config deprecated", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ui_dir": "/bar" }`}, hcl: []string{` ui_dir = "/bar" `}, expectedWarnings: []string{ `The 'ui_dir' field is deprecated. Use the 'ui_config.dir' field instead.`, }, expected: func(rt *RuntimeConfig) { // Should still work! rt.UIConfig.Dir = "/bar" rt.DataDir = dataDir }, }) run(t, testCase{ desc: "metrics_provider constraint", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ui_config": { "metrics_provider": "((((lisp 4 life))))" } }`}, hcl: []string{` ui_config { metrics_provider = "((((lisp 4 life))))" } `}, expectedErr: `ui_config.metrics_provider can only contain lowercase alphanumeric, - or _ characters.`, }) run(t, testCase{ desc: "metrics_provider_options_json invalid JSON", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ui_config": { "metrics_provider_options_json": "not valid JSON" } }`}, hcl: []string{` ui_config { metrics_provider_options_json = "not valid JSON" } `}, expectedErr: `ui_config.metrics_provider_options_json must be empty or a string containing a valid JSON object.`, }) run(t, testCase{ desc: "metrics_provider_options_json not an object", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ui_config": { "metrics_provider_options_json": "1.0" } }`}, hcl: []string{` ui_config { metrics_provider_options_json = "1.0" } `}, expectedErr: `ui_config.metrics_provider_options_json must be empty or a string containing a valid JSON object.`, }) run(t, testCase{ desc: "metrics_proxy.base_url valid", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ui_config": { "metrics_proxy": { "base_url": "___" } } }`}, hcl: []string{` ui_config { metrics_proxy { base_url = "___" } } `}, expectedErr: `ui_config.metrics_proxy.base_url must be a valid http or https URL.`, }) run(t, testCase{ desc: "metrics_proxy.path_allowlist invalid (empty)", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ui_config": { "metrics_proxy": { "path_allowlist": ["", "/foo"] } } }`}, hcl: []string{` ui_config { metrics_proxy { path_allowlist = ["", "/foo"] } } `}, expectedErr: `ui_config.metrics_proxy.path_allowlist: path "" is not an absolute path`, }) run(t, testCase{ desc: "metrics_proxy.path_allowlist invalid (relative)", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ui_config": { "metrics_proxy": { "path_allowlist": ["bar/baz", "/foo"] } } }`}, hcl: []string{` ui_config { metrics_proxy { path_allowlist = ["bar/baz", "/foo"] } } `}, expectedErr: `ui_config.metrics_proxy.path_allowlist: path "bar/baz" is not an absolute path`, }) run(t, testCase{ desc: "metrics_proxy.path_allowlist invalid (weird)", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ui_config": { "metrics_proxy": { "path_allowlist": ["://bar/baz", "/foo"] } } }`}, hcl: []string{` ui_config { metrics_proxy { path_allowlist = ["://bar/baz", "/foo"] } } `}, expectedErr: `ui_config.metrics_proxy.path_allowlist: path "://bar/baz" is not an absolute path`, }) run(t, testCase{ desc: "metrics_proxy.path_allowlist invalid (fragment)", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ui_config": { "metrics_proxy": { "path_allowlist": ["/bar/baz#stuff", "/foo"] } } }`}, hcl: []string{` ui_config { metrics_proxy { path_allowlist = ["/bar/baz#stuff", "/foo"] } } `}, expectedErr: `ui_config.metrics_proxy.path_allowlist: path "/bar/baz#stuff" is not an absolute path`, }) run(t, testCase{ desc: "metrics_proxy.path_allowlist invalid (querystring)", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ui_config": { "metrics_proxy": { "path_allowlist": ["/bar/baz?stu=ff", "/foo"] } } }`}, hcl: []string{` ui_config { metrics_proxy { path_allowlist = ["/bar/baz?stu=ff", "/foo"] } } `}, expectedErr: `ui_config.metrics_proxy.path_allowlist: path "/bar/baz?stu=ff" is not an absolute path`, }) run(t, testCase{ desc: "metrics_proxy.path_allowlist invalid (encoded slash)", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ui_config": { "metrics_proxy": { "path_allowlist": ["/bar%2fbaz", "/foo"] } } }`}, hcl: []string{` ui_config { metrics_proxy { path_allowlist = ["/bar%2fbaz", "/foo"] } } `}, expectedErr: `ui_config.metrics_proxy.path_allowlist: path "/bar%2fbaz" is not an absolute path`, }) run(t, testCase{ desc: "metrics_proxy.path_allowlist ok", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ui_config": { "metrics_proxy": { "path_allowlist": ["/bar/baz", "/foo"] } } }`}, hcl: []string{` ui_config { metrics_proxy { path_allowlist = ["/bar/baz", "/foo"] } } `}, expected: func(rt *RuntimeConfig) { rt.UIConfig.MetricsProxy.PathAllowlist = []string{"/bar/baz", "/foo"} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "metrics_proxy.path_allowlist defaulted for prometheus", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ui_config": { "metrics_provider": "prometheus" } }`}, hcl: []string{` ui_config { metrics_provider = "prometheus" } `}, expected: func(rt *RuntimeConfig) { rt.UIConfig.MetricsProvider = "prometheus" rt.UIConfig.MetricsProxy.PathAllowlist = []string{ "/api/v1/query", "/api/v1/query_range", } rt.DataDir = dataDir }, }) run(t, testCase{ desc: "metrics_proxy.path_allowlist not overridden with defaults for prometheus", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ui_config": { "metrics_provider": "prometheus", "metrics_proxy": { "path_allowlist": ["/bar/baz", "/foo"] } } }`}, hcl: []string{` ui_config { metrics_provider = "prometheus" metrics_proxy { path_allowlist = ["/bar/baz", "/foo"] } } `}, expected: func(rt *RuntimeConfig) { rt.UIConfig.MetricsProvider = "prometheus" rt.UIConfig.MetricsProxy.PathAllowlist = []string{"/bar/baz", "/foo"} rt.DataDir = dataDir }, }) run(t, testCase{ desc: "metrics_proxy.base_url http(s)", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ui_config": { "metrics_proxy": { "base_url": "localhost:1234" } } }`}, hcl: []string{` ui_config { metrics_proxy { base_url = "localhost:1234" } } `}, expectedErr: `ui_config.metrics_proxy.base_url must be a valid http or https URL.`, }) run(t, testCase{ desc: "dashboard_url_templates key format", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ui_config": { "dashboard_url_templates": { "(*&ASDOUISD)": "localhost:1234" } } }`}, hcl: []string{` ui_config { dashboard_url_templates { "(*&ASDOUISD)" = "localhost:1234" } } `}, expectedErr: `ui_config.dashboard_url_templates key names can only contain lowercase alphanumeric, - or _ characters.`, }) run(t, testCase{ desc: "dashboard_url_templates value format", args: []string{`-data-dir=` + dataDir}, json: []string{`{ "ui_config": { "dashboard_url_templates": { "services": "localhost:1234" } } }`}, hcl: []string{` ui_config { dashboard_url_templates { services = "localhost:1234" } } `}, expectedErr: `ui_config.dashboard_url_templates values must be a valid http or https URL.`, }) // Per node reconnect timeout test run(t, testCase{ desc: "server and advertised reconnect timeout error", args: []string{ `-data-dir=` + dataDir, `-server`, }, hcl: []string{` advertise_reconnect_timeout = "5s" `}, json: []string{` { "advertise_reconnect_timeout": "5s" }`}, expectedErr: "advertise_reconnect_timeout can only be used on a client", }) } func (tc testCase) run(format string, dataDir string) func(t *testing.T) { return func(t *testing.T) { // clean data dir before every test os.RemoveAll(dataDir) os.MkdirAll(dataDir, 0755) if tc.setup != nil { tc.setup() } opts := tc.opts fs := flag.NewFlagSet("", flag.ContinueOnError) AddFlags(fs, &opts) require.NoError(t, fs.Parse(tc.args)) require.Len(t, fs.Args(), 0) // Then create a builder with the flags. patchLoadOptsShims(&opts) b, err := newBuilder(opts) require.NoError(t, err) // read the source fragments for i, data := range tc.source(format) { b.Sources = append(b.Sources, FileSource{ Name: fmt.Sprintf("src-%d.%s", i, format), Format: format, Data: data, }) } // build/merge the config fragments actual, err := b.BuildAndValidate() switch { case err == nil && tc.expectedErr != "": t.Fatalf("got nil want error to contain %q", tc.expectedErr) case err != nil && tc.expectedErr == "": t.Fatalf("got error %s want nil", err) case err != nil && tc.expectedErr != "" && !strings.Contains(err.Error(), tc.expectedErr): t.Fatalf("error %q does not contain %q", err.Error(), tc.expectedErr) } if tc.expectedErr != "" { return } require.Equal(t, tc.expectedWarnings, b.Warnings, "warnings") // build a default configuration, then patch the fields we expect to change // and compare it with the generated configuration. Since the expected // runtime config has been validated we do not need to validate it again. expectedOpts := LoadOpts{} patchLoadOptsShims(&expectedOpts) x, err := newBuilder(expectedOpts) require.NoError(t, err) expected, err := x.Build() require.NoError(t, err) if tc.expected != nil { tc.expected(&expected) } // both DataDir fields should always be the same, so test for the // invariant, and than updated the expected, so that every test // case does not need to set this field. require.Equal(t, actual.DataDir, actual.ACLTokens.DataDir) expected.ACLTokens.DataDir = actual.ACLTokens.DataDir assertDeepEqual(t, expected, actual, cmpopts.EquateEmpty()) } } func runCase(t *testing.T, name string, fn func(t *testing.T)) { t.Helper() t.Run(name, func(t *testing.T) { t.Helper() t.Log("case:", name) fn(t) }) } func assertDeepEqual(t *testing.T, x, y interface{}, opts ...cmp.Option) { t.Helper() if diff := cmp.Diff(x, y, opts...); diff != "" { t.Fatalf("assertion failed: values are not equal\n--- expected\n+++ actual\n%v", diff) } } func TestLoad_InvalidConfigFormat(t *testing.T) { _, err := Load(LoadOpts{ConfigFormat: "yaml"}) require.Error(t, err) require.Contains(t, err.Error(), "-config-format must be either 'hcl' or 'json'") } // TestFullConfig tests the conversion from a fully populated JSON or // HCL config file to a RuntimeConfig structure. All fields must be set // to a unique non-zero value. func TestLoad_FullConfig(t *testing.T) { dataDir := testutil.TempDir(t, "consul") cidr := func(s string) *net.IPNet { _, n, _ := net.ParseCIDR(s) return n } defaultEntMeta := structs.DefaultEnterpriseMeta() expected := &RuntimeConfig{ // non-user configurable values ACLDisabledTTL: 120 * time.Second, AEInterval: time.Minute, CheckDeregisterIntervalMin: time.Minute, CheckReapInterval: 30 * time.Second, SegmentLimit: 64, SegmentNameLimit: 64, SyncCoordinateIntervalMin: 15 * time.Second, SyncCoordinateRateTarget: 64, Revision: "JNtPSav3", Version: "R909Hblt", VersionPrerelease: "ZT1JOQLn", // consul configuration ConsulCoordinateUpdateBatchSize: 128, ConsulCoordinateUpdateMaxBatches: 5, ConsulCoordinateUpdatePeriod: 5 * time.Second, ConsulRaftElectionTimeout: 5 * time.Second, ConsulRaftHeartbeatTimeout: 5 * time.Second, ConsulRaftLeaderLeaseTimeout: 2500 * time.Millisecond, GossipLANGossipInterval: 25252 * time.Second, GossipLANGossipNodes: 6, GossipLANProbeInterval: 101 * time.Millisecond, GossipLANProbeTimeout: 102 * time.Millisecond, GossipLANSuspicionMult: 1235, GossipLANRetransmitMult: 1234, GossipWANGossipInterval: 6966 * time.Second, GossipWANGossipNodes: 2, GossipWANProbeInterval: 103 * time.Millisecond, GossipWANProbeTimeout: 104 * time.Millisecond, GossipWANSuspicionMult: 16385, GossipWANRetransmitMult: 16384, ConsulServerHealthInterval: 2 * time.Second, // user configurable values ACLTokens: token.Config{ EnablePersistence: true, DataDir: dataDir, ACLDefaultToken: "418fdff1", ACLAgentToken: "bed2377c", ACLAgentMasterToken: "64fd0e08", ACLReplicationToken: "5795983a", }, ACLsEnabled: true, ACLDatacenter: "ejtmd43d", ACLDefaultPolicy: "72c2e7a0", ACLDownPolicy: "03eb2aee", ACLEnableKeyListPolicy: true, ACLMasterToken: "8a19ac27", ACLTokenTTL: 3321 * time.Second, ACLPolicyTTL: 1123 * time.Second, ACLRoleTTL: 9876 * time.Second, ACLTokenReplication: true, AdvertiseAddrLAN: ipAddr("17.99.29.16"), AdvertiseAddrWAN: ipAddr("78.63.37.19"), AdvertiseReconnectTimeout: 0 * time.Second, AutopilotCleanupDeadServers: true, AutopilotDisableUpgradeMigration: true, AutopilotLastContactThreshold: 12705 * time.Second, AutopilotMaxTrailingLogs: 17849, AutopilotMinQuorum: 3, AutopilotRedundancyZoneTag: "3IsufDJf", AutopilotServerStabilizationTime: 23057 * time.Second, AutopilotUpgradeVersionTag: "W9pDwFAL", BindAddr: ipAddr("16.99.34.17"), BootstrapExpect: 53, Cache: cache.Options{ EntryFetchMaxBurst: 42, EntryFetchRate: 0.334, }, CAFile: "erA7T0PM", CAPath: "mQEN1Mfp", CertFile: "7s4QAzDk", CheckOutputMaxSize: checks.DefaultBufSize, Checks: []*structs.CheckDefinition{ { ID: "uAjE6m9Z", Name: "QsZRGpYr", Notes: "VJ7Sk4BY", ServiceID: "lSulPcyz", Token: "toO59sh8", Status: "9RlWsXMV", ScriptArgs: []string{"4BAJttck", "4D2NPtTQ"}, HTTP: "dohLcyQ2", Header: map[string][]string{ "ZBfTin3L": {"1sDbEqYG", "lJGASsWK"}, "Ui0nU99X": {"LMccm3Qe", "k5H5RggQ"}, }, Method: "aldrIQ4l", Body: "wSjTy7dg", TCP: "RJQND605", Interval: 22164 * time.Second, OutputMaxSize: checks.DefaultBufSize, DockerContainerID: "ipgdFtjd", Shell: "qAeOYy0M", TLSSkipVerify: true, Timeout: 1813 * time.Second, TTL: 21743 * time.Second, DeregisterCriticalServiceAfter: 14232 * time.Second, }, { ID: "Cqq95BhP", Name: "3qXpkS0i", Notes: "sb5qLTex", ServiceID: "CmUUcRna", Token: "a3nQzHuy", Status: "irj26nf3", ScriptArgs: []string{"9s526ogY", "gSlOHj1w"}, HTTP: "yzhgsQ7Y", Header: map[string][]string{ "zcqwA8dO": {"qb1zx0DL", "sXCxPFsD"}, "qxvdnSE9": {"6wBPUYdF", "YYh8wtSZ"}, }, Method: "gLrztrNw", Body: "0jkKgGUC", OutputMaxSize: checks.DefaultBufSize, TCP: "4jG5casb", Interval: 28767 * time.Second, DockerContainerID: "THW6u7rL", Shell: "C1Zt3Zwh", TLSSkipVerify: true, Timeout: 18506 * time.Second, TTL: 31006 * time.Second, DeregisterCriticalServiceAfter: 2366 * time.Second, }, { ID: "fZaCAXww", Name: "OOM2eo0f", Notes: "zXzXI9Gt", ServiceID: "L8G0QNmR", Token: "oo4BCTgJ", Status: "qLykAl5u", ScriptArgs: []string{"f3BemRjy", "e5zgpef7"}, HTTP: "29B93haH", Header: map[string][]string{ "hBq0zn1q": {"2a9o9ZKP", "vKwA5lR6"}, "f3r6xFtM": {"RyuIdDWv", "QbxEcIUM"}, }, Method: "Dou0nGT5", Body: "5PBQd2OT", OutputMaxSize: checks.DefaultBufSize, TCP: "JY6fTTcw", Interval: 18714 * time.Second, DockerContainerID: "qF66POS9", Shell: "sOnDy228", TLSSkipVerify: true, Timeout: 5954 * time.Second, TTL: 30044 * time.Second, DeregisterCriticalServiceAfter: 13209 * time.Second, }, }, CheckUpdateInterval: 16507 * time.Second, ClientAddrs: []*net.IPAddr{ipAddr("93.83.18.19")}, ConfigEntryBootstrap: []structs.ConfigEntry{ &structs.ProxyConfigEntry{ Kind: structs.ProxyDefaults, Name: structs.ProxyConfigGlobal, EnterpriseMeta: *defaultEntMeta, Config: map[string]interface{}{ "foo": "bar", // has to be a float due to being a map[string]interface "bar": float64(1), }, }, }, AutoEncryptTLS: false, AutoEncryptDNSSAN: []string{"a.com", "b.com"}, AutoEncryptIPSAN: []net.IP{net.ParseIP("192.168.4.139"), net.ParseIP("192.168.4.140")}, AutoEncryptAllowTLS: true, AutoConfig: AutoConfig{ Enabled: false, IntroToken: "OpBPGRwt", IntroTokenFile: "gFvAXwI8", DNSSANs: []string{"6zdaWg9J"}, IPSANs: []net.IP{net.IPv4(198, 18, 99, 99)}, ServerAddresses: []string{"198.18.100.1"}, Authorizer: AutoConfigAuthorizer{ Enabled: true, AllowReuse: true, ClaimAssertions: []string{"value.node == \"${node}\""}, AuthMethod: structs.ACLAuthMethod{ Name: "Auto Config Authorizer", Type: "jwt", EnterpriseMeta: *structs.DefaultEnterpriseMeta(), Config: map[string]interface{}{ "JWTValidationPubKeys": []string{"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERVchfCZng4mmdvQz1+sJHRN40snC\nYt8NjYOnbnScEXMkyoUmASr88gb7jaVAVt3RYASAbgBjB2Z+EUizWkx5Tg==\n-----END PUBLIC KEY-----"}, "ClaimMappings": map[string]string{ "node": "node", }, "BoundIssuer": "consul", "BoundAudiences": []string{"consul-cluster-1"}, "ListClaimMappings": map[string]string{ "foo": "bar", }, "OIDCDiscoveryURL": "", "OIDCDiscoveryCACert": "", "JWKSURL": "", "JWKSCACert": "", "ExpirationLeeway": 0 * time.Second, "NotBeforeLeeway": 0 * time.Second, "ClockSkewLeeway": 0 * time.Second, "JWTSupportedAlgs": []string(nil), }, }, }, }, ConnectEnabled: true, ConnectSidecarMinPort: 8888, ConnectSidecarMaxPort: 9999, ExposeMinPort: 1111, ExposeMaxPort: 2222, ConnectCAProvider: "consul", ConnectCAConfig: map[string]interface{}{ "RotationPeriod": "90h", "IntermediateCertTTL": "8760h", "LeafCertTTL": "1h", "CSRMaxPerSecond": float64(100), "CSRMaxConcurrent": float64(2), }, ConnectMeshGatewayWANFederationEnabled: false, DNSAddrs: []net.Addr{tcpAddr("93.95.95.81:7001"), udpAddr("93.95.95.81:7001")}, DNSARecordLimit: 29907, DNSAllowStale: true, DNSDisableCompression: true, DNSDomain: "7W1xXSqd", DNSAltDomain: "1789hsd", DNSEnableTruncate: true, DNSMaxStale: 29685 * time.Second, DNSNodeTTL: 7084 * time.Second, DNSOnlyPassing: true, DNSPort: 7001, DNSRecursorTimeout: 4427 * time.Second, DNSRecursors: []string{"63.38.39.58", "92.49.18.18"}, DNSSOA: RuntimeSOAConfig{Refresh: 3600, Retry: 600, Expire: 86400, Minttl: 0}, DNSServiceTTL: map[string]time.Duration{"*": 32030 * time.Second}, DNSUDPAnswerLimit: 29909, DNSNodeMetaTXT: true, DNSUseCache: true, DNSCacheMaxAge: 5 * time.Minute, DataDir: dataDir, Datacenter: "rzo029wg", DefaultQueryTime: 16743 * time.Second, DisableAnonymousSignature: true, DisableCoordinates: true, DisableHostNodeID: true, DisableHTTPUnprintableCharFilter: true, DisableKeyringFile: true, DisableRemoteExec: true, DisableUpdateCheck: true, DiscardCheckOutput: true, DiscoveryMaxStale: 5 * time.Second, EnableAgentTLSForChecks: true, EnableCentralServiceConfig: false, EnableDebug: true, EnableRemoteScriptChecks: true, EnableLocalScriptChecks: true, EncryptKey: "A4wELWqH", EncryptVerifyIncoming: true, EncryptVerifyOutgoing: true, GRPCPort: 4881, GRPCAddrs: []net.Addr{tcpAddr("32.31.61.91:4881")}, HTTPAddrs: []net.Addr{tcpAddr("83.39.91.39:7999")}, HTTPBlockEndpoints: []string{"RBvAFcGD", "fWOWFznh"}, AllowWriteHTTPFrom: []*net.IPNet{cidr("127.0.0.0/8"), cidr("22.33.44.55/32"), cidr("0.0.0.0/0")}, HTTPPort: 7999, HTTPResponseHeaders: map[string]string{"M6TKa9NP": "xjuxjOzQ", "JRCrHZed": "rl0mTx81"}, HTTPSAddrs: []net.Addr{tcpAddr("95.17.17.19:15127")}, HTTPMaxConnsPerClient: 100, HTTPMaxHeaderBytes: 10, HTTPSHandshakeTimeout: 2391 * time.Millisecond, HTTPSPort: 15127, HTTPUseCache: false, KeyFile: "IEkkwgIA", KVMaxValueSize: 1234567800000000, LeaveDrainTime: 8265 * time.Second, LeaveOnTerm: true, Logging: logging.Config{ LogLevel: "k1zo9Spt", LogJSON: true, EnableSyslog: true, SyslogFacility: "hHv79Uia", }, MaxQueryTime: 18237 * time.Second, NodeID: types.NodeID("AsUIlw99"), NodeMeta: map[string]string{"5mgGQMBk": "mJLtVMSG", "A7ynFMJB": "0Nx6RGab"}, NodeName: "otlLxGaI", ReadReplica: true, PidFile: "43xN80Km", PrimaryDatacenter: "ejtmd43d", PrimaryGateways: []string{"aej8eeZo", "roh2KahS"}, PrimaryGatewaysInterval: 18866 * time.Second, RPCAdvertiseAddr: tcpAddr("17.99.29.16:3757"), RPCBindAddr: tcpAddr("16.99.34.17:3757"), RPCHandshakeTimeout: 1932 * time.Millisecond, RPCHoldTimeout: 15707 * time.Second, RPCProtocol: 30793, RPCRateLimit: 12029.43, RPCMaxBurst: 44848, RPCMaxConnsPerClient: 2954, RaftProtocol: 3, RaftSnapshotThreshold: 16384, RaftSnapshotInterval: 30 * time.Second, RaftTrailingLogs: 83749, ReconnectTimeoutLAN: 23739 * time.Second, ReconnectTimeoutWAN: 26694 * time.Second, RejoinAfterLeave: true, RetryJoinIntervalLAN: 8067 * time.Second, RetryJoinIntervalWAN: 28866 * time.Second, RetryJoinLAN: []string{"pbsSFY7U", "l0qLtWij"}, RetryJoinMaxAttemptsLAN: 913, RetryJoinMaxAttemptsWAN: 23160, RetryJoinWAN: []string{"PFsR02Ye", "rJdQIhER"}, RPCConfig: consul.RPCConfig{EnableStreaming: true}, SerfPortLAN: 8301, SerfPortWAN: 8302, ServerMode: true, ServerName: "Oerr9n1G", ServerPort: 3757, Services: []*structs.ServiceDefinition{ { ID: "wI1dzxS4", Name: "7IszXMQ1", Tags: []string{"0Zwg8l6v", "zebELdN5"}, Address: "9RhqPSPB", Token: "myjKJkWH", Port: 72219, Weights: &structs.Weights{ Passing: 1, Warning: 1, }, EnableTagOverride: true, Checks: []*structs.CheckType{ { CheckID: "qmfeO5if", Name: "atDGP7n5", Status: "pDQKEhWL", Notes: "Yt8EDLev", ScriptArgs: []string{"81EDZLPa", "bPY5X8xd"}, HTTP: "qzHYvmJO", Header: map[string][]string{ "UkpmZ3a3": {"2dfzXuxZ"}, "cVFpko4u": {"gGqdEB6k", "9LsRo22u"}, }, Method: "X5DrovFc", Body: "WeikigLh", OutputMaxSize: checks.DefaultBufSize, TCP: "ICbxkpSF", Interval: 24392 * time.Second, DockerContainerID: "ZKXr68Yb", Shell: "CEfzx0Fo", TLSSkipVerify: true, Timeout: 38333 * time.Second, TTL: 57201 * time.Second, DeregisterCriticalServiceAfter: 44214 * time.Second, }, }, // Note that although this SidecarService is only syntax sugar for // registering another service, that has to happen in the agent code so // it can make intelligent decisions about automatic port assignments // etc. So we expect config just to pass it through verbatim. Connect: &structs.ServiceConnect{ SidecarService: &structs.ServiceDefinition{ Weights: &structs.Weights{ Passing: 1, Warning: 1, }, }, }, }, { ID: "MRHVMZuD", Name: "6L6BVfgH", Tags: []string{"7Ale4y6o", "PMBW08hy"}, Address: "R6H6g8h0", Token: "ZgY8gjMI", Port: 38292, Weights: &structs.Weights{ Passing: 1979, Warning: 6, }, EnableTagOverride: true, Checks: structs.CheckTypes{ &structs.CheckType{ CheckID: "GTti9hCo", Name: "9OOS93ne", Notes: "CQy86DH0", Status: "P0SWDvrk", ScriptArgs: []string{"EXvkYIuG", "BATOyt6h"}, HTTP: "u97ByEiW", Header: map[string][]string{ "MUlReo8L": {"AUZG7wHG", "gsN0Dc2N"}, "1UJXjVrT": {"OJgxzTfk", "xZZrFsq7"}, }, Method: "5wkAxCUE", Body: "7CRjCJyz", OutputMaxSize: checks.DefaultBufSize, TCP: "MN3oA9D2", Interval: 32718 * time.Second, DockerContainerID: "cU15LMet", Shell: "nEz9qz2l", TLSSkipVerify: true, Timeout: 34738 * time.Second, TTL: 22773 * time.Second, DeregisterCriticalServiceAfter: 84282 * time.Second, }, &structs.CheckType{ CheckID: "UHsDeLxG", Name: "PQSaPWlT", Notes: "jKChDOdl", Status: "5qFz6OZn", ScriptArgs: []string{"NMtYWlT9", "vj74JXsm"}, HTTP: "1LBDJhw4", Header: map[string][]string{ "cXPmnv1M": {"imDqfaBx", "NFxZ1bQe"}, "vr7wY7CS": {"EtCoNPPL", "9vAarJ5s"}, }, Method: "wzByP903", Body: "4I8ucZgZ", OutputMaxSize: checks.DefaultBufSize, TCP: "2exjZIGE", Interval: 5656 * time.Second, DockerContainerID: "5tDBWpfA", Shell: "rlTpLM8s", TLSSkipVerify: true, Timeout: 4868 * time.Second, TTL: 11222 * time.Second, DeregisterCriticalServiceAfter: 68482 * time.Second, }, }, Connect: &structs.ServiceConnect{}, }, { ID: "Kh81CPF6", Name: "Kh81CPF6-proxy", Port: 31471, Kind: "connect-proxy", Proxy: &structs.ConnectProxyConfig{ DestinationServiceName: "6L6BVfgH", DestinationServiceID: "6L6BVfgH-id", LocalServiceAddress: "127.0.0.2", LocalServicePort: 23759, Config: map[string]interface{}{ "cedGGtZf": "pWrUNiWw", }, Upstreams: structs.Upstreams{ { DestinationType: "service", // Default should be explicitly filled DestinationName: "KPtAj2cb", LocalBindPort: 4051, Config: map[string]interface{}{ "kzRnZOyd": "nUNKoL8H", }, }, { DestinationType: "prepared_query", DestinationNamespace: "9nakw0td", DestinationName: "KSd8HsRl", LocalBindPort: 11884, LocalBindAddress: "127.24.88.0", }, }, Expose: structs.ExposeConfig{ Checks: true, Paths: []structs.ExposePath{ { Path: "/health", LocalPathPort: 8080, ListenerPort: 21500, Protocol: "http", }, }, }, }, Weights: &structs.Weights{ Passing: 1, Warning: 1, }, }, { ID: "kvVqbwSE", Kind: "mesh-gateway", Name: "gw-primary-dc", Port: 27147, Proxy: &structs.ConnectProxyConfig{ Config: map[string]interface{}{ "1CuJHVfw": "Kzqsa7yc", }, Upstreams: structs.Upstreams{}, }, Weights: &structs.Weights{ Passing: 1, Warning: 1, }, }, { ID: "dLOXpSCI", Name: "o1ynPkp0", TaggedAddresses: map[string]structs.ServiceAddress{ "lan": { Address: "2d79888a", Port: 2143, }, "wan": { Address: "d4db85e2", Port: 6109, }, }, Tags: []string{"nkwshvM5", "NTDWn3ek"}, Address: "cOlSOhbp", Token: "msy7iWER", Meta: map[string]string{"mymeta": "data"}, Port: 24237, Weights: &structs.Weights{ Passing: 100, Warning: 1, }, EnableTagOverride: true, Connect: &structs.ServiceConnect{ Native: true, }, Checks: structs.CheckTypes{ &structs.CheckType{ CheckID: "Zv99e9Ka", Name: "sgV4F7Pk", Notes: "yP5nKbW0", Status: "7oLMEyfu", ScriptArgs: []string{"5wEZtZpv", "0Ihyk8cS"}, HTTP: "KyDjGY9H", Header: map[string][]string{ "gv5qefTz": {"5Olo2pMG", "PvvKWQU5"}, "SHOVq1Vv": {"jntFhyym", "GYJh32pp"}, }, Method: "T66MFBfR", Body: "OwGjTFQi", OutputMaxSize: checks.DefaultBufSize, TCP: "bNnNfx2A", Interval: 22224 * time.Second, DockerContainerID: "ipgdFtjd", Shell: "omVZq7Sz", TLSSkipVerify: true, Timeout: 18913 * time.Second, TTL: 44743 * time.Second, DeregisterCriticalServiceAfter: 8482 * time.Second, }, &structs.CheckType{ CheckID: "G79O6Mpr", Name: "IEqrzrsd", Notes: "SVqApqeM", Status: "XXkVoZXt", ScriptArgs: []string{"wD05Bvao", "rLYB7kQC"}, HTTP: "kyICZsn8", Header: map[string][]string{ "4ebP5vL4": {"G20SrL5Q", "DwPKlMbo"}, "p2UI34Qz": {"UsG1D0Qh", "NHhRiB6s"}, }, Method: "ciYHWors", Body: "lUVLGYU7", OutputMaxSize: checks.DefaultBufSize, TCP: "FfvCwlqH", Interval: 12356 * time.Second, DockerContainerID: "HBndBU6R", Shell: "hVI33JjA", TLSSkipVerify: true, Timeout: 38282 * time.Second, TTL: 1181 * time.Second, DeregisterCriticalServiceAfter: 4992 * time.Second, }, &structs.CheckType{ CheckID: "RMi85Dv8", Name: "iehanzuq", Status: "rCvn53TH", Notes: "fti5lfF3", ScriptArgs: []string{"16WRUmwS", "QWk7j7ae"}, HTTP: "dl3Fgme3", Header: map[string][]string{ "rjm4DEd3": {"2m3m2Fls"}, "l4HwQ112": {"fk56MNlo", "dhLK56aZ"}, }, Method: "9afLm3Mj", Body: "wVVL2V6f", OutputMaxSize: checks.DefaultBufSize, TCP: "fjiLFqVd", Interval: 23926 * time.Second, DockerContainerID: "dO5TtRHk", Shell: "e6q2ttES", TLSSkipVerify: true, Timeout: 38483 * time.Second, TTL: 10943 * time.Second, DeregisterCriticalServiceAfter: 68787 * time.Second, }, }, }, }, UseStreamingBackend: true, SerfAdvertiseAddrLAN: tcpAddr("17.99.29.16:8301"), SerfAdvertiseAddrWAN: tcpAddr("78.63.37.19:8302"), SerfBindAddrLAN: tcpAddr("99.43.63.15:8301"), SerfBindAddrWAN: tcpAddr("67.88.33.19:8302"), SerfAllowedCIDRsLAN: []net.IPNet{}, SerfAllowedCIDRsWAN: []net.IPNet{}, SessionTTLMin: 26627 * time.Second, SkipLeaveOnInt: true, StartJoinAddrsLAN: []string{"LR3hGDoG", "MwVpZ4Up"}, StartJoinAddrsWAN: []string{"EbFSc3nA", "kwXTh623"}, Telemetry: lib.TelemetryConfig{ CirconusAPIApp: "p4QOTe9j", CirconusAPIToken: "E3j35V23", CirconusAPIURL: "mEMjHpGg", CirconusBrokerID: "BHlxUhed", CirconusBrokerSelectTag: "13xy1gHm", CirconusCheckDisplayName: "DRSlQR6n", CirconusCheckForceMetricActivation: "Ua5FGVYf", CirconusCheckID: "kGorutad", CirconusCheckInstanceID: "rwoOL6R4", CirconusCheckSearchTag: "ovT4hT4f", CirconusCheckTags: "prvO4uBl", CirconusSubmissionInterval: "DolzaflP", CirconusSubmissionURL: "gTcbS93G", DisableCompatOneNine: true, DisableHostname: true, DogstatsdAddr: "0wSndumK", DogstatsdTags: []string{"3N81zSUB", "Xtj8AnXZ"}, FilterDefault: true, AllowedPrefixes: []string{"oJotS8XJ"}, BlockedPrefixes: []string{"cazlEhGn"}, MetricsPrefix: "ftO6DySn", StatsdAddr: "drce87cy", StatsiteAddr: "HpFwKB8R", PrometheusOpts: prometheus.PrometheusOpts{ Expiration: 15 * time.Second, }, }, TLSCipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256}, TLSMinVersion: "pAOWafkR", TLSPreferServerCipherSuites: true, TaggedAddresses: map[string]string{ "7MYgHrYH": "dALJAhLD", "h6DdBy6K": "ebrr9zZ8", "lan": "17.99.29.16", "lan_ipv4": "17.99.29.16", "wan": "78.63.37.19", "wan_ipv4": "78.63.37.19", }, TranslateWANAddrs: true, TxnMaxReqLen: 5678000000000000, UIConfig: UIConfig{ Dir: "pVncV4Ey", ContentPath: "/qp1WRhYH/", // slashes are added in parsing MetricsProvider: "sgnaoa_lower_case", MetricsProviderFiles: []string{"sgnaMFoa", "dicnwkTH"}, MetricsProviderOptionsJSON: "{\"DIbVQadX\": 1}", MetricsProxy: UIMetricsProxy{ BaseURL: "http://foo.bar", AddHeaders: []UIMetricsProxyAddHeader{ { Name: "p3nynwc9", Value: "TYBgnN2F", }, }, PathAllowlist: []string{"/aSh3cu", "/eiK/2Th"}, }, DashboardURLTemplates: map[string]string{"u2eziu2n_lower_case": "http://lkjasd.otr"}, }, UnixSocketUser: "E0nB1DwA", UnixSocketGroup: "8pFodrV8", UnixSocketMode: "E8sAwOv4", VerifyIncoming: true, VerifyIncomingHTTPS: true, VerifyIncomingRPC: true, VerifyOutgoing: true, VerifyServerHostname: true, Watches: []map[string]interface{}{ { "type": "key", "datacenter": "GyE6jpeW", "key": "j9lF1Tve", "handler": "90N7S4LN", }, { "type": "keyprefix", "datacenter": "fYrl3F5d", "key": "sl3Dffu7", "args": []interface{}{"dltjDJ2a", "flEa7C2d"}, }, }, } entFullRuntimeConfig(expected) expectedWarns := []string{ `The 'acl_datacenter' field is deprecated. Use the 'primary_datacenter' field instead.`, `bootstrap_expect > 0: expecting 53 servers`, } expectedWarns = append(expectedWarns, enterpriseConfigKeyWarnings...) // FIXME: ensure that all fields are set to unique non-zero values. // There are many fields that are not set to non-zero values, so this nonZero // check does not actually work. if err := nonZero("RuntimeConfig", nil, expected); err != nil { t.Log(err) } for _, format := range []string{"json", "hcl"} { t.Run(format, func(t *testing.T) { opts := LoadOpts{ ConfigFiles: []string{"testdata/full-config." + format}, HCL: []string{fmt.Sprintf(`data_dir = "%s"`, dataDir)}, } opts.Overrides = append(opts.Overrides, versionSource("JNtPSav3", "R909Hblt", "ZT1JOQLn")) r, err := Load(opts) require.NoError(t, err) assertDeepEqual(t, expected, r.RuntimeConfig) require.ElementsMatch(t, expectedWarns, r.Warnings, "Warnings: %#v", r.Warnings) }) } } // nonZero verifies recursively that all fields are set to unique, // non-zero and non-nil values. // // struct: check all fields recursively // slice: check len > 0 and all values recursively // ptr: check not nil // bool: check not zero (cannot check uniqueness) // string, int, uint: check not zero and unique // other: error func nonZero(name string, uniq map[interface{}]string, v interface{}) error { if v == nil { return fmt.Errorf("%q is nil", name) } if uniq == nil { uniq = map[interface{}]string{} } isUnique := func(v interface{}) error { if other := uniq[v]; other != "" { return fmt.Errorf("%q and %q both use value %q", name, other, v) } uniq[v] = name return nil } val, typ := reflect.ValueOf(v), reflect.TypeOf(v) // fmt.Printf("%s: %T\n", name, v) switch typ.Kind() { case reflect.Struct: for i := 0; i < typ.NumField(); i++ { f := typ.Field(i) fieldname := fmt.Sprintf("%s.%s", name, f.Name) err := nonZero(fieldname, uniq, val.Field(i).Interface()) if err != nil { return err } } case reflect.Slice: if val.Len() == 0 { return fmt.Errorf("%q is empty slice", name) } for i := 0; i < val.Len(); i++ { elemname := fmt.Sprintf("%s[%d]", name, i) err := nonZero(elemname, uniq, val.Index(i).Interface()) if err != nil { return err } } case reflect.Map: if val.Len() == 0 { return fmt.Errorf("%q is empty map", name) } for _, key := range val.MapKeys() { keyname := fmt.Sprintf("%s[%s]", name, key.String()) if err := nonZero(keyname, uniq, key.Interface()); err != nil { if strings.Contains(err.Error(), "is zero value") { return fmt.Errorf("%q has zero value map key", name) } return err } if err := nonZero(keyname, uniq, val.MapIndex(key).Interface()); err != nil { return err } } case reflect.Bool: if val.Bool() != true { return fmt.Errorf("%q is zero value", name) } // do not test bool for uniqueness since there are only two values case reflect.String: if val.Len() == 0 { return fmt.Errorf("%q is zero value", name) } return isUnique(v) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if val.Int() == 0 { return fmt.Errorf("%q is zero value", name) } return isUnique(v) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: if val.Uint() == 0 { return fmt.Errorf("%q is zero value", name) } return isUnique(v) case reflect.Float32, reflect.Float64: if val.Float() == 0 { return fmt.Errorf("%q is zero value", name) } return isUnique(v) case reflect.Ptr: if val.IsNil() { return fmt.Errorf("%q is nil", name) } return nonZero("*"+name, uniq, val.Elem().Interface()) default: return fmt.Errorf("%T is not supported", v) } return nil } func TestNonZero(t *testing.T) { tests := []struct { desc string v interface{} err error }{ {"nil", nil, errors.New(`"x" is nil`)}, {"zero bool", false, errors.New(`"x" is zero value`)}, {"zero string", "", errors.New(`"x" is zero value`)}, {"zero int", int(0), errors.New(`"x" is zero value`)}, {"zero int8", int8(0), errors.New(`"x" is zero value`)}, {"zero int16", int16(0), errors.New(`"x" is zero value`)}, {"zero int32", int32(0), errors.New(`"x" is zero value`)}, {"zero int64", int64(0), errors.New(`"x" is zero value`)}, {"zero uint", uint(0), errors.New(`"x" is zero value`)}, {"zero uint8", uint8(0), errors.New(`"x" is zero value`)}, {"zero uint16", uint16(0), errors.New(`"x" is zero value`)}, {"zero uint32", uint32(0), errors.New(`"x" is zero value`)}, {"zero uint64", uint64(0), errors.New(`"x" is zero value`)}, {"zero float32", float32(0), errors.New(`"x" is zero value`)}, {"zero float64", float64(0), errors.New(`"x" is zero value`)}, {"ptr to zero value", pString(""), errors.New(`"*x" is zero value`)}, {"empty slice", []string{}, errors.New(`"x" is empty slice`)}, {"slice with zero value", []string{""}, errors.New(`"x[0]" is zero value`)}, {"empty map", map[string]string{}, errors.New(`"x" is empty map`)}, {"map with zero value key", map[string]string{"": "y"}, errors.New(`"x" has zero value map key`)}, {"map with zero value elem", map[string]string{"y": ""}, errors.New(`"x[y]" is zero value`)}, {"struct with nil field", struct{ Y *int }{}, errors.New(`"x.Y" is nil`)}, {"struct with zero value field", struct{ Y string }{}, errors.New(`"x.Y" is zero value`)}, {"struct with empty array", struct{ Y []string }{}, errors.New(`"x.Y" is empty slice`)}, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { require.Equal(t, tt.err, nonZero("x", nil, tt.v)) }) } } func TestConfigDecodeBytes(t *testing.T) { // Test with some input src := []byte("abc") key := base64.StdEncoding.EncodeToString(src) result, err := decodeBytes(key) if err != nil { t.Fatalf("err: %s", err) } if !bytes.Equal(src, result) { t.Fatalf("bad: %#v", result) } // Test with no input result, err = decodeBytes("") if err != nil { t.Fatalf("err: %s", err) } if len(result) > 0 { t.Fatalf("bad: %#v", result) } } func parseCIDR(t *testing.T, cidr string) *net.IPNet { _, x, err := net.ParseCIDR(cidr) if err != nil { t.Fatalf("CIDRParse: %v", err) } return x } func TestRuntimeConfig_Sanitize(t *testing.T) { rt := RuntimeConfig{ BindAddr: &net.IPAddr{IP: net.ParseIP("127.0.0.1")}, CheckOutputMaxSize: checks.DefaultBufSize, SerfAdvertiseAddrLAN: &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 5678}, DNSAddrs: []net.Addr{ &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 5678}, &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 5678}, }, DNSSOA: RuntimeSOAConfig{Refresh: 3600, Retry: 600, Expire: 86400, Minttl: 0}, AllowWriteHTTPFrom: []*net.IPNet{ parseCIDR(t, "127.0.0.0/8"), parseCIDR(t, "::1/128"), }, HTTPAddrs: []net.Addr{ &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 5678}, &net.UnixAddr{Name: "/var/run/foo"}, }, Cache: cache.Options{ EntryFetchMaxBurst: 42, EntryFetchRate: 0.334, }, ConsulCoordinateUpdatePeriod: 15 * time.Second, RaftProtocol: 3, RetryJoinLAN: []string{ "foo=bar key=baz secret=boom bang=bar", }, RetryJoinWAN: []string{ "wan_foo=bar wan_key=baz wan_secret=boom wan_bang=bar", }, PrimaryGateways: []string{ "pmgw_foo=bar pmgw_key=baz pmgw_secret=boom pmgw_bang=bar", }, Services: []*structs.ServiceDefinition{ { Name: "foo", Token: "bar", Check: structs.CheckType{ Name: "blurb", OutputMaxSize: checks.DefaultBufSize, }, Weights: &structs.Weights{ Passing: 67, Warning: 3, }, }, }, Checks: []*structs.CheckDefinition{ { Name: "zoo", Token: "zope", OutputMaxSize: checks.DefaultBufSize, }, }, KVMaxValueSize: 1234567800000000, SerfAllowedCIDRsLAN: []net.IPNet{ *parseCIDR(t, "192.168.1.0/24"), *parseCIDR(t, "127.0.0.0/8"), }, TxnMaxReqLen: 5678000000000000, UIConfig: UIConfig{ MetricsProxy: UIMetricsProxy{ AddHeaders: []UIMetricsProxyAddHeader{ {Name: "foo", Value: "secret"}, }, }, }, } b, err := json.MarshalIndent(rt.Sanitized(), "", " ") require.NoError(t, err) actual := string(b) require.JSONEq(t, golden(t, actual, testRuntimeConfigSanitizeExpectedFilename), actual) } func TestRuntime_apiAddresses(t *testing.T) { rt := RuntimeConfig{ HTTPAddrs: []net.Addr{ &net.TCPAddr{IP: net.ParseIP("198.18.0.1"), Port: 5678}, &net.UnixAddr{Name: "/var/run/foo"}, }, HTTPSAddrs: []net.Addr{ &net.TCPAddr{IP: net.ParseIP("198.18.0.2"), Port: 5678}, }} unixAddrs, httpAddrs, httpsAddrs := rt.apiAddresses(1) require.Len(t, unixAddrs, 1) require.Len(t, httpAddrs, 1) require.Len(t, httpsAddrs, 1) require.Equal(t, "/var/run/foo", unixAddrs[0]) require.Equal(t, "198.18.0.1:5678", httpAddrs[0]) require.Equal(t, "198.18.0.2:5678", httpsAddrs[0]) } func TestRuntime_APIConfigHTTPS(t *testing.T) { rt := RuntimeConfig{ HTTPAddrs: []net.Addr{ &net.TCPAddr{IP: net.ParseIP("198.18.0.1"), Port: 5678}, &net.UnixAddr{Name: "/var/run/foo"}, }, HTTPSAddrs: []net.Addr{ &net.TCPAddr{IP: net.ParseIP("198.18.0.2"), Port: 5678}, }, Datacenter: "dc-test", CAFile: "/etc/consul/ca.crt", CAPath: "/etc/consul/ca.dir", CertFile: "/etc/consul/server.crt", KeyFile: "/etc/consul/ssl/server.key", VerifyOutgoing: false, } cfg, err := rt.APIConfig(false) require.NoError(t, err) require.Equal(t, "198.18.0.2:5678", cfg.Address) require.Equal(t, "https", cfg.Scheme) require.Equal(t, rt.CAFile, cfg.TLSConfig.CAFile) require.Equal(t, rt.CAPath, cfg.TLSConfig.CAPath) require.Equal(t, "", cfg.TLSConfig.CertFile) require.Equal(t, "", cfg.TLSConfig.KeyFile) require.Equal(t, rt.Datacenter, cfg.Datacenter) require.Equal(t, true, cfg.TLSConfig.InsecureSkipVerify) rt.VerifyOutgoing = true cfg, err = rt.APIConfig(true) require.NoError(t, err) require.Equal(t, "198.18.0.2:5678", cfg.Address) require.Equal(t, "https", cfg.Scheme) require.Equal(t, rt.CAFile, cfg.TLSConfig.CAFile) require.Equal(t, rt.CAPath, cfg.TLSConfig.CAPath) require.Equal(t, rt.CertFile, cfg.TLSConfig.CertFile) require.Equal(t, rt.KeyFile, cfg.TLSConfig.KeyFile) require.Equal(t, rt.Datacenter, cfg.Datacenter) require.Equal(t, false, cfg.TLSConfig.InsecureSkipVerify) } func TestRuntime_APIConfigHTTP(t *testing.T) { rt := RuntimeConfig{ HTTPAddrs: []net.Addr{ &net.UnixAddr{Name: "/var/run/foo"}, &net.TCPAddr{IP: net.ParseIP("198.18.0.1"), Port: 5678}, }, Datacenter: "dc-test", } cfg, err := rt.APIConfig(false) require.NoError(t, err) require.Equal(t, rt.Datacenter, cfg.Datacenter) require.Equal(t, "198.18.0.1:5678", cfg.Address) require.Equal(t, "http", cfg.Scheme) require.Equal(t, "", cfg.TLSConfig.CAFile) require.Equal(t, "", cfg.TLSConfig.CAPath) require.Equal(t, "", cfg.TLSConfig.CertFile) require.Equal(t, "", cfg.TLSConfig.KeyFile) } func TestRuntime_APIConfigUNIX(t *testing.T) { rt := RuntimeConfig{ HTTPAddrs: []net.Addr{ &net.UnixAddr{Name: "/var/run/foo"}, }, Datacenter: "dc-test", } cfg, err := rt.APIConfig(false) require.NoError(t, err) require.Equal(t, rt.Datacenter, cfg.Datacenter) require.Equal(t, "unix:///var/run/foo", cfg.Address) require.Equal(t, "http", cfg.Scheme) require.Equal(t, "", cfg.TLSConfig.CAFile) require.Equal(t, "", cfg.TLSConfig.CAPath) require.Equal(t, "", cfg.TLSConfig.CertFile) require.Equal(t, "", cfg.TLSConfig.KeyFile) } func TestRuntime_APIConfigANYAddrV4(t *testing.T) { rt := RuntimeConfig{ HTTPAddrs: []net.Addr{ &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 5678}, }, Datacenter: "dc-test", } cfg, err := rt.APIConfig(false) require.NoError(t, err) require.Equal(t, rt.Datacenter, cfg.Datacenter) require.Equal(t, "127.0.0.1:5678", cfg.Address) require.Equal(t, "http", cfg.Scheme) require.Equal(t, "", cfg.TLSConfig.CAFile) require.Equal(t, "", cfg.TLSConfig.CAPath) require.Equal(t, "", cfg.TLSConfig.CertFile) require.Equal(t, "", cfg.TLSConfig.KeyFile) } func TestRuntime_APIConfigANYAddrV6(t *testing.T) { rt := RuntimeConfig{ HTTPAddrs: []net.Addr{ &net.TCPAddr{IP: net.ParseIP("::"), Port: 5678}, }, Datacenter: "dc-test", } cfg, err := rt.APIConfig(false) require.NoError(t, err) require.Equal(t, rt.Datacenter, cfg.Datacenter) require.Equal(t, "[::1]:5678", cfg.Address) require.Equal(t, "http", cfg.Scheme) require.Equal(t, "", cfg.TLSConfig.CAFile) require.Equal(t, "", cfg.TLSConfig.CAPath) require.Equal(t, "", cfg.TLSConfig.CertFile) require.Equal(t, "", cfg.TLSConfig.KeyFile) } func TestRuntime_ClientAddress(t *testing.T) { rt := RuntimeConfig{ HTTPAddrs: []net.Addr{ &net.TCPAddr{IP: net.ParseIP("::"), Port: 5678}, &net.TCPAddr{IP: net.ParseIP("198.18.0.1"), Port: 5679}, &net.UnixAddr{Name: "/var/run/foo", Net: "unix"}, }, HTTPSAddrs: []net.Addr{ &net.TCPAddr{IP: net.ParseIP("::"), Port: 5688}, &net.TCPAddr{IP: net.ParseIP("198.18.0.1"), Port: 5689}, }, } unix, http, https := rt.ClientAddress() require.Equal(t, "unix:///var/run/foo", unix) require.Equal(t, "198.18.0.1:5679", http) require.Equal(t, "198.18.0.1:5689", https) } func TestRuntime_ClientAddressAnyV4(t *testing.T) { rt := RuntimeConfig{ HTTPAddrs: []net.Addr{ &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 5678}, &net.UnixAddr{Name: "/var/run/foo", Net: "unix"}, }, HTTPSAddrs: []net.Addr{ &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 5688}, }, } unix, http, https := rt.ClientAddress() require.Equal(t, "unix:///var/run/foo", unix) require.Equal(t, "127.0.0.1:5678", http) require.Equal(t, "127.0.0.1:5688", https) } func TestRuntime_ClientAddressAnyV6(t *testing.T) { rt := RuntimeConfig{ HTTPAddrs: []net.Addr{ &net.TCPAddr{IP: net.ParseIP("::"), Port: 5678}, &net.UnixAddr{Name: "/var/run/foo", Net: "unix"}, }, HTTPSAddrs: []net.Addr{ &net.TCPAddr{IP: net.ParseIP("::"), Port: 5688}, }, } unix, http, https := rt.ClientAddress() require.Equal(t, "unix:///var/run/foo", unix) require.Equal(t, "[::1]:5678", http) require.Equal(t, "[::1]:5688", https) } func TestRuntime_ToTLSUtilConfig(t *testing.T) { c := &RuntimeConfig{ VerifyIncoming: true, VerifyIncomingRPC: true, VerifyIncomingHTTPS: true, VerifyOutgoing: true, VerifyServerHostname: true, CAFile: "a", CAPath: "b", CertFile: "c", KeyFile: "d", NodeName: "e", ServerName: "f", DNSDomain: "g", TLSMinVersion: "tls12", TLSCipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA}, TLSPreferServerCipherSuites: true, EnableAgentTLSForChecks: true, AutoEncryptTLS: true, } r := c.ToTLSUtilConfig() require.True(t, r.VerifyIncoming) require.True(t, r.VerifyIncomingRPC) require.True(t, r.VerifyIncomingHTTPS) require.True(t, r.VerifyOutgoing) require.True(t, r.EnableAgentTLSForChecks) require.True(t, r.AutoTLS) require.True(t, r.VerifyServerHostname) require.True(t, r.PreferServerCipherSuites) require.Equal(t, "a", r.CAFile) require.Equal(t, "b", r.CAPath) require.Equal(t, "c", r.CertFile) require.Equal(t, "d", r.KeyFile) require.Equal(t, "e", r.NodeName) require.Equal(t, "f", r.ServerName) require.Equal(t, "g", r.Domain) require.Equal(t, "tls12", r.TLSMinVersion) require.Equal(t, []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA}, r.CipherSuites) } func TestRuntime_ToTLSUtilConfig_AutoConfig(t *testing.T) { c := &RuntimeConfig{ VerifyIncoming: true, VerifyIncomingRPC: true, VerifyIncomingHTTPS: true, VerifyOutgoing: true, VerifyServerHostname: true, CAFile: "a", CAPath: "b", CertFile: "c", KeyFile: "d", NodeName: "e", ServerName: "f", DNSDomain: "g", TLSMinVersion: "tls12", TLSCipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA}, TLSPreferServerCipherSuites: true, EnableAgentTLSForChecks: true, AutoConfig: AutoConfig{Enabled: true}, } r := c.ToTLSUtilConfig() require.True(t, r.VerifyIncoming) require.True(t, r.VerifyIncomingRPC) require.True(t, r.VerifyIncomingHTTPS) require.True(t, r.VerifyOutgoing) require.True(t, r.EnableAgentTLSForChecks) require.True(t, r.AutoTLS) require.True(t, r.VerifyServerHostname) require.True(t, r.PreferServerCipherSuites) require.Equal(t, "a", r.CAFile) require.Equal(t, "b", r.CAPath) require.Equal(t, "c", r.CertFile) require.Equal(t, "d", r.KeyFile) require.Equal(t, "e", r.NodeName) require.Equal(t, "f", r.ServerName) require.Equal(t, "g", r.Domain) require.Equal(t, "tls12", r.TLSMinVersion) require.Equal(t, []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA}, r.CipherSuites) } func Test_UIPathBuilder(t *testing.T) { cases := []struct { name string path string expected string }{ { "Letters only string", "hello", "/hello/", }, { "Alphanumeric", "Hello1", "/Hello1/", }, { "Hyphen and underscore", "-_", "/-_/", }, { "Many slashes", "/hello/ui/1/", "/hello/ui/1/", }, } for _, tt := range cases { actual := UIPathBuilder(tt.path) require.Equal(t, tt.expected, actual) } } func splitIPPort(hostport string) (net.IP, int) { h, p, err := net.SplitHostPort(hostport) if err != nil { panic(err) } port, err := strconv.Atoi(p) if err != nil { panic(err) } return net.ParseIP(h), port } func ipAddr(addr string) *net.IPAddr { return &net.IPAddr{IP: net.ParseIP(addr)} } func tcpAddr(addr string) *net.TCPAddr { ip, port := splitIPPort(addr) return &net.TCPAddr{IP: ip, Port: port} } func udpAddr(addr string) *net.UDPAddr { ip, port := splitIPPort(addr) return &net.UDPAddr{IP: ip, Port: port} } func unixAddr(addr string) *net.UnixAddr { if !strings.HasPrefix(addr, "unix://") { panic("not a unix socket addr: " + addr) } return &net.UnixAddr{Net: "unix", Name: addr[len("unix://"):]} } func writeFile(path string, data []byte) { if err := os.MkdirAll(filepath.Dir(path), 0750); err != nil { panic(err) } if err := ioutil.WriteFile(path, data, 0640); err != nil { panic(err) } } func randomString(n int) string { s := "" for ; n > 0; n-- { s += "x" } return s } func metaPairs(n int, format string) string { var s []string for i := 0; i < n; i++ { switch format { case "json": s = append(s, fmt.Sprintf(`"%d":"%d"`, i, i)) case "hcl": s = append(s, fmt.Sprintf(`"%d"="%d"`, i, i)) default: panic("invalid format: " + format) } } switch format { case "json": return strings.Join(s, ",") case "hcl": return strings.Join(s, " ") default: panic("invalid format: " + format) } } func TestConnectCAConfiguration(t *testing.T) { type testCase struct { config RuntimeConfig expected *structs.CAConfiguration err string } cases := map[string]testCase{ "defaults": { config: RuntimeConfig{ ConnectEnabled: true, }, expected: &structs.CAConfiguration{ Provider: "consul", Config: map[string]interface{}{ "RotationPeriod": "2160h", "LeafCertTTL": "72h", "IntermediateCertTTL": "8760h", // 365 * 24h }, }, }, "cluster-id-override": { config: RuntimeConfig{ ConnectEnabled: true, ConnectCAConfig: map[string]interface{}{ "cluster_id": "adfe7697-09b4-413a-ac0a-fa81ed3a3001", }, }, expected: &structs.CAConfiguration{ Provider: "consul", ClusterID: "adfe7697-09b4-413a-ac0a-fa81ed3a3001", Config: map[string]interface{}{ "RotationPeriod": "2160h", "LeafCertTTL": "72h", "IntermediateCertTTL": "8760h", // 365 * 24h "cluster_id": "adfe7697-09b4-413a-ac0a-fa81ed3a3001", }, }, }, "cluster-id-non-uuid": { config: RuntimeConfig{ ConnectEnabled: true, ConnectCAConfig: map[string]interface{}{ "cluster_id": "foo", }, }, err: "cluster_id was supplied but was not a valid UUID", }, "provider-override": { config: RuntimeConfig{ ConnectEnabled: true, ConnectCAProvider: "vault", }, expected: &structs.CAConfiguration{ Provider: "vault", Config: map[string]interface{}{ "RotationPeriod": "2160h", "LeafCertTTL": "72h", "IntermediateCertTTL": "8760h", // 365 * 24h }, }, }, "other-config": { config: RuntimeConfig{ ConnectEnabled: true, ConnectCAConfig: map[string]interface{}{ "foo": "bar", }, }, expected: &structs.CAConfiguration{ Provider: "consul", Config: map[string]interface{}{ "RotationPeriod": "2160h", "LeafCertTTL": "72h", "IntermediateCertTTL": "8760h", // 365 * 24h "foo": "bar", }, }, }, } for name, tcase := range cases { t.Run(name, func(t *testing.T) { actual, err := tcase.config.ConnectCAConfiguration() if tcase.err != "" { testutil.RequireErrorContains(t, err, tcase.err) } else { require.NoError(t, err) require.Equal(t, tcase.expected, actual) } }) } }