Files
nomad/command/agent/config_test.go
Brendan MacDonell 26485c45a2 Add job_max_count option to keep Nomad server from running out of memory (#26858)
If a Nomad job is started with a large number of instances (e.g. 4 billion),
then the Nomad servers that attempt to schedule it will run out of memory and
crash. While it's unlikely that anyone would intentionally schedule a job with 4
billion instances, we have occasionally run into issues with bugs in external
automation. For example, an automated deployment system running on a test
environment had an off-by-one error, and deployed a job with count = uint32(-1),
causing the Nomad servers for that environment to run out of memory and crash.

To prevent this, this PR introduces a job_max_count Nomad server configuration
parameter. job_max_count limits the number of allocs that may be created from a
job. The default value is 50000 - this is low enough that a job with the maximum
possible number of allocs will not require much memory on the server, but is
still much higher than the number of allocs in the largest Nomad job we have
ever run.
2025-10-06 09:35:10 -04:00

2109 lines
55 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package agent
import (
"errors"
"fmt"
"net"
"os"
"path/filepath"
"reflect"
"runtime"
"testing"
"time"
sockaddr "github.com/hashicorp/go-sockaddr"
"github.com/hashicorp/nomad/ci"
client "github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/client/testutil"
"github.com/hashicorp/nomad/helper/pointer"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/nomad/structs/config"
"github.com/shoenig/test"
"github.com/shoenig/test/must"
"github.com/stretchr/testify/require"
)
var (
// trueValue/falseValue are used to get a pointer to a boolean
trueValue = true
falseValue = false
)
func TestConfig_Merge(t *testing.T) {
ci.Parallel(t)
c0 := &Config{}
c1 := &Config{
Telemetry: &Telemetry{},
Client: &ClientConfig{},
Server: &ServerConfig{},
ACL: &ACLConfig{},
Audit: &config.AuditConfig{},
Ports: &Ports{},
Addresses: &Addresses{},
AdvertiseAddrs: &AdvertiseAddrs{},
Sentinel: &config.SentinelConfig{},
Autopilot: &config.AutopilotConfig{},
Eventlog: &Eventlog{},
}
c2 := &Config{
Region: "global",
Datacenter: "dc1",
NodeName: "node1",
DataDir: "/tmp/dir1",
PluginDir: "/tmp/pluginDir1",
LogLevel: "INFO",
LogIncludeLocation: false,
LogJson: false,
EnableDebug: false,
LeaveOnInt: false,
LeaveOnTerm: false,
EnableSyslog: false,
SyslogFacility: "local0.info",
DisableUpdateCheck: pointer.Of(false),
DisableAnonymousSignature: false,
BindAddr: "127.0.0.1",
Telemetry: &Telemetry{
StatsiteAddr: "127.0.0.1:8125",
StatsdAddr: "127.0.0.1:8125",
DataDogAddr: "127.0.0.1:8125",
DataDogTags: []string{"cat1:tag1", "cat2:tag2"},
PrometheusMetrics: true,
DisableHostname: false,
DisableAllocationHookMetrics: pointer.Of(false),
CirconusAPIToken: "0",
CirconusAPIApp: "nomadic",
CirconusAPIURL: "http://api.circonus.com/v2",
CirconusSubmissionInterval: "60s",
CirconusCheckSubmissionURL: "https://someplace.com/metrics",
CirconusCheckID: "0",
CirconusCheckForceMetricActivation: "true",
CirconusCheckInstanceID: "node1:nomadic",
CirconusCheckSearchTag: "service:nomadic",
CirconusCheckDisplayName: "node1:nomadic",
CirconusCheckTags: "cat1:tag1,cat2:tag2",
CirconusBrokerID: "0",
CirconusBrokerSelectTag: "dc:dc1",
PrefixFilter: []string{"filter1", "filter2"},
},
Audit: &config.AuditConfig{
Enabled: pointer.Of(true),
Sinks: []*config.AuditSink{
{
DeliveryGuarantee: "enforced",
Name: "file",
Type: "file",
Format: "json",
Path: "/opt/nomad/audit.log",
RotateDuration: 24 * time.Hour,
RotateDurationHCL: "24h",
RotateBytes: 100,
RotateMaxFiles: 10,
},
},
},
Client: &ClientConfig{
Enabled: false,
StateDir: "/tmp/state1",
AllocDir: "/tmp/alloc1",
NodeClass: "class1",
NodePool: "dev",
Options: map[string]string{
"foo": "bar",
},
NetworkSpeed: 100,
CpuCompute: 100,
MinDynamicPort: 10001,
MaxDynamicPort: 10002,
MemoryMB: 100,
MaxKillTimeout: "20s",
ClientMaxPort: 19996,
DisableRemoteExec: false,
TemplateConfig: &client.ClientTemplateConfig{
FunctionDenylist: client.DefaultTemplateFunctionDenylist,
DisableSandbox: false,
},
Reserved: &Resources{
CPU: 10,
MemoryMB: 10,
DiskMB: 10,
ReservedPorts: "1,10-30,55",
},
NomadServiceDiscovery: pointer.Of(false),
},
Server: &ServerConfig{
Enabled: false,
AuthoritativeRegion: "global",
BootstrapExpect: 1,
DataDir: "/tmp/data1",
ProtocolVersion: 1,
RaftProtocol: 1,
RaftMultiplier: pointer.Of(5),
RaftSnapshotThreshold: pointer.Of(100),
RaftSnapshotInterval: pointer.Of("30m"),
RaftTrailingLogs: pointer.Of(200),
NumSchedulers: pointer.Of(1),
NodeGCThreshold: "1h",
BatchEvalGCThreshold: "4h",
HeartbeatGrace: 30 * time.Second,
MinHeartbeatTTL: 30 * time.Second,
MaxHeartbeatsPerSecond: 30.0,
RedundancyZone: "foo",
UpgradeVersion: "foo",
EnableEventBroker: pointer.Of(false),
EventBufferSize: pointer.Of(0),
PlanRejectionTracker: &PlanRejectionTracker{
Enabled: pointer.Of(true),
NodeThreshold: 100,
NodeWindow: 11 * time.Minute,
},
OIDCIssuer: "https://oidc.test.nomadproject.io",
StartTimeout: "45s",
},
ACL: &ACLConfig{
Enabled: true,
TokenTTL: 60 * time.Second,
PolicyTTL: 60 * time.Second,
RoleTTL: 60 * time.Second,
TokenMinExpirationTTL: 60 * time.Second,
TokenMaxExpirationTTL: 60 * time.Second,
ReplicationToken: "foo",
},
Ports: &Ports{
HTTP: 4646,
RPC: 4647,
Serf: 4648,
},
Addresses: &Addresses{
HTTP: "127.0.0.1",
RPC: "127.0.0.1",
Serf: "127.0.0.1",
},
AdvertiseAddrs: &AdvertiseAddrs{
RPC: "127.0.0.1",
Serf: "127.0.0.1",
},
HTTPAPIResponseHeaders: map[string]string{
"Access-Control-Allow-Origin": "*",
},
Vaults: []*config.VaultConfig{{
Name: structs.VaultDefaultCluster,
Addr: "1",
TLSCaFile: "1",
TLSCaPath: "1",
TLSCertFile: "1",
TLSKeyFile: "1",
TLSSkipVerify: &falseValue,
TLSServerName: "1",
}},
Consuls: []*config.ConsulConfig{{
ServerServiceName: "1",
ClientServiceName: "1",
AutoAdvertise: &falseValue,
Addr: "1",
Timeout: 1 * time.Second,
Token: "1",
Auth: "1",
EnableSSL: &falseValue,
VerifySSL: &falseValue,
CAFile: "1",
CertFile: "1",
KeyFile: "1",
ServerAutoJoin: &falseValue,
ClientAutoJoin: &falseValue,
ChecksUseAdvertise: &falseValue,
}},
Autopilot: &config.AutopilotConfig{
CleanupDeadServers: &falseValue,
ServerStabilizationTime: 1 * time.Second,
LastContactThreshold: 1 * time.Second,
MaxTrailingLogs: 1,
MinQuorum: 1,
EnableRedundancyZones: &falseValue,
DisableUpgradeMigration: &falseValue,
EnableCustomUpgrades: &falseValue,
},
Plugins: []*config.PluginConfig{
{
Name: "docker",
Args: []string{"foo"},
Config: map[string]interface{}{
"bar": 1,
},
},
},
Eventlog: &Eventlog{
Enabled: true,
Level: "INFO",
},
}
c3 := &Config{
Region: "global",
Datacenter: "dc2",
NodeName: "node2",
DataDir: "/tmp/dir2",
PluginDir: "/tmp/pluginDir2",
LogLevel: "DEBUG",
LogIncludeLocation: true,
LogJson: true,
EnableDebug: true,
LeaveOnInt: true,
LeaveOnTerm: true,
EnableSyslog: true,
SyslogFacility: "local0.debug",
DisableUpdateCheck: pointer.Of(true),
DisableAnonymousSignature: true,
BindAddr: "127.0.0.2",
Audit: &config.AuditConfig{
Enabled: pointer.Of(true),
Sinks: []*config.AuditSink{
{
DeliveryGuarantee: "enforced",
Name: "file",
Type: "file",
Format: "json",
Path: "/opt/nomad/audit.log",
RotateDuration: 24 * time.Hour,
RotateDurationHCL: "24h",
RotateBytes: 100,
RotateMaxFiles: 10,
},
},
},
Telemetry: &Telemetry{
StatsiteAddr: "127.0.0.2:8125",
StatsdAddr: "127.0.0.2:8125",
DataDogAddr: "127.0.0.1:8125",
DataDogTags: []string{"cat1:tag1", "cat2:tag2"},
PrometheusMetrics: true,
DisableHostname: true,
DisableAllocationHookMetrics: pointer.Of(true),
PublishNodeMetrics: true,
PublishAllocationMetrics: true,
CirconusAPIToken: "1",
CirconusAPIApp: "nomad",
CirconusAPIURL: "https://api.circonus.com/v2",
CirconusSubmissionInterval: "10s",
CirconusCheckSubmissionURL: "https://example.com/metrics",
CirconusCheckID: "1",
CirconusCheckForceMetricActivation: "false",
CirconusCheckInstanceID: "node2:nomad",
CirconusCheckSearchTag: "service:nomad",
CirconusCheckDisplayName: "node2:nomad",
CirconusCheckTags: "cat1:tag1,cat2:tag2",
CirconusBrokerID: "1",
CirconusBrokerSelectTag: "dc:dc2",
PrefixFilter: []string{"prefix1", "prefix2"},
DisableDispatchedJobSummaryMetrics: true,
DisableQuotaUtilizationMetrics: false,
DisableRPCRateMetricsLabels: true,
FilterDefault: pointer.Of(false),
},
Client: &ClientConfig{
Enabled: true,
StateDir: "/tmp/state2",
AllocDir: "/tmp/alloc2",
NodeClass: "class2",
NodePool: "dev",
Servers: []string{"server2"},
Meta: map[string]string{
"baz": "zip",
},
Options: map[string]string{
"foo": "bar",
"baz": "zip",
},
ChrootEnv: map[string]string{},
ClientMaxPort: 20000,
ClientMinPort: 22000,
NetworkSpeed: 105,
CpuCompute: 105,
MinDynamicPort: 10002,
MaxDynamicPort: 10003,
MemoryMB: 105,
MaxKillTimeout: "50s",
DisableRemoteExec: false,
TemplateConfig: &client.ClientTemplateConfig{
FunctionDenylist: client.DefaultTemplateFunctionDenylist,
DisableSandbox: false,
BlockQueryWaitTime: pointer.Of(5 * time.Minute),
MaxStale: pointer.Of(client.DefaultTemplateMaxStale),
Wait: &client.WaitConfig{
Min: pointer.Of(5 * time.Second),
Max: pointer.Of(4 * time.Minute),
},
ConsulRetry: &client.RetryConfig{Attempts: pointer.Of(0)},
VaultRetry: &client.RetryConfig{Attempts: pointer.Of(0)},
NomadRetry: &client.RetryConfig{Attempts: pointer.Of(0)},
},
Reserved: &Resources{
CPU: 15,
MemoryMB: 15,
DiskMB: 15,
ReservedPorts: "2,10-30,55",
},
GCInterval: 6 * time.Second,
GCParallelDestroys: 6,
GCDiskUsageThreshold: 71,
GCInodeUsageThreshold: 86,
NomadServiceDiscovery: pointer.Of(false),
},
Server: &ServerConfig{
Enabled: true,
AuthoritativeRegion: "global2",
BootstrapExpect: 2,
DataDir: "/tmp/data2",
ProtocolVersion: 2,
RaftProtocol: 2,
RaftMultiplier: pointer.Of(6),
RaftSnapshotThreshold: pointer.Of(100),
RaftSnapshotInterval: pointer.Of("30m"),
RaftTrailingLogs: pointer.Of(200),
NumSchedulers: pointer.Of(2),
EnabledSchedulers: []string{structs.JobTypeBatch},
NodeGCThreshold: "12h",
BatchEvalGCThreshold: "4h",
HeartbeatGrace: 2 * time.Minute,
MinHeartbeatTTL: 2 * time.Minute,
MaxHeartbeatsPerSecond: 200.0,
RejoinAfterLeave: true,
StartJoin: []string{"1.1.1.1"},
RetryJoin: []string{"1.1.1.1"},
RetryInterval: time.Second * 10,
NonVotingServer: true,
RedundancyZone: "bar",
UpgradeVersion: "bar",
EnableEventBroker: pointer.Of(true),
EventBufferSize: pointer.Of(100),
PlanRejectionTracker: &PlanRejectionTracker{
Enabled: pointer.Of(true),
NodeThreshold: 100,
NodeWindow: 11 * time.Minute,
},
JobMaxPriority: pointer.Of(200),
JobDefaultPriority: pointer.Of(100),
JobMaxCount: pointer.Of(1000),
OIDCIssuer: "https://oidc.test.nomadproject.io",
StartTimeout: "1m",
},
ACL: &ACLConfig{
Enabled: true,
TokenTTL: 20 * time.Second,
PolicyTTL: 20 * time.Second,
RoleTTL: 20 * time.Second,
TokenMinExpirationTTL: 20 * time.Second,
TokenMaxExpirationTTL: 20 * time.Second,
ReplicationToken: "foobar",
},
Ports: &Ports{
HTTP: 20000,
RPC: 21000,
Serf: 22000,
},
Addresses: &Addresses{
HTTP: "127.0.0.2",
RPC: "127.0.0.2",
Serf: "127.0.0.2",
},
AdvertiseAddrs: &AdvertiseAddrs{
RPC: "127.0.0.2",
Serf: "127.0.0.2",
},
HTTPAPIResponseHeaders: map[string]string{
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
},
Vaults: []*config.VaultConfig{{
Name: structs.VaultDefaultCluster,
Addr: "2",
TLSCaFile: "2",
TLSCaPath: "2",
TLSCertFile: "2",
TLSKeyFile: "2",
TLSSkipVerify: &trueValue,
TLSServerName: "2",
ConnectionRetryIntv: time.Duration(30000000000),
JWTAuthBackendPath: "jwt",
}},
Consuls: []*config.ConsulConfig{{
Name: "default",
ServerServiceName: "2",
ClientServiceName: "2",
AutoAdvertise: &trueValue,
Addr: "2",
Timeout: 2 * time.Second,
Token: "2",
Auth: "2",
EnableSSL: &trueValue,
VerifySSL: &trueValue,
CAFile: "2",
CertFile: "2",
KeyFile: "2",
ServerAutoJoin: &trueValue,
ClientAutoJoin: &trueValue,
ChecksUseAdvertise: &trueValue,
ServerHTTPCheckName: "Nomad Server HTTP Check",
ServerSerfCheckName: "Nomad Server Serf Check",
ServerRPCCheckName: "Nomad Server RPC Check",
ClientHTTPCheckName: "Nomad Client HTTP Check",
ServiceIdentityAuthMethod: structs.ConsulWorkloadsDefaultAuthMethodName,
TaskIdentityAuthMethod: structs.ConsulWorkloadsDefaultAuthMethodName,
}},
Sentinel: &config.SentinelConfig{
Imports: []*config.SentinelImport{
{
Name: "foo",
Path: "foo",
Args: []string{"a", "b", "c"},
},
},
},
Autopilot: &config.AutopilotConfig{
CleanupDeadServers: &trueValue,
ServerStabilizationTime: 2 * time.Second,
LastContactThreshold: 2 * time.Second,
MaxTrailingLogs: 2,
MinQuorum: 2,
EnableRedundancyZones: &trueValue,
DisableUpgradeMigration: &trueValue,
EnableCustomUpgrades: &trueValue,
},
Plugins: []*config.PluginConfig{
{
Name: "docker",
Args: []string{"bam"},
Config: map[string]interface{}{
"baz": 2,
},
},
{
Name: "exec",
Args: []string{"arg"},
Config: map[string]interface{}{
"config": true,
},
},
},
Reporting: &config.ReportingConfig{
License: &config.LicenseReportingConfig{
Enabled: pointer.Of(true),
},
},
Eventlog: &Eventlog{
Enabled: true,
Level: "ERROR",
},
}
result := c0.Merge(c1)
result = result.Merge(c2)
result = result.Merge(c3)
expected := c3.Copy()
must.Eq(t, expected, result)
}
func TestConfig_ParseConfigFile(t *testing.T) {
ci.Parallel(t)
// Fails if the file doesn't exist
if _, err := ParseConfigFile("/unicorns/leprechauns"); err == nil {
t.Fatalf("expected error, got nothing")
}
fh, err := os.CreateTemp("", "nomad")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.RemoveAll(fh.Name())
// Invalid content returns error
if _, err := fh.WriteString("nope;!!!"); err != nil {
t.Fatalf("err: %s", err)
}
if _, err := ParseConfigFile(fh.Name()); err == nil {
t.Fatalf("expected load error, got nothing")
}
// Valid content parses successfully
if err := fh.Truncate(0); err != nil {
t.Fatalf("err: %s", err)
}
if _, err := fh.Seek(0, 0); err != nil {
t.Fatalf("err: %s", err)
}
if _, err := fh.WriteString(`{"region":"west"}`); err != nil {
t.Fatalf("err: %s", err)
}
config, err := ParseConfigFile(fh.Name())
if err != nil {
t.Fatalf("err: %s", err)
}
if config.Region != "west" {
t.Fatalf("bad region: %q", config.Region)
}
}
func TestConfig_LoadConfigDir(t *testing.T) {
ci.Parallel(t)
// Fails if the dir doesn't exist.
if _, err := LoadConfigDir("/unicorns/leprechauns"); err == nil {
t.Fatalf("expected error, got nothing")
}
dir := t.TempDir()
// Returns empty config on empty dir
config, err := LoadConfig(dir)
if err != nil {
t.Fatalf("err: %s", err)
}
if config == nil {
t.Fatalf("should not be nil")
}
file1 := filepath.Join(dir, "conf1.hcl")
err = os.WriteFile(file1, []byte(`{"region":"west"}`), 0600)
if err != nil {
t.Fatalf("err: %s", err)
}
file2 := filepath.Join(dir, "conf2.hcl")
err = os.WriteFile(file2, []byte(`{"datacenter":"sfo"}`), 0600)
if err != nil {
t.Fatalf("err: %s", err)
}
file3 := filepath.Join(dir, "conf3.hcl")
err = os.WriteFile(file3, []byte(`nope;!!!`), 0600)
if err != nil {
t.Fatalf("err: %s", err)
}
// Fails if we have a bad config file
if _, err := LoadConfigDir(dir); err == nil {
t.Fatalf("expected load error, got nothing")
}
if err := os.Remove(file3); err != nil {
t.Fatalf("err: %s", err)
}
// Works if configs are valid
config, err = LoadConfigDir(dir)
if err != nil {
t.Fatalf("err: %s", err)
}
if config.Region != "west" || config.Datacenter != "sfo" {
t.Fatalf("bad: %#v", config)
}
}
func TestConfig_LoadConfig(t *testing.T) {
ci.Parallel(t)
// Fails if the target doesn't exist
if _, err := LoadConfig("/unicorns/leprechauns"); err == nil {
t.Fatalf("expected error, got nothing")
}
fh, err := os.CreateTemp("", "nomad")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.Remove(fh.Name())
if _, err := fh.WriteString(`{"region":"west"}`); err != nil {
t.Fatalf("err: %s", err)
}
// Works on a config file
config, err := LoadConfig(fh.Name())
if err != nil {
t.Fatalf("err: %s", err)
}
if config.Region != "west" {
t.Fatalf("bad: %#v", config)
}
expectedConfigFiles := []string{fh.Name()}
if !reflect.DeepEqual(config.Files, expectedConfigFiles) {
t.Errorf("Loaded configs don't match\nExpected\n%+vGot\n%+v\n",
expectedConfigFiles, config.Files)
}
dir := t.TempDir()
file1 := filepath.Join(dir, "config1.hcl")
err = os.WriteFile(file1, []byte(`{"datacenter":"sfo"}`), 0600)
if err != nil {
t.Fatalf("err: %s", err)
}
// Works on config dir
config, err = LoadConfig(dir)
if err != nil {
t.Fatalf("err: %s", err)
}
if config.Datacenter != "sfo" {
t.Fatalf("bad: %#v", config)
}
expectedConfigFiles = []string{file1}
if !reflect.DeepEqual(config.Files, expectedConfigFiles) {
t.Errorf("Loaded configs don't match\nExpected\n%+vGot\n%+v\n",
expectedConfigFiles, config.Files)
}
}
func TestConfig_LoadConfigsFileOrder(t *testing.T) {
ci.Parallel(t)
config1, err := LoadConfigDir("test-resources/etcnomad")
if err != nil {
t.Fatalf("Failed to load config: %s", err)
}
config2, err := LoadConfig("test-resources/myconf")
if err != nil {
t.Fatalf("Failed to load config: %s", err)
}
expected := []string{
// filepath.FromSlash changes these to backslash \ on Windows
filepath.FromSlash("test-resources/etcnomad/common.hcl"),
filepath.FromSlash("test-resources/etcnomad/server.json"),
filepath.FromSlash("test-resources/myconf"),
}
config := config1.Merge(config2)
if !reflect.DeepEqual(config.Files, expected) {
t.Errorf("Loaded configs don't match\nwant: %+v\n got: %+v\n",
expected, config.Files)
}
}
func TestConfig_Listener(t *testing.T) {
ci.Parallel(t)
config := DefaultConfig()
// Fails on invalid input
if ln, err := config.Listener("tcp", "nope", 8080); err == nil {
ln.Close()
t.Fatalf("expected addr error")
}
if ln, err := config.Listener("nope", "127.0.0.1", 8080); err == nil {
ln.Close()
t.Fatalf("expected protocol err")
}
if ln, err := config.Listener("tcp", "127.0.0.1", -1); err == nil {
ln.Close()
t.Fatalf("expected port error")
}
// Works with valid inputs
ports := ci.PortAllocator.Grab(2)
ln, err := config.Listener("tcp", "127.0.0.1", ports[0])
if err != nil {
t.Fatalf("err: %s", err)
}
ln.Close()
if net := ln.Addr().Network(); net != "tcp" {
t.Fatalf("expected tcp, got: %q", net)
}
want := fmt.Sprintf("127.0.0.1:%d", ports[0])
if addr := ln.Addr().String(); addr != want {
t.Fatalf("expected %q, got: %q", want, addr)
}
// Falls back to default bind address if non provided
config.BindAddr = "0.0.0.0"
ln, err = config.Listener("tcp4", "", ports[1])
if err != nil {
t.Fatalf("err: %s", err)
}
ln.Close()
want = fmt.Sprintf("0.0.0.0:%d", ports[1])
if addr := ln.Addr().String(); addr != want {
t.Fatalf("expected %q, got: %q", want, addr)
}
}
func TestConfig_DevMode_validate(t *testing.T) {
ci.Parallel(t)
cases := []struct {
devConfig *devModeConfig
expectedErr string
}{}
if runtime.GOOS != "linux" {
cases = []struct {
devConfig *devModeConfig
expectedErr string
}{
{
devConfig: &devModeConfig{
connectMode: true,
},
expectedErr: "-dev-connect is only supported on linux",
},
{
devConfig: &devModeConfig{
defaultMode: true,
connectMode: true,
},
expectedErr: "-dev-connect is only supported on linux",
},
}
}
if runtime.GOOS == "linux" {
testutil.RequireRoot(t)
cases = []struct {
devConfig *devModeConfig
expectedErr string
}{
{
devConfig: &devModeConfig{
connectMode: true,
},
expectedErr: "",
},
{
devConfig: &devModeConfig{
defaultMode: true,
connectMode: true,
},
expectedErr: "",
},
}
}
for _, c := range cases {
t.Run("", func(t *testing.T) {
err := c.devConfig.validate()
if c.expectedErr != "" {
must.Error(t, err)
} else {
must.NoError(t, err)
}
})
}
}
// TestConfig_normalizeAddrs_DevMode asserts that normalizeAddrs allows
// advertising localhost in dev mode.
func TestConfig_normalizeAddrs_DevMode(t *testing.T) {
ci.Parallel(t)
// allow to advertise 127.0.0.1 if dev-mode is enabled
c := &Config{
BindAddr: "127.0.0.1",
Ports: &Ports{
HTTP: 4646,
RPC: 4647,
Serf: 4648,
},
Addresses: &Addresses{},
AdvertiseAddrs: &AdvertiseAddrs{},
DevMode: true,
}
if err := c.normalizeAddrs(); err != nil {
t.Fatalf("unable to normalize addresses: %s", err)
}
if c.BindAddr != "127.0.0.1" {
t.Fatalf("expected BindAddr 127.0.0.1, got %s", c.BindAddr)
}
if c.normalizedAddrs.HTTP[0] != "127.0.0.1:4646" {
t.Fatalf("expected HTTP address 127.0.0.1:4646, got %s", c.normalizedAddrs.HTTP)
}
if c.normalizedAddrs.RPC != "127.0.0.1:4647" {
t.Fatalf("expected RPC address 127.0.0.1:4647, got %s", c.normalizedAddrs.RPC)
}
if c.normalizedAddrs.Serf != "127.0.0.1:4648" {
t.Fatalf("expected Serf address 127.0.0.1:4648, got %s", c.normalizedAddrs.Serf)
}
if c.AdvertiseAddrs.HTTP != "127.0.0.1:4646" {
t.Fatalf("expected HTTP advertise address 127.0.0.1:4646, got %s", c.AdvertiseAddrs.HTTP)
}
if c.AdvertiseAddrs.RPC != "127.0.0.1:4647" {
t.Fatalf("expected RPC advertise address 127.0.0.1:4647, got %s", c.AdvertiseAddrs.RPC)
}
// Client mode, no Serf address defined
if c.AdvertiseAddrs.Serf != "" {
t.Fatalf("expected unset Serf advertise address, got %s", c.AdvertiseAddrs.Serf)
}
}
// TestConfig_normalizeAddrs_NoAdvertise asserts that normalizeAddrs will
// fail if no valid advertise address available in non-dev mode.
func TestConfig_normalizeAddrs_NoAdvertise(t *testing.T) {
ci.Parallel(t)
c := &Config{
BindAddr: "127.0.0.1",
Ports: &Ports{
HTTP: 4646,
RPC: 4647,
Serf: 4648,
},
Addresses: &Addresses{},
AdvertiseAddrs: &AdvertiseAddrs{},
DevMode: false,
}
if err := c.normalizeAddrs(); err == nil {
t.Fatalf("expected an error when no valid advertise address is available")
}
if c.AdvertiseAddrs.HTTP == "127.0.0.1:4646" {
t.Fatalf("expected non-localhost HTTP advertise address, got %s", c.AdvertiseAddrs.HTTP)
}
if c.AdvertiseAddrs.RPC == "127.0.0.1:4647" {
t.Fatalf("expected non-localhost RPC advertise address, got %s", c.AdvertiseAddrs.RPC)
}
if c.AdvertiseAddrs.Serf == "127.0.0.1:4648" {
t.Fatalf("expected non-localhost Serf advertise address, got %s", c.AdvertiseAddrs.Serf)
}
}
// TestConfig_normalizeAddrs_AdvertiseLocalhost asserts localhost can be
// advertised if it's explicitly set in the config.
func TestConfig_normalizeAddrs_AdvertiseLocalhost(t *testing.T) {
ci.Parallel(t)
c := &Config{
BindAddr: "127.0.0.1",
Ports: &Ports{
HTTP: 4646,
RPC: 4647,
Serf: 4648,
},
Addresses: &Addresses{},
AdvertiseAddrs: &AdvertiseAddrs{
HTTP: "127.0.0.1",
RPC: "127.0.0.1",
Serf: "127.0.0.1",
},
DevMode: false,
Server: &ServerConfig{Enabled: true},
}
if err := c.normalizeAddrs(); err != nil {
t.Fatalf("unexpected error when manually setting bind mode: %v", err)
}
if c.AdvertiseAddrs.HTTP != "127.0.0.1:4646" {
t.Errorf("expected localhost HTTP advertise address, got %s", c.AdvertiseAddrs.HTTP)
}
if c.AdvertiseAddrs.RPC != "127.0.0.1:4647" {
t.Errorf("expected localhost RPC advertise address, got %s", c.AdvertiseAddrs.RPC)
}
if c.AdvertiseAddrs.Serf != "127.0.0.1:4648" {
t.Errorf("expected localhost Serf advertise address, got %s", c.AdvertiseAddrs.Serf)
}
}
// TestConfig_normalizeAddrs_IPv6Loopback asserts that an IPv6 loopback address
// is normalized properly. See #2739
func TestConfig_normalizeAddrs_IPv6Loopback(t *testing.T) {
ci.Parallel(t)
c := &Config{
BindAddr: "::1",
Ports: &Ports{
HTTP: 4646,
RPC: 4647,
},
Addresses: &Addresses{},
AdvertiseAddrs: &AdvertiseAddrs{
HTTP: "::1",
RPC: "::1",
},
DevMode: false,
}
if err := c.normalizeAddrs(); err != nil {
t.Fatalf("unexpected error when manually setting bind mode: %v", err)
}
if c.Addresses.HTTP != "::1" {
t.Errorf("expected ::1 HTTP address, got %s", c.Addresses.HTTP)
}
if c.Addresses.RPC != "::1" {
t.Errorf("expected ::1 RPC address, got %s", c.Addresses.RPC)
}
if c.AdvertiseAddrs.HTTP != "[::1]:4646" {
t.Errorf("expected [::1] HTTP advertise address, got %s", c.AdvertiseAddrs.HTTP)
}
if c.AdvertiseAddrs.RPC != "[::1]:4647" {
t.Errorf("expected [::1] RPC advertise address, got %s", c.AdvertiseAddrs.RPC)
}
}
// TestConfig_normalizeAddrs_IPv6 asserts that bind and advertise addrs conform
// to RFC 5942 §4: https://www.rfc-editor.org/rfc/rfc5942.html#section-4
// Full coverage is provided by tests for ipaddr.NormalizeAddr
func TestConfig_normalizeAddrs_IPv6(t *testing.T) {
c := &Config{
Addresses: &Addresses{},
BindAddr: "0:0::1F",
Ports: &Ports{
HTTP: 4646,
RPC: 4647,
},
AdvertiseAddrs: &AdvertiseAddrs{
HTTP: "[A110::0:0:C8]:8080",
RPC: "0:00FA:0:0:0::CE",
},
DevMode: false,
}
must.NoError(t, c.normalizeAddrs())
test.Eq(t, "::1f", c.Addresses.HTTP, test.Sprint("bind HTTP"))
test.Eq(t, "::1f", c.Addresses.RPC, test.Sprint("bind RPC"))
test.Eq(t, []string{"[::1f]:4646"}, c.normalizedAddrs.HTTP, test.Sprint("normalized HTTP"))
test.Eq(t, "[::1f]:4647", c.normalizedAddrs.RPC, test.Sprint("normalized RPC"))
test.Eq(t, "[a110::c8]:8080", c.AdvertiseAddrs.HTTP, test.Sprint("advertise HTTP"))
test.Eq(t, "[0:fa::ce]:4647", c.AdvertiseAddrs.RPC, test.Sprint("advertise RPC"))
}
// TestConfig_normalizeAddrs_MultipleInterface asserts that normalizeAddrs will
// handle normalizing multiple interfaces in a single protocol.
func TestConfig_normalizeAddrs_MultipleInterfaces(t *testing.T) {
ci.Parallel(t)
testCases := []struct {
name string
addressConfig *Addresses
expectedNormalizedAddrs *NormalizedAddrs
expectErr bool
}{
{
name: "multiple http addresses",
addressConfig: &Addresses{
HTTP: "127.0.0.1 127.0.0.2",
},
expectedNormalizedAddrs: &NormalizedAddrs{
HTTP: []string{"127.0.0.1:4646", "127.0.0.2:4646"},
RPC: "127.0.0.1:4647",
Serf: "127.0.0.1:4648",
},
expectErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c := &Config{
BindAddr: "127.0.0.1",
Ports: &Ports{
HTTP: 4646,
RPC: 4647,
Serf: 4648,
},
Addresses: tc.addressConfig,
AdvertiseAddrs: &AdvertiseAddrs{
HTTP: "127.0.0.1",
RPC: "127.0.0.1",
Serf: "127.0.0.1",
},
}
err := c.normalizeAddrs()
if tc.expectErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.expectedNormalizedAddrs, c.normalizedAddrs)
})
}
}
func TestConfig_normalizeAddrs(t *testing.T) {
ci.Parallel(t)
c := &Config{
BindAddr: "169.254.1.5",
Ports: &Ports{
HTTP: 4646,
RPC: 4647,
Serf: 4648,
},
Addresses: &Addresses{
HTTP: "169.254.1.10",
},
AdvertiseAddrs: &AdvertiseAddrs{
RPC: "169.254.1.40",
},
Server: &ServerConfig{
Enabled: true,
},
}
if err := c.normalizeAddrs(); err != nil {
t.Fatalf("unable to normalize addresses: %s", err)
}
if c.BindAddr != "169.254.1.5" {
t.Fatalf("expected BindAddr 169.254.1.5, got %s", c.BindAddr)
}
if c.AdvertiseAddrs.HTTP != "169.254.1.10:4646" {
t.Fatalf("expected HTTP advertise address 169.254.1.10:4646, got %s", c.AdvertiseAddrs.HTTP)
}
if c.AdvertiseAddrs.RPC != "169.254.1.40:4647" {
t.Fatalf("expected RPC advertise address 169.254.1.40:4647, got %s", c.AdvertiseAddrs.RPC)
}
if c.AdvertiseAddrs.Serf != "169.254.1.5:4648" {
t.Fatalf("expected Serf advertise address 169.254.1.5:4648, got %s", c.AdvertiseAddrs.Serf)
}
c = &Config{
BindAddr: "{{ GetPrivateIP }}",
Ports: &Ports{
HTTP: 4646,
RPC: 4647,
Serf: 4648,
},
Addresses: &Addresses{},
AdvertiseAddrs: &AdvertiseAddrs{
RPC: "{{ GetPrivateIP }}",
},
Server: &ServerConfig{
Enabled: true,
},
}
if err := c.normalizeAddrs(); err != nil {
t.Fatalf("unable to normalize addresses: %s", err)
}
exp := net.JoinHostPort(c.BindAddr, "4646")
if c.AdvertiseAddrs.HTTP != exp {
t.Fatalf("expected HTTP advertise address %s, got %s", exp, c.AdvertiseAddrs.HTTP)
}
exp = net.JoinHostPort(c.BindAddr, "4647")
if c.AdvertiseAddrs.RPC != exp {
t.Fatalf("expected RPC advertise address %s, got %s", exp, c.AdvertiseAddrs.RPC)
}
exp = net.JoinHostPort(c.BindAddr, "4648")
if c.AdvertiseAddrs.Serf != exp {
t.Fatalf("expected Serf advertise address %s, got %s", exp, c.AdvertiseAddrs.Serf)
}
// allow to advertise 127.0.0.1 in non-dev mode, if explicitly configured to do so
c = &Config{
BindAddr: "127.0.0.1",
Ports: &Ports{
HTTP: 4646,
RPC: 4647,
Serf: 4648,
},
Addresses: &Addresses{},
AdvertiseAddrs: &AdvertiseAddrs{
HTTP: "127.0.0.1:4646",
RPC: "127.0.0.1:4647",
Serf: "127.0.0.1:4648",
},
DevMode: false,
Server: &ServerConfig{
Enabled: true,
},
}
if err := c.normalizeAddrs(); err != nil {
t.Fatalf("unable to normalize addresses: %s", err)
}
if c.AdvertiseAddrs.HTTP != "127.0.0.1:4646" {
t.Fatalf("expected HTTP advertise address 127.0.0.1:4646, got %s", c.AdvertiseAddrs.HTTP)
}
if c.AdvertiseAddrs.RPC != "127.0.0.1:4647" {
t.Fatalf("expected RPC advertise address 127.0.0.1:4647, got %s", c.AdvertiseAddrs.RPC)
}
if c.AdvertiseAddrs.RPC != "127.0.0.1:4647" {
t.Fatalf("expected RPC advertise address 127.0.0.1:4647, got %s", c.AdvertiseAddrs.RPC)
}
}
func TestConfig_templateNetworkInterface(t *testing.T) {
ci.Parallel(t)
// find the first interface
ifaces, err := sockaddr.GetAllInterfaces()
if err != nil {
t.Fatalf("failed to get interfaces: %v", err)
}
iface := ifaces[0]
testCases := []struct {
name string
clientConfig *ClientConfig
expectedInterface string
expectErr bool
}{
{
name: "empty string",
clientConfig: &ClientConfig{
Enabled: true,
NetworkInterface: "",
},
expectedInterface: "",
expectErr: false,
},
{
name: "simple string",
clientConfig: &ClientConfig{
Enabled: true,
NetworkInterface: iface.Name,
},
expectedInterface: iface.Name,
expectErr: false,
},
{
name: "valid interface",
clientConfig: &ClientConfig{
Enabled: true,
NetworkInterface: `{{ GetAllInterfaces | attr "name" }}`,
},
expectedInterface: iface.Name,
expectErr: false,
},
{
name: "invalid interface",
clientConfig: &ClientConfig{
Enabled: true,
NetworkInterface: `no such interface`,
},
expectedInterface: iface.Name,
expectErr: true,
},
{
name: "insignificant whitespace",
clientConfig: &ClientConfig{
Enabled: true,
NetworkInterface: ` {{GetAllInterfaces | attr "name" }}`,
},
expectedInterface: iface.Name,
expectErr: false,
},
{
name: "empty template return",
clientConfig: &ClientConfig{
Enabled: true,
NetworkInterface: `{{ printf "" }}`,
},
expectedInterface: iface.Name,
expectErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
c := &Config{
BindAddr: "127.0.0.1",
Ports: &Ports{
HTTP: 4646,
RPC: 4647,
Serf: 4648,
},
Addresses: &Addresses{},
AdvertiseAddrs: &AdvertiseAddrs{
HTTP: "127.0.0.1:4646",
RPC: "127.0.0.1:4647",
Serf: "127.0.0.1:4648",
},
DevMode: false,
Client: tc.clientConfig,
}
err := c.normalizeAddrs()
if tc.expectErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, c.Client.NetworkInterface, tc.expectedInterface)
})
}
}
func TestIsMissingPort(t *testing.T) {
ci.Parallel(t)
_, _, err := net.SplitHostPort("localhost")
if missing := isMissingPort(err); !missing {
t.Errorf("expected missing port error, but got %v", err)
}
_, _, err = net.SplitHostPort("localhost:9000")
if missing := isMissingPort(err); missing {
t.Errorf("expected no error, but got %v", err)
}
}
func TestClientIntroduction_Copy(t *testing.T) {
ci.Parallel(t)
clientIntro := &ClientIntroduction{
Enforcement: "warn",
DefaultIdentityTTL: 5 * time.Minute,
MaxIdentityTTL: 30 * time.Minute,
}
copiedClientIntro := clientIntro.Copy()
// Ensure the copied object contains the same values, but the underlying
// pointer address is different.
must.Eq(t, clientIntro, copiedClientIntro)
must.NotEq(t, fmt.Sprintf("%p", clientIntro), fmt.Sprintf("%p", copiedClientIntro))
}
func TestClientIntroduction_Merge(t *testing.T) {
ci.Parallel(t)
clientIntro1 := &ClientIntroduction{
Enforcement: "warn",
DefaultIdentityTTL: 5 * time.Minute,
MaxIdentityTTL: 30 * time.Minute,
ExtraKeysHCL: []string{"key1", "key2"},
}
clientIntro2 := &ClientIntroduction{
Enforcement: "strict",
DefaultIdentityTTL: 30 * time.Minute,
MaxIdentityTTL: 60 * time.Minute,
ExtraKeysHCL: []string{"key3", "key4"},
}
expectedClientIntro := &ClientIntroduction{
Enforcement: "strict",
DefaultIdentityTTL: 30 * time.Minute,
MaxIdentityTTL: 60 * time.Minute,
ExtraKeysHCL: []string{"key1", "key2", "key3", "key4"},
}
must.Eq(t, expectedClientIntro, clientIntro1.Merge(clientIntro2))
}
func TestClientIntroduction_Validate(t *testing.T) {
ci.Parallel(t)
testCases := []struct {
name string
inputClientIntroduction *ClientIntroduction
expectedError bool
}{
{
name: "nil block",
inputClientIntroduction: nil,
expectedError: false,
},
{
name: "empty enforcement",
inputClientIntroduction: &ClientIntroduction{
Enforcement: "",
},
expectedError: true,
},
{
name: "invalid enforcement",
inputClientIntroduction: &ClientIntroduction{
Enforcement: "nuclear",
},
expectedError: true,
},
{
name: "invalid default_identity_ttl",
inputClientIntroduction: &ClientIntroduction{
Enforcement: "warn",
DefaultIdentityTTL: 0,
},
expectedError: true,
},
{
name: "invalid max_identity_ttl",
inputClientIntroduction: &ClientIntroduction{
Enforcement: "warn",
DefaultIdentityTTL: 5 * time.Minute,
MaxIdentityTTL: 0,
},
expectedError: true,
},
{
name: "invalid ttl combination",
inputClientIntroduction: &ClientIntroduction{
Enforcement: "warn",
DefaultIdentityTTL: 5 * time.Minute,
MaxIdentityTTL: 4 * time.Minute,
},
expectedError: true,
},
{
name: "valid",
inputClientIntroduction: &ClientIntroduction{
Enforcement: "warn",
DefaultIdentityTTL: 5 * time.Minute,
MaxIdentityTTL: 30 * time.Minute,
},
expectedError: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actualOutput := tc.inputClientIntroduction.Validate()
if tc.expectedError {
must.Error(t, actualOutput)
} else {
must.NoError(t, actualOutput)
}
})
}
}
func TestMergeServerJoin(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
{
retryJoin := []string{"127.0.0.1", "127.0.0.2"}
startJoin := []string{"127.0.0.1", "127.0.0.2"}
retryMaxAttempts := 1
retryInterval := time.Duration(0)
a := &ServerJoin{
RetryJoin: retryJoin,
StartJoin: startJoin,
RetryMaxAttempts: retryMaxAttempts,
RetryInterval: time.Duration(retryInterval),
}
b := &ServerJoin{}
result := a.Merge(b)
require.Equal(result.RetryJoin, retryJoin)
require.Equal(result.StartJoin, startJoin)
require.Equal(result.RetryMaxAttempts, retryMaxAttempts)
require.Equal(result.RetryInterval, retryInterval)
}
{
retryJoin := []string{"127.0.0.1", "127.0.0.2"}
startJoin := []string{"127.0.0.1", "127.0.0.2"}
retryMaxAttempts := 1
retryInterval := time.Duration(0)
a := &ServerJoin{}
b := &ServerJoin{
RetryJoin: retryJoin,
StartJoin: startJoin,
RetryMaxAttempts: retryMaxAttempts,
RetryInterval: time.Duration(retryInterval),
}
result := a.Merge(b)
require.Equal(result.RetryJoin, retryJoin)
require.Equal(result.StartJoin, startJoin)
require.Equal(result.RetryMaxAttempts, retryMaxAttempts)
require.Equal(result.RetryInterval, retryInterval)
}
{
retryJoin := []string{"127.0.0.1", "127.0.0.2"}
startJoin := []string{"127.0.0.1", "127.0.0.2"}
retryMaxAttempts := 1
retryInterval := time.Duration(0)
var a *ServerJoin
b := &ServerJoin{
RetryJoin: retryJoin,
StartJoin: startJoin,
RetryMaxAttempts: retryMaxAttempts,
RetryInterval: time.Duration(retryInterval),
}
result := a.Merge(b)
require.Equal(result.RetryJoin, retryJoin)
require.Equal(result.StartJoin, startJoin)
require.Equal(result.RetryMaxAttempts, retryMaxAttempts)
require.Equal(result.RetryInterval, retryInterval)
}
{
retryJoin := []string{"127.0.0.1", "127.0.0.2"}
startJoin := []string{"127.0.0.1", "127.0.0.2"}
retryMaxAttempts := 1
retryInterval := time.Duration(0)
a := &ServerJoin{
RetryJoin: retryJoin,
StartJoin: startJoin,
RetryMaxAttempts: retryMaxAttempts,
RetryInterval: time.Duration(retryInterval),
}
var b *ServerJoin
result := a.Merge(b)
require.Equal(result.RetryJoin, retryJoin)
require.Equal(result.StartJoin, startJoin)
require.Equal(result.RetryMaxAttempts, retryMaxAttempts)
require.Equal(result.RetryInterval, retryInterval)
}
{
retryJoin := []string{"127.0.0.1", "127.0.0.2"}
startJoin := []string{"127.0.0.1", "127.0.0.2"}
retryMaxAttempts := 1
retryInterval := time.Duration(0)
a := &ServerJoin{
RetryJoin: retryJoin,
StartJoin: startJoin,
}
b := &ServerJoin{
RetryMaxAttempts: retryMaxAttempts,
RetryInterval: time.Duration(retryInterval),
}
result := a.Merge(b)
require.Equal(result.RetryJoin, retryJoin)
require.Equal(result.StartJoin, startJoin)
require.Equal(result.RetryMaxAttempts, retryMaxAttempts)
require.Equal(result.RetryInterval, retryInterval)
}
}
func TestTelemetry_PrefixFilters(t *testing.T) {
ci.Parallel(t)
cases := []struct {
in []string
expAllow []string
expBlock []string
expErr bool
}{
{
in: []string{"+foo"},
expAllow: []string{"foo"},
},
{
in: []string{"-foo"},
expBlock: []string{"foo"},
},
{
in: []string{"+a.b.c", "-x.y.z"},
expAllow: []string{"a.b.c"},
expBlock: []string{"x.y.z"},
},
{
in: []string{"+foo", "bad", "-bar"},
expErr: true,
},
}
for i, c := range cases {
t.Run(fmt.Sprintf("PrefixCase%d", i), func(t *testing.T) {
require := require.New(t)
tel := &Telemetry{
PrefixFilter: c.in,
}
allow, block, err := tel.PrefixFilters()
require.Exactly(c.expAllow, allow)
require.Exactly(c.expBlock, block)
require.Equal(c.expErr, err != nil)
})
}
}
func TestTelemetry_Validate(t *testing.T) {
ci.Parallel(t)
testCases := []struct {
name string
inputTelemetry *Telemetry
expectedError error
}{
{
name: "nil",
inputTelemetry: nil,
expectedError: nil,
},
{
name: "invalid",
inputTelemetry: &Telemetry{
inMemoryCollectionInterval: 10 * time.Second,
inMemoryRetentionPeriod: 1 * time.Second,
},
expectedError: errors.New("telemetry in-memory collection interval cannot be greater than retention period"),
},
{
name: "valid",
inputTelemetry: &Telemetry{
inMemoryCollectionInterval: 1 * time.Second,
inMemoryRetentionPeriod: 10 * time.Second,
},
expectedError: nil,
},
{
name: "missing in-memory interval",
inputTelemetry: &Telemetry{
inMemoryRetentionPeriod: 10 * time.Second,
},
expectedError: errors.New("telemetry in-memory collection interval must be greater than zero"),
},
{
name: "missing in-memory collection",
inputTelemetry: &Telemetry{
inMemoryCollectionInterval: 10 * time.Second,
},
expectedError: errors.New("telemetry in-memory retention period must be greater than zero"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actualError := tc.inputTelemetry.Validate()
if tc.expectedError != nil {
must.EqError(t, actualError, tc.expectedError.Error())
} else {
must.NoError(t, actualError)
}
})
}
}
func TestTelemetry_Parse(t *testing.T) {
ci.Parallel(t)
dir := t.TempDir()
file1 := filepath.Join(dir, "config1.hcl")
err := os.WriteFile(file1, []byte(`telemetry{
prefix_filter = ["+nomad.raft"]
filter_default = false
disable_dispatched_job_summary_metrics = true
disable_quota_utilization_metrics = true
disable_rpc_rate_metrics_labels = true
}`), 0600)
must.NoError(t, err)
// Works on config dir
config, err := LoadConfig(dir)
must.NoError(t, err)
must.False(t, *config.Telemetry.FilterDefault)
must.Eq(t, []string{"+nomad.raft"}, config.Telemetry.PrefixFilter)
must.True(t, config.Telemetry.DisableDispatchedJobSummaryMetrics)
must.True(t, config.Telemetry.DisableQuotaUtilizationMetrics)
must.True(t, config.Telemetry.DisableRPCRateMetricsLabels)
}
func TestEventBroker_Parse(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
{
a := &ServerConfig{
EnableEventBroker: pointer.Of(false),
EventBufferSize: pointer.Of(0),
}
b := DefaultConfig().Server
b.EnableEventBroker = nil
b.EventBufferSize = nil
result := a.Merge(b)
require.Equal(false, *result.EnableEventBroker)
require.Equal(0, *result.EventBufferSize)
}
{
a := &ServerConfig{
EnableEventBroker: pointer.Of(true),
EventBufferSize: pointer.Of(5000),
}
b := DefaultConfig().Server
b.EnableEventBroker = nil
b.EventBufferSize = nil
result := a.Merge(b)
require.Equal(true, *result.EnableEventBroker)
require.Equal(5000, *result.EventBufferSize)
}
{
a := &ServerConfig{
EnableEventBroker: pointer.Of(false),
EventBufferSize: pointer.Of(0),
}
b := DefaultConfig().Server
b.EnableEventBroker = pointer.Of(true)
b.EventBufferSize = pointer.Of(20000)
result := a.Merge(b)
require.Equal(true, *result.EnableEventBroker)
require.Equal(20000, *result.EventBufferSize)
}
}
func TestConfig_LoadConsulTemplateConfig(t *testing.T) {
ci.Parallel(t)
t.Run("minimal client expect defaults", func(t *testing.T) {
defaultConfig := DefaultConfig()
agentConfig, err := LoadConfig("test-resources/minimal_client.hcl")
must.NoError(t, err)
agentConfig = defaultConfig.Merge(agentConfig)
must.Eq(t, defaultConfig.Client.TemplateConfig, agentConfig.Client.TemplateConfig)
})
t.Run("client config with nil function denylist", func(t *testing.T) {
defaultConfig := DefaultConfig()
agentConfig, err := LoadConfig("test-resources/client_with_function_denylist_nil.hcl")
must.NoError(t, err)
agentConfig = defaultConfig.Merge(agentConfig)
templateConfig := agentConfig.Client.TemplateConfig
must.Len(t, 3, templateConfig.FunctionDenylist)
})
t.Run("client config with basic template", func(t *testing.T) {
defaultConfig := DefaultConfig()
agentConfig, err := LoadConfig("test-resources/client_with_basic_template.hcl")
must.NoError(t, err)
agentConfig = defaultConfig.Merge(agentConfig)
templateConfig := agentConfig.Client.TemplateConfig
// check explicit overrides
must.Eq(t, true, templateConfig.DisableSandbox)
must.Len(t, 0, templateConfig.FunctionDenylist)
// check all the complex defaults
must.Eq(t, 87600*time.Hour, *templateConfig.MaxStale)
must.Eq(t, 5*time.Minute, *templateConfig.BlockQueryWaitTime)
// Wait
must.NotNil(t, templateConfig.Wait)
must.Eq(t, 5*time.Second, *templateConfig.Wait.Min)
must.Eq(t, 4*time.Minute, *templateConfig.Wait.Max)
// WaitBounds
must.Nil(t, templateConfig.WaitBounds)
// Consul Retry
must.NotNil(t, templateConfig.ConsulRetry)
must.Eq(t, 12, *templateConfig.ConsulRetry.Attempts)
must.Eq(t, time.Millisecond*250, *templateConfig.ConsulRetry.Backoff)
must.Eq(t, time.Minute, *templateConfig.ConsulRetry.MaxBackoff)
// Vault Retry
must.NotNil(t, templateConfig.VaultRetry)
must.Eq(t, 12, *templateConfig.VaultRetry.Attempts)
must.Eq(t, time.Millisecond*250, *templateConfig.VaultRetry.Backoff)
must.Eq(t, time.Minute, *templateConfig.VaultRetry.MaxBackoff)
// Nomad Retry
must.NotNil(t, templateConfig.NomadRetry)
must.Eq(t, 12, *templateConfig.NomadRetry.Attempts)
must.Eq(t, time.Millisecond*250, *templateConfig.NomadRetry.Backoff)
must.Eq(t, time.Minute, *templateConfig.NomadRetry.MaxBackoff)
})
t.Run("client config with full template block", func(t *testing.T) {
defaultConfig := DefaultConfig()
agentConfig, err := LoadConfig("test-resources/client_with_template.hcl")
must.NoError(t, err)
agentConfig = defaultConfig.Merge(agentConfig)
clientAgent := Agent{config: agentConfig}
clientConfig, err := clientAgent.clientConfig()
must.NoError(t, err)
templateConfig := clientConfig.TemplateConfig
// Make sure all fields to test are set
must.NotNil(t, templateConfig.BlockQueryWaitTime)
must.NotNil(t, templateConfig.MaxStale)
must.NotNil(t, templateConfig.Wait)
must.NotNil(t, templateConfig.WaitBounds)
must.NotNil(t, templateConfig.ConsulRetry)
must.NotNil(t, templateConfig.VaultRetry)
must.NotNil(t, templateConfig.NomadRetry)
// Direct properties
must.Eq(t, 300*time.Second, *templateConfig.MaxStale)
must.Eq(t, 90*time.Second, *templateConfig.BlockQueryWaitTime)
// Wait
must.Eq(t, 2*time.Second, *templateConfig.Wait.Min)
must.Eq(t, 60*time.Second, *templateConfig.Wait.Max)
// WaitBounds
must.Eq(t, 2*time.Second, *templateConfig.WaitBounds.Min)
must.Eq(t, 60*time.Second, *templateConfig.WaitBounds.Max)
// Consul Retry
must.NotNil(t, templateConfig.ConsulRetry)
must.Eq(t, 5, *templateConfig.ConsulRetry.Attempts)
must.Eq(t, 5*time.Second, *templateConfig.ConsulRetry.Backoff)
must.Eq(t, 10*time.Second, *templateConfig.ConsulRetry.MaxBackoff)
// Vault Retry
must.NotNil(t, templateConfig.VaultRetry)
must.Eq(t, 0, *templateConfig.VaultRetry.Attempts)
must.Eq(t, 15*time.Second, *templateConfig.VaultRetry.Backoff)
must.Eq(t, 20*time.Second, *templateConfig.VaultRetry.MaxBackoff)
// Nomad Retry
must.NotNil(t, templateConfig.NomadRetry)
must.Eq(t, 12, *templateConfig.NomadRetry.Attempts)
must.Eq(t, 20*time.Second, *templateConfig.NomadRetry.Backoff)
must.Eq(t, 25*time.Second, *templateConfig.NomadRetry.MaxBackoff)
})
}
func TestConfig_LoadConsulTemplate_FunctionDenylist(t *testing.T) {
cases := []struct {
File string
Expected *client.ClientTemplateConfig
}{
{
"test-resources/minimal_client.hcl",
nil,
},
{
"test-resources/client_with_basic_template.json",
&client.ClientTemplateConfig{
DisableSandbox: true,
FunctionDenylist: []string{},
},
},
{
"test-resources/client_with_basic_template.hcl",
&client.ClientTemplateConfig{
DisableSandbox: true,
FunctionDenylist: []string{},
},
},
{
"test-resources/client_with_function_denylist.hcl",
&client.ClientTemplateConfig{
DisableSandbox: false,
FunctionDenylist: []string{"foo"},
},
},
{
"test-resources/client_with_function_denylist_empty.hcl",
&client.ClientTemplateConfig{
DisableSandbox: false,
FunctionDenylist: []string{},
},
},
{
"test-resources/client_with_function_denylist_empty_string.hcl",
&client.ClientTemplateConfig{
DisableSandbox: true,
FunctionDenylist: []string{""},
},
},
{
"test-resources/client_with_function_denylist_empty_string.json",
&client.ClientTemplateConfig{
DisableSandbox: true,
FunctionDenylist: []string{""},
},
},
{
"test-resources/client_with_function_denylist_nil.hcl",
&client.ClientTemplateConfig{
DisableSandbox: true,
},
},
{
"test-resources/client_with_empty_template.hcl",
nil,
},
}
for _, tc := range cases {
t.Run(tc.File, func(t *testing.T) {
agentConfig, err := LoadConfig(tc.File)
require.NoError(t, err)
templateConfig := agentConfig.Client.TemplateConfig
require.Equal(t, tc.Expected, templateConfig)
})
}
}
func TestParseMultipleIPTemplates(t *testing.T) {
ci.Parallel(t)
testCases := []struct {
name string
tmpl string
expectedOut []string
expectErr bool
}{
{
name: "deduplicates same ip and preserves order",
tmpl: "127.0.0.1 10.0.0.1 127.0.0.1",
expectedOut: []string{"127.0.0.1", "10.0.0.1"},
expectErr: false,
},
{
name: "includes sockaddr expression",
tmpl: "10.0.0.1 {{ GetAllInterfaces | include \"flags\" \"loopback\" | limit 1 | attr \"address\" }} 10.0.0.2",
expectedOut: []string{"10.0.0.1", "127.0.0.1", "10.0.0.2"},
expectErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
out, err := parseMultipleIPTemplate(tc.tmpl)
require.NoError(t, err)
require.Equal(t, tc.expectedOut, out)
})
}
}
// this test makes sure Consul configs with and without WI merging happens
// correctly; here to assure we don't introduce regressions
func Test_mergeConsulConfigs(t *testing.T) {
ci.Parallel(t)
c0 := &Config{
Consuls: []*config.ConsulConfig{
{
Token: "foo",
},
},
}
c1 := &Config{
Consuls: []*config.ConsulConfig{
{
ServiceIdentity: &config.WorkloadIdentityConfig{
Audience: []string{"consul.io"},
TTL: pointer.Of(time.Hour),
},
TaskIdentity: &config.WorkloadIdentityConfig{
Audience: []string{"consul.io"},
TTL: pointer.Of(time.Hour),
},
},
},
}
result := c0.Merge(c1)
must.Eq(t, c1.Consuls[0].ServiceIdentity, result.Consuls[0].ServiceIdentity)
must.Eq(t, c1.Consuls[0].TaskIdentity, result.Consuls[0].TaskIdentity)
must.Eq(t, c0.Consuls[0].Token, result.Consuls[0].Token)
}
func Test_mergeKEKProviderConfigs(t *testing.T) {
ci.Parallel(t)
left := []*structs.KEKProviderConfig{
{
// incomplete config with name
Provider: "awskms",
Name: "foo",
Active: true,
Config: map[string]string{
"region": "us-east-1",
"access_key": "AKIAIOSFODNN7EXAMPLE",
},
},
{
// empty config
Provider: "aead",
},
}
right := []*structs.KEKProviderConfig{
{
// same awskms.foo provider with fields to merge
Provider: "awskms",
Name: "foo",
Active: false,
Config: map[string]string{
"access_key": "AKIAIOSXABCD7EXAMPLE",
"secret_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"kms_key_id": "19ec80b0-dfdd-4d97-8164-c6examplekey",
},
},
{
// same awskms provider, different name
Provider: "awskms",
Name: "bar",
Active: false,
Config: map[string]string{
"region": "us-east-1",
"access_key": "AKIAIOSFODNN7EXAMPLE",
},
},
}
result := mergeKEKProviderConfigs(left, right)
must.Eq(t, []*structs.KEKProviderConfig{
{
Provider: "aead",
},
{
Provider: "awskms",
Name: "bar",
Active: false,
Config: map[string]string{
"region": "us-east-1",
"access_key": "AKIAIOSFODNN7EXAMPLE",
},
},
{
Provider: "awskms",
Name: "foo",
Active: false, // should be flipped
Config: map[string]string{
"region": "us-east-1",
"access_key": "AKIAIOSXABCD7EXAMPLE", // override
"secret_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", // added
"kms_key_id": "19ec80b0-dfdd-4d97-8164-c6examplekey", // added
},
},
}, result)
}
func TestConfig_LoadClientNodeMaxAllocs(t *testing.T) {
ci.Parallel(t)
testCases := []struct {
fileName string
}{
{
fileName: "test-resources/client_with_maxallocs.hcl",
},
{
fileName: "test-resources/client_with_maxallocs.json",
},
}
for _, tc := range testCases {
t.Run("minimal client expect defaults", func(t *testing.T) {
defaultConfig := DefaultConfig()
agentConfig, err := LoadConfig(tc.fileName)
must.NoError(t, err)
agentConfig = defaultConfig.Merge(agentConfig)
must.Eq(t, 5, agentConfig.Client.NodeMaxAllocs)
})
}
}
func TestEventlog_Merge(t *testing.T) {
t.Run("nil rhs merge", func(t *testing.T) {
var c1, c2 *Eventlog
c1 = &Eventlog{
Enabled: true,
Level: "info",
}
result := c1.Merge(c2)
must.Eq(t, result, c1)
})
t.Run("nil lhs merge", func(t *testing.T) {
var c1, c2 *Eventlog
c2 = &Eventlog{
Enabled: true,
Level: "info",
}
result := c1.Merge(c2)
must.Eq(t, result, c2)
})
t.Run("full merge", func(t *testing.T) {
c1 := &Eventlog{
Enabled: false,
Level: "info",
}
c2 := &Eventlog{
Enabled: true,
Level: "error",
}
result := c1.Merge(c2)
must.True(t, result.Enabled)
must.Eq(t, result.Level, "error")
})
t.Run("enabled merge", func(t *testing.T) {
// NOTE: Can only be enabled, not disabled
c1 := &Eventlog{
Enabled: true,
}
c2 := &Eventlog{
Enabled: false,
}
result := c1.Merge(c2)
must.True(t, result.Enabled)
})
}
func TestEventlog_Validate(t *testing.T) {
ci.Parallel(t)
testCases := []struct {
desc string
eventlog *Eventlog
shouldErr bool
}{
{
desc: "valid level",
eventlog: &Eventlog{Level: "info"},
},
{
desc: "invalid level",
eventlog: &Eventlog{Level: "debug"},
shouldErr: true,
},
{
desc: "nil eventlog",
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
ci.Parallel(t)
if tc.shouldErr {
must.Error(t, tc.eventlog.Validate())
} else {
must.NoError(t, tc.eventlog.Validate())
}
})
}
}