You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/cmd/config/config_test.go

3538 lines
88 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package config
import (
"encoding/base64"
"fmt"
"net"
"net/http"
"os"
"testing"
"time"
"github.com/hashicorp/boundary/internal/event"
"github.com/hashicorp/boundary/internal/ratelimit"
"github.com/hashicorp/boundary/internal/util"
configutil "github.com/hashicorp/go-secure-stdlib/configutil/v2"
"github.com/hashicorp/go-secure-stdlib/listenerutil"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDevController(t *testing.T) {
actual, err := DevController()
if err != nil {
t.Fatal(err)
}
truePointer := new(bool)
*truePointer = true
apiHeaders := map[int]http.Header{
0: {
"Content-Security-Policy": {"default-src 'none'"},
"X-Content-Type-Options": {"nosniff"},
"Strict-Transport-Security": {"max-age=31536000; includeSubDomains"},
"Cache-Control": {"no-store"},
},
}
uiHeaders := map[int]http.Header{
0: {
"Content-Security-Policy": {defaultCsp},
"X-Content-Type-Options": {"nosniff"},
"Strict-Transport-Security": {"max-age=31536000; includeSubDomains"},
"Cache-Control": {"no-store"},
},
}
exp := &Config{
Eventing: event.DefaultEventerConfig(),
SharedConfig: &configutil.SharedConfig{
DisableMlock: true,
Listeners: []*listenerutil.ListenerConfig{
{
Type: "tcp",
Purpose: []string{"api"},
TLSDisable: true,
CorsEnabled: truePointer,
CorsAllowedOrigins: []string{"*"},
CustomApiResponseHeaders: apiHeaders,
CustomUiResponseHeaders: uiHeaders,
},
{
Type: "tcp",
Purpose: []string{"cluster"},
CustomApiResponseHeaders: apiHeaders,
CustomUiResponseHeaders: uiHeaders,
},
{
Type: "tcp",
Purpose: []string{"ops"},
TLSDisable: true,
CustomApiResponseHeaders: apiHeaders,
CustomUiResponseHeaders: uiHeaders,
},
},
Seals: []*configutil.KMS{
{
Type: "aead",
Purpose: []string{"root"},
Config: map[string]string{
"aead_type": "aes-gcm",
"key_id": "global_root",
},
},
{
Type: "aead",
Purpose: []string{"worker-auth"},
Config: map[string]string{
"aead_type": "aes-gcm",
"key_id": "global_worker-auth",
},
},
{
Type: "aead",
Purpose: []string{"bsr"},
Config: map[string]string{
"aead_type": "aes-gcm",
"key_id": "global_bsr",
},
},
{
Type: "aead",
Purpose: []string{"recovery"},
Config: map[string]string{
"aead_type": "aes-gcm",
"key_id": "global_recovery",
},
},
},
},
Controller: &Controller{
Name: "dev-controller",
Description: "A default controller created in dev mode",
ApiRateLimits: make(ratelimit.Configs, 0),
ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(),
},
DevController: true,
}
exp.Eventing.ErrorEventsDisabled = true
exp.Eventing.SysEventsEnabled = false
exp.Eventing.ObservationsEnabled = false
exp.Listeners[0].RawConfig = actual.Listeners[0].RawConfig
exp.Listeners[1].RawConfig = actual.Listeners[1].RawConfig
exp.Listeners[2].RawConfig = actual.Listeners[2].RawConfig
exp.Seals[0].Config["key"] = actual.Seals[0].Config["key"]
exp.Seals[1].Config["key"] = actual.Seals[1].Config["key"]
exp.Seals[2].Config["key"] = actual.Seals[2].Config["key"]
exp.Seals[3].Config["key"] = actual.Seals[3].Config["key"]
exp.DevControllerKey = actual.Seals[0].Config["key"]
exp.DevWorkerAuthKey = actual.Seals[1].Config["key"]
exp.DevBsrKey = actual.Seals[2].Config["key"]
exp.DevRecoveryKey = actual.Seals[3].Config["key"]
assert.Equal(t, exp, actual)
// Do some CORS-specific testing
{
// CORS disabled
conf := `
listener "tcp" {
purpose = "api"
cors_enabled = false
}
`
actual, err = Parse(conf)
assert.NoError(t, err)
l0 := actual.Listeners[0]
assert.False(t, *l0.CorsEnabled)
assert.Empty(t, l0.CorsAllowedHeaders)
// Enabled with a wildcard
conf = `
listener "tcp" {
purpose = "api"
cors_enabled = true
cors_allowed_origins = ["*"]
}
`
actual, err = Parse(conf)
assert.NoError(t, err)
l0 = actual.Listeners[0]
assert.True(t, *l0.CorsEnabled)
assert.Equal(t, []string{"*"}, l0.CorsAllowedOrigins)
assert.Nil(t, l0.CorsDisableDefaultAllowedOriginValues)
// Disabled, default behavior
conf = `
listener "tcp" {
purpose = "api"
}
`
actual, err = Parse(conf)
assert.NoError(t, err)
l0 = actual.Listeners[0]
assert.True(t, *l0.CorsEnabled)
assert.Equal(t, []string{"*"}, l0.CorsAllowedOrigins)
assert.Nil(t, l0.CorsDisableDefaultAllowedOriginValues)
// Disabled, default behavior
conf = `
listener "tcp" {
purpose = "api"
cors_disable_default_allowed_origin_values = true
}
`
actual, err = Parse(conf)
assert.NoError(t, err)
l0 = actual.Listeners[0]
assert.Nil(t, l0.CorsEnabled)
assert.Empty(t, l0.CorsAllowedOrigins)
assert.True(t, *l0.CorsDisableDefaultAllowedOriginValues)
}
// Test plugins block
{
// CORS disabled
conf := `
plugins {
execution_dir = "/tmp/foobar"
}
`
actual, err = Parse(conf)
assert.NoError(t, err)
assert.Equal(t, actual.Plugins.ExecutionDir, "/tmp/foobar")
}
}
func TestDevWorker(t *testing.T) {
actual, err := DevWorker(WithSysEventsEnabled(true), WithObservationsEnabled(true), TestWithErrorEventsEnabled(t, true))
if err != nil {
t.Fatal(err)
}
exp := &Config{
Eventing: event.DefaultEventerConfig(),
SharedConfig: &configutil.SharedConfig{
DisableMlock: true,
Listeners: []*listenerutil.ListenerConfig{
{
Type: "tcp",
Purpose: []string{"proxy"},
CustomApiResponseHeaders: map[int]http.Header{
0: {
"Content-Security-Policy": {"default-src 'none'"},
"X-Content-Type-Options": {"nosniff"},
"Strict-Transport-Security": {"max-age=31536000; includeSubDomains"},
"Cache-Control": {"no-store"},
},
},
CustomUiResponseHeaders: map[int]http.Header{
0: {
"Content-Security-Policy": {defaultCsp},
"X-Content-Type-Options": {"nosniff"},
"Strict-Transport-Security": {"max-age=31536000; includeSubDomains"},
"Cache-Control": {"no-store"},
},
},
},
},
Seals: []*configutil.KMS{
{
Type: "aead",
Purpose: []string{"worker-auth-storage"},
Config: map[string]string{
"aead_type": "aes-gcm",
"key_id": "worker-auth-storage",
},
},
},
},
Worker: &Worker{
Name: "w_1234567890",
Description: "A default worker created in dev mode",
InitialUpstreams: []string{"127.0.0.1"},
InitialUpstreamsRaw: []any{"127.0.0.1"},
Tags: map[string][]string{
"type": {"dev", "local"},
},
},
}
exp.Listeners[0].RawConfig = actual.Listeners[0].RawConfig
exp.Seals[0].Config["key"] = actual.Seals[0].Config["key"]
exp.Worker.TagsRaw = actual.Worker.TagsRaw
assert.Equal(t, exp, actual)
// Redo it with key=value syntax for tags
devWorkerKeyValueConfig := `
listener "tcp" {
purpose = "proxy"
}
worker {
name = "w_1234567890"
description = "A default worker created in dev mode"
initial_upstreams = ["127.0.0.1"]
tags = ["type=dev", "type=local"]
}
`
actual, err = Parse(devConfig + devWorkerKeyValueConfig)
assert.NoError(t, err)
exp.Listeners[0].RawConfig = actual.Listeners[0].RawConfig
exp.Seals = nil
exp.Worker.TagsRaw = actual.Worker.TagsRaw
assert.Equal(t, exp, actual)
// Handle when there is a singular value not indicated as a slice
devWorkerKeyValueConfig = `
listener "tcp" {
purpose = "proxy"
}
worker {
name = "w_1234567890"
description = "A default worker created in dev mode"
initial_upstreams = ["127.0.0.1"]
tags {
type = "local"
}
}
`
actual, err = Parse(devConfig + devWorkerKeyValueConfig)
assert.NoError(t, err)
exp.Listeners[0].RawConfig = actual.Listeners[0].RawConfig
exp.Worker.TagsRaw = actual.Worker.TagsRaw
prevTags := exp.Worker.Tags
exp.Worker.Tags = map[string][]string{"type": {"local"}}
assert.Equal(t, exp, actual)
exp.Worker.Tags = prevTags
// Redo it with non-lower-cased keys
devWorkerKeyValueConfig = `
listener "tcp" {
purpose = "proxy"
}
worker {
name = "w_1234567890"
description = "A default worker created in dev mode"
initial_upstreams = ["127.0.0.1"]
tags = ["tyPe=dev", "type=local"]
}
`
_, err = Parse(devConfig + devWorkerKeyValueConfig)
assert.Error(t, err)
// Redo it with non-lower-cased values
devWorkerKeyValueConfig = `
listener "tcp" {
purpose = "proxy"
}
worker {
name = "w_1234567890"
description = "A default worker created in dev mode"
initial_upstreams = ["127.0.0.1"]
tags = ["type=dev", "type=loCal"]
}
`
_, err = Parse(devConfig + devWorkerKeyValueConfig)
assert.Error(t, err)
// Redo with non-printable characters to validate the strutil function
devWorkerKeyValueConfig = `
listener "tcp" {
purpose = "proxy"
}
worker {
name = "dev-work\u0000er"
description = "A default worker created in dev mode"
initial_upstreams = ["127.0.0.1"]
tags = ["type=dev", "type=local"]
}
`
_, err = Parse(devConfig + devWorkerKeyValueConfig)
assert.Error(t, err)
// Check activation token parsing
devWorkerActivationTokenConfig := `
listener "tcp" {
purpose = "proxy"
}
worker {
name = "dev-worker"
description = "A default worker created in dev mode"
initial_upstreams = ["127.0.0.1"]
controller_generated_activation_token = "foobar"
}
`
actual, err = Parse(devConfig + devWorkerActivationTokenConfig)
require.NoError(t, err)
assert.Equal(t, "foobar", actual.Worker.ControllerGeneratedActivationToken)
}
func TestDevCombined(t *testing.T) {
actual, err := DevCombined()
if err != nil {
t.Fatal(err)
}
truePointer := new(bool)
*truePointer = true
apiHeaders := map[int]http.Header{
0: {
"Content-Security-Policy": {"default-src 'none'"},
"X-Content-Type-Options": {"nosniff"},
"Strict-Transport-Security": {"max-age=31536000; includeSubDomains"},
"Cache-Control": {"no-store"},
},
}
uiHeaders := map[int]http.Header{
0: {
"Content-Security-Policy": {defaultCsp},
"X-Content-Type-Options": {"nosniff"},
"Strict-Transport-Security": {"max-age=31536000; includeSubDomains"},
"Cache-Control": {"no-store"},
},
}
exp := &Config{
Eventing: event.DefaultEventerConfig(),
SharedConfig: &configutil.SharedConfig{
DisableMlock: true,
Listeners: []*listenerutil.ListenerConfig{
{
Type: "tcp",
Purpose: []string{"api"},
TLSDisable: true,
CorsEnabled: truePointer,
CorsAllowedOrigins: []string{"*"},
CustomApiResponseHeaders: apiHeaders,
CustomUiResponseHeaders: uiHeaders,
},
{
Type: "tcp",
Purpose: []string{"cluster"},
CustomApiResponseHeaders: apiHeaders,
CustomUiResponseHeaders: uiHeaders,
},
{
Type: "tcp",
Purpose: []string{"ops"},
TLSDisable: true,
CustomApiResponseHeaders: apiHeaders,
CustomUiResponseHeaders: uiHeaders,
},
{
Type: "tcp",
Purpose: []string{"proxy"},
CustomApiResponseHeaders: apiHeaders,
CustomUiResponseHeaders: uiHeaders,
},
},
Seals: []*configutil.KMS{
{
Type: "aead",
Purpose: []string{"root"},
Config: map[string]string{
"aead_type": "aes-gcm",
"key_id": "global_root",
},
},
{
Type: "aead",
Purpose: []string{"worker-auth"},
Config: map[string]string{
"aead_type": "aes-gcm",
"key_id": "global_worker-auth",
},
},
{
Type: "aead",
Purpose: []string{"bsr"},
Config: map[string]string{
"aead_type": "aes-gcm",
"key_id": "global_bsr",
},
},
{
Type: "aead",
Purpose: []string{"recovery"},
Config: map[string]string{
"aead_type": "aes-gcm",
"key_id": "global_recovery",
},
},
{
Type: "aead",
Purpose: []string{"worker-auth-storage"},
Config: map[string]string{
"aead_type": "aes-gcm",
"key_id": "worker-auth-storage",
},
},
},
},
Controller: &Controller{
Name: "dev-controller",
Description: "A default controller created in dev mode",
ApiRateLimits: make(ratelimit.Configs, 0),
ApiRateLimiterMaxQuotas: ratelimit.DefaultLimiterMaxQuotas(),
},
DevController: true,
Worker: &Worker{
Name: "w_1234567890",
Description: "A default worker created in dev mode",
InitialUpstreams: []string{"127.0.0.1"},
InitialUpstreamsRaw: []any{"127.0.0.1"},
Tags: map[string][]string{
"type": {"dev", "local"},
},
},
}
exp.Listeners[0].RawConfig = actual.Listeners[0].RawConfig
exp.Listeners[1].RawConfig = actual.Listeners[1].RawConfig
exp.Listeners[2].RawConfig = actual.Listeners[2].RawConfig
exp.Listeners[3].RawConfig = actual.Listeners[3].RawConfig
exp.Seals[0].Config["key"] = actual.Seals[0].Config["key"]
exp.Seals[1].Config["key"] = actual.Seals[1].Config["key"]
exp.Seals[2].Config["key"] = actual.Seals[2].Config["key"]
exp.Seals[3].Config["key"] = actual.Seals[3].Config["key"]
exp.Seals[4].Config["key"] = actual.Seals[4].Config["key"]
exp.DevControllerKey = actual.Seals[0].Config["key"]
exp.DevWorkerAuthKey = actual.Seals[1].Config["key"]
exp.DevBsrKey = actual.Seals[2].Config["key"]
exp.DevRecoveryKey = actual.Seals[3].Config["key"]
exp.DevWorkerAuthStorageKey = actual.Seals[4].Config["key"]
exp.Worker.TagsRaw = actual.Worker.TagsRaw
assert.Equal(t, exp, actual)
}
func TestDevWorkerCredentialStoragePath(t *testing.T) {
t.Parallel()
tests := []struct {
name string
devWorkerProvidedConfiguration string
storagePath string
}{
{
name: "Relative Storage Directory",
devWorkerProvidedConfiguration: `
listener "tcp" {
purpose = "proxy"
}
worker {
name = "w_1234567890"
description = "A default worker created in dev mode"
initial_upstreams = ["127.0.0.1"]
tags {
type = ["dev", "local"]
}
auth_storage_path = ".."
}
`,
storagePath: "..",
},
{
name: "Nonexistent Storage Directory",
devWorkerProvidedConfiguration: `
listener "tcp" {
purpose = "proxy"
}
worker {
name = "w_1234567890"
description = "A default worker created in dev mode"
initial_upstreams = ["127.0.0.1"]
tags {
type = ["dev", "local"]
}
auth_storage_path = "nonexistent_dir/here"
}
`,
storagePath: "nonexistent_dir/here",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parsed, err := Parse(devConfig + tt.devWorkerProvidedConfiguration)
require.NoError(t, err)
require.Equal(t, tt.storagePath, parsed.Worker.AuthStoragePath)
})
}
}
func TestDevWorkerRecordingStorageMinimumAvailableCapacity(t *testing.T) {
t.Parallel()
td := t.TempDir()
tests := []struct {
name string
devWorkerProvidedConfiguration string
storagePath string
storageCapacity string
expectedDiskSpace uint64
expectedErrMsg string
}{
{
name: "empty storage with empty capacity",
devWorkerProvidedConfiguration: `
listener "tcp" {
purpose = "proxy"
}
worker {
name = "w_1234567890"
description = "A default worker created in dev mode"
initial_upstreams = ["127.0.0.1"]
tags {
type = ["dev", "local"]
}
}
`,
expectedDiskSpace: 0,
storagePath: "",
},
{
name: "empty storage path with set capacity",
devWorkerProvidedConfiguration: `
listener "tcp" {
purpose = "proxy"
}
worker {
name = "w_1234567890"
description = "A default worker created in dev mode"
initial_upstreams = ["127.0.0.1"]
tags {
type = ["dev", "local"]
}
recording_storage_minimum_available_capacity = "4kib"
}
`,
expectedErrMsg: "recording_storage_path cannot be empty when providing recording_storage_minimum_available_capacity",
},
{
name: "storage path with empty capacity defaults to 500mib",
devWorkerProvidedConfiguration: fmt.Sprintf(`
listener "tcp" {
purpose = "proxy"
}
worker {
name = "w_1234567890"
description = "A default worker created in dev mode"
initial_upstreams = ["127.0.0.1"]
tags {
type = ["dev", "local"]
}
recording_storage_path = "%v"
}
`, td),
storagePath: td,
expectedDiskSpace: 524288000,
},
{
name: "storage path with capacity string",
devWorkerProvidedConfiguration: fmt.Sprintf(`
listener "tcp" {
purpose = "proxy"
}
worker {
name = "w_1234567890"
description = "A default worker created in dev mode"
initial_upstreams = ["127.0.0.1"]
tags {
type = ["dev", "local"]
}
recording_storage_path = "%v"
recording_storage_minimum_available_capacity = "4kib"
}
`, td),
storagePath: td,
expectedDiskSpace: 4096,
},
{
name: "storage path with raw byte value",
devWorkerProvidedConfiguration: fmt.Sprintf(`
listener "tcp" {
purpose = "proxy"
}
worker {
name = "w_1234567890"
description = "A default worker created in dev mode"
initial_upstreams = ["127.0.0.1"]
tags {
type = ["dev", "local"]
}
recording_storage_path = "%v"
recording_storage_minimum_available_capacity = "4096"
}
`, td),
storagePath: td,
expectedDiskSpace: 4096,
},
{
name: "storage path with invalid capacity input",
devWorkerProvidedConfiguration: fmt.Sprintf(`
listener "tcp" {
purpose = "proxy"
}
worker {
name = "w_1234567890"
description = "A default worker created in dev mode"
initial_upstreams = ["127.0.0.1"]
tags {
type = ["dev", "local"]
}
recording_storage_path = "%v"
recording_storage_minimum_available_capacity = "gib"
}
`, td),
storagePath: td,
expectedErrMsg: "could not parse capacity from input",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parsed, err := Parse(devConfig + tt.devWorkerProvidedConfiguration)
if tt.expectedErrMsg != "" {
require.Error(t, err)
assert.ErrorContains(t, err, tt.expectedErrMsg)
return
}
require.NoError(t, err)
assert.Equal(t, tt.storagePath, parsed.Worker.RecordingStoragePath)
assert.Equal(t, tt.expectedDiskSpace, parsed.Worker.RecordingStorageMinimumAvailableDiskSpace)
})
}
}
func TestDevWorkerRecordingStoragePath(t *testing.T) {
t.Parallel()
td := t.TempDir()
tests := []struct {
name string
devWorkerProvidedConfiguration string
storagePath string
}{
{
name: "Relative Storage Directory",
devWorkerProvidedConfiguration: `
listener "tcp" {
purpose = "proxy"
}
worker {
name = "w_1234567890"
description = "A default worker created in dev mode"
initial_upstreams = ["127.0.0.1"]
tags {
type = ["dev", "local"]
}
recording_storage_path = ".."
}
`,
storagePath: "..",
},
{
name: "temp dir",
devWorkerProvidedConfiguration: fmt.Sprintf(`
listener "tcp" {
purpose = "proxy"
}
worker {
name = "w_1234567890"
description = "A default worker created in dev mode"
initial_upstreams = ["127.0.0.1"]
tags {
type = ["dev", "local"]
}
recording_storage_path = "%v"
}
`, td),
storagePath: td,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parsed, err := Parse(devConfig + tt.devWorkerProvidedConfiguration)
require.NoError(t, err)
require.Equal(t, tt.storagePath, parsed.Worker.RecordingStoragePath)
})
}
}
func TestDevControllerIpv6(t *testing.T) {
require, assert := require.New(t), assert.New(t)
// This test only validates that all listeners are utilizing an IPv6 address.
// Other dev controller configurations are validates in TestDevController.
actual, err := DevController(WithIPv6Enabled(true))
require.NoError(err)
// expected an error here because we purposely did not provide a port number
// to allow randomly assigned port values
_, _, err = net.SplitHostPort(actual.Controller.PublicClusterAddr)
require.Error(err)
// assert the square brackets are removed from the host ipv6 address and that the port value is empty
publicAddr, port, err := util.SplitHostPort(actual.Controller.PublicClusterAddr)
require.NoError(err)
assert.Empty(port)
assert.Empty(publicAddr)
require.NotEmpty(actual.Listeners)
for _, l := range actual.Listeners {
addr, _, err := util.SplitHostPort(l.Address)
require.NoError(err)
ip := net.ParseIP(addr)
assert.NotNil(ip, "failed to parse listener address for %v", l.Purpose)
assert.NotNil(ip.To16(), "failed to convert address to IPv6 for %v, found %v", l.Purpose, addr)
}
}
func TestDevWorkerIpv6(t *testing.T) {
require, assert := require.New(t), assert.New(t)
// This test only validates that all listeners are utilizing an IPv6 address.
// Other dev worker configurations are validates in TestDevWorker.
actual, err := DevWorker(WithIPv6Enabled(true))
require.NoError(err)
// expected an error here because we purposely did not provide a port number
// to allow randomly assigned port values
_, _, err = net.SplitHostPort(actual.Worker.PublicAddr)
require.Error(err)
// assert the square brackets are removed from the worker ipv6 address and that the port value is empty
publicAddr, port, err := util.SplitHostPort(actual.Worker.PublicAddr)
require.NoError(err)
assert.Empty(port)
ip := net.ParseIP(publicAddr)
assert.NotNil(ip, "failed to parse worker public address")
assert.NotNil(ip.To16(), "worker public address is not IPv6 %s", actual.Worker.PublicAddr)
require.NotEmpty(actual.Listeners)
for _, l := range actual.Listeners {
addr, _, err := util.SplitHostPort(l.Address)
require.NoError(err)
ip := net.ParseIP(addr)
assert.NotNil(ip, "failed to parse listener address for %v", l.Purpose)
assert.NotNil(ip.To16(), "failed to convert address to IPv6 for %v, found %v", l.Purpose, addr)
}
}
func TestDevCombinedIpv6(t *testing.T) {
require, assert := require.New(t), assert.New(t)
// This test only validates that all listeners are utilizing an IPv6 address.
actual, err := DevCombined(WithIPv6Enabled(true))
require.NoError(err)
// expected an error here because we purposely did not provide a port number
// to allow randomly assigned port values for the worker and controller
_, _, err = net.SplitHostPort(actual.Worker.PublicAddr)
require.Error(err)
_, _, err = net.SplitHostPort(actual.Controller.PublicClusterAddr)
require.Error(err)
// assert the square brackets are removed from the host ipv6 address and that the port value is empty
publicAddr, port, err := util.SplitHostPort(actual.Worker.PublicAddr)
require.NoError(err)
assert.Empty(port)
ip := net.ParseIP(publicAddr)
assert.NotNil(ip, "failed to parse worker public address")
assert.NotNil(ip.To16(), "worker public address is not IPv6 %s", actual.Worker.PublicAddr)
// assert the square brackets are removed from the controller ipv6 address and that the port value is empty
publicAddr, port, err = util.SplitHostPort(actual.Controller.PublicClusterAddr)
require.NoError(err)
assert.Empty(port)
assert.Empty(publicAddr)
require.NotEmpty(actual.Listeners)
for _, l := range actual.Listeners {
addr, _, err := util.SplitHostPort(l.Address)
require.NoError(err)
ip := net.ParseIP(addr)
assert.NotNil(ip, "failed to parse listener address for %v", l.Purpose)
assert.NotNil(ip.To16(), "failed to convert address to IPv6 for %v, found %v", l.Purpose, addr)
}
}
func TestDevKeyGeneration(t *testing.T) {
t.Parallel()
dk := DevKeyGeneration()
buf, err := base64.StdEncoding.DecodeString(dk)
require.NoError(t, err)
require.Len(t, buf, 32)
require.NotEqual(t, dk, DevKeyGeneration())
}
func TestParsingName(t *testing.T) {
t.Parallel()
config := `
controller {
name = "%s"
}
worker {
name = "%s"
}
`
controllerEnv := "FOOENV"
workerEnv := "BARENV"
cases := []struct {
name string
templateController string
templateWorker string
envController string
envWorker string
expectedController string
expectedWorker string
}{
{
name: "no env",
templateController: "foobar",
templateWorker: "test_worker_barfoo",
expectedController: "foobar",
expectedWorker: "test_worker_barfoo",
},
{
name: "env",
templateController: fmt.Sprintf("env://%s", controllerEnv),
templateWorker: fmt.Sprintf("env://%s", workerEnv),
envController: "foobar2",
envWorker: "env_name_barfoo2",
expectedController: "foobar2",
expectedWorker: "env_name_barfoo2",
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
if tt.envController != "" {
os.Setenv(controllerEnv, tt.envController)
}
if tt.envWorker != "" {
os.Setenv(workerEnv, tt.envWorker)
}
out, err := Parse(fmt.Sprintf(config, tt.templateController, tt.templateWorker))
require.NoError(t, err)
assert.Equal(t, tt.expectedController, out.Controller.Name)
assert.Equal(t, tt.expectedWorker, out.Worker.Name)
})
}
}
func TestParsingSchedulerIntervals(t *testing.T) {
t.Parallel()
cases := []struct {
name string
config string
wantErr bool
wantMonitorInterval time.Duration
wantRunJobInterval time.Duration
}{
{
name: "invalid-run-interval",
config: `
controller {
scheduler {
job_run_interval = "hello"
}
}
`,
wantErr: true,
},
{
name: "invalid-monitor-interval",
config: `
controller {
scheduler {
monitor_interval = "hello"
}
}
`,
wantErr: true,
},
{
name: "valid-undefined",
config: `controller { scheduler {} }`,
wantMonitorInterval: 0,
wantRunJobInterval: 0,
},
{
name: "run-job-interval",
config: `
controller {
scheduler {
job_run_interval = "10m"
}
}
`,
wantMonitorInterval: 0,
wantRunJobInterval: 10 * time.Minute,
},
{
name: "monitor-interval",
config: `
controller {
scheduler {
monitor_interval = "6h"
}
}
`,
wantMonitorInterval: 6 * time.Hour,
wantRunJobInterval: 0,
},
{
name: "both",
config: `
controller {
scheduler {
monitor_interval = "7d"
job_run_interval = "20s"
}
}
`,
wantMonitorInterval: 7 * 24 * time.Hour,
wantRunJobInterval: 20 * time.Second,
},
}
for _, tt := range cases {
tt := tt
t.Run(tt.name, func(t *testing.T) {
out, err := Parse(tt.config)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantMonitorInterval, out.Controller.Scheduler.MonitorIntervalDuration)
assert.Equal(t, tt.wantRunJobInterval, out.Controller.Scheduler.JobRunIntervalDuration)
})
}
}
func TestWorkerTags(t *testing.T) {
defaultStateFn := func(t *testing.T, tags string) {
t.Setenv("BOUNDARY_WORKER_TAGS", tags)
}
tests := []struct {
name string
in string
stateFn func(t *testing.T, tags string)
actualTags string
expWorkerTags map[string][]string
expErr bool
expErrStr string
}{
{
name: "tags in HCL",
in: `
worker {
tags {
type = ["dev", "local"]
typetwo = "devtwo"
}
}`,
expWorkerTags: map[string][]string{
"type": {"dev", "local"},
"typetwo": {"devtwo"},
},
expErr: false,
},
{
name: "tags in HCL key=value",
in: `
worker {
tags = ["type=dev", "type=local", "typetwo=devtwo"]
}
`,
expWorkerTags: map[string][]string{
"type": {"dev", "local"},
"typetwo": {"devtwo"},
},
expErr: false,
},
{
name: "no tags",
in: `
worker {
name = "w_1234567890"
}
`,
expWorkerTags: nil,
expErr: false,
},
{
name: "empty tags",
in: `
worker {
name = "w_1234567890"
tags = {}
}
`,
expWorkerTags: map[string][]string{},
expErr: false,
},
{
name: "empty tags 2",
in: `
worker {
name = "w_1234567890"
tags = []
}
`,
expWorkerTags: map[string][]string{},
expErr: false,
},
{
name: "empty str",
in: `
worker {
tags = ""
}`,
expWorkerTags: map[string][]string{},
expErr: false,
},
{
name: "empty env var",
in: `
worker {
tags = "env://BOUNDARY_WORKER_TAGS"
}`,
expWorkerTags: map[string][]string{},
expErr: false,
},
{
name: "not a url - entire tags block",
in: `
worker {
tags = "\x00"
}`,
expWorkerTags: map[string][]string{},
expErr: true,
expErrStr: `Error parsing worker tags: error parsing url ("parse \"\\x00\": net/url: invalid control character in URL"): not a url`,
},
{
name: "not a url - key's value set to string",
in: `
worker {
tags {
type = "\x00"
}
}
`,
expWorkerTags: map[string][]string{
"type": {"\x00"},
},
expErr: false,
},
{
name: "one tag key",
in: `
worker {
tags = "env://BOUNDARY_WORKER_TAGS"
}`,
stateFn: defaultStateFn,
actualTags: `type = ["dev", "local"]`,
expWorkerTags: map[string][]string{
"type": {"dev", "local"},
},
expErr: false,
},
{
name: "multiple tag keys",
in: `
worker {
tags = "env://BOUNDARY_WORKER_TAGS"
}`,
stateFn: defaultStateFn,
actualTags: `
type = ["dev", "local"]
typetwo = ["devtwo", "localtwo"]
`,
expWorkerTags: map[string][]string{
"type": {"dev", "local"},
"typetwo": {"devtwo", "localtwo"},
},
expErr: false,
},
{
name: "comma in tag key string",
in: `
worker {
tags {
"key,"= ["value"],
}
}`,
expErr: true,
expErrStr: `Tag key "key," cannot contain commas`,
},
{
name: "comma in tag value string",
in: `
worker {
tags {
"key"= ["va,lue","value2"],
}
}`,
expErr: true,
expErrStr: `Tag value "va,lue" for tag key "key" cannot contain commas`,
},
{
name: "json tags - entire tags block",
in: `
worker {
tags = "env://BOUNDARY_WORKER_TAGS"
}`,
stateFn: defaultStateFn,
actualTags: `
{
"type": ["dev", "local"],
"typetwo": ["devtwo", "localtwo"]
}
`,
expWorkerTags: map[string][]string{
"type": {"dev", "local"},
"typetwo": {"devtwo", "localtwo"},
},
expErr: false,
},
{
name: "json tags - keys specified in the HCL file, values point to env/file",
in: `
worker {
tags = {
type = "env://BOUNDARY_WORKER_TAGS"
typetwo = "env://BOUNDARY_WORKER_TAGS_TWO"
}
}`,
stateFn: func(t *testing.T, tags string) {
defaultStateFn(t, tags)
t.Setenv("BOUNDARY_WORKER_TAGS_TWO", `["devtwo", "localtwo"]`)
},
actualTags: `["dev","local"]`,
expWorkerTags: map[string][]string{
"type": {"dev", "local"},
"typetwo": {"devtwo", "localtwo"},
},
expErr: false,
},
{
name: "json tags - mix n' match",
in: `
worker {
name = "web-prod-us-east-1"
tags {
type = "env://BOUNDARY_WORKER_TYPE_TAGS"
typetwo = "file://type_two_tags.json"
typethree = ["devthree", "localthree"]
}
}
`,
stateFn: func(t *testing.T, tags string) {
workerTypeTags := `["dev", "local"]`
t.Setenv("BOUNDARY_WORKER_TYPE_TAGS", workerTypeTags)
filepath := "./type_two_tags.json"
err := os.WriteFile(filepath, []byte(`["devtwo", "localtwo"]`), 0o666)
require.NoError(t, err)
t.Cleanup(func() {
err := os.Remove(filepath)
require.NoError(t, err)
})
},
expWorkerTags: map[string][]string{
"type": {"dev", "local"},
"typetwo": {"devtwo", "localtwo"},
"typethree": {"devthree", "localthree"},
},
expErr: false,
},
{
name: "bad json tags",
in: `
worker {
tags = {
type = "env://BOUNDARY_WORKER_TAGS"
typetwo = "env://BOUNDARY_WORKER_TAGS"
}
}`,
stateFn: defaultStateFn,
actualTags: `
{
"type": ["dev", "local"],
"typetwo": ["devtwo", "localtwo"]
}
`,
expWorkerTags: nil,
expErr: true,
expErrStr: "Error unmarshaling env var/file contents: json: cannot unmarshal object into Go value of type []string",
},
{
name: "no clean mapping to internal structures",
in: `
worker {
tags = "env://BOUNDARY_WORKER_TAGS"
}`,
stateFn: defaultStateFn,
actualTags: `
worker {
tags {
type = "indeed"
}
}
`,
expErr: true,
expErrStr: "Error decoding the worker's tags: 1 error(s) decoding:\n\n* '[0][worker][0]' expected type 'string', got unconvertible type 'map[string]interface {}', value: 'map[tags:[map[type:indeed]]]'",
},
{
name: "not HCL",
in: `worker {
tags = "env://BOUNDARY_WORKER_TAGS"
}`,
stateFn: defaultStateFn,
actualTags: `not_hcl`,
expErr: true,
expErrStr: "Error decoding raw worker tags: At 1:9: key 'not_hcl' expected start of object ('{') or assignment ('=')",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.stateFn != nil {
tt.stateFn(t, tt.actualTags)
}
c, err := Parse(tt.in)
if tt.expErr {
require.EqualError(t, err, tt.expErrStr)
require.Nil(t, c)
return
}
require.NoError(t, err)
require.NotNil(t, c)
require.NotNil(t, c.Worker)
require.Equal(t, tt.expWorkerTags, c.Worker.Tags)
})
}
}
func TestController_EventingConfig(t *testing.T) {
t.Parallel()
tests := []struct {
name string
config []string
wantEventerConfig *event.EventerConfig
wantErr string
}{
{
name: "default",
wantEventerConfig: event.DefaultEventerConfig(),
},
{
name: "audit-enabled",
config: []string{`
events {
audit_enabled = true
}
`},
wantEventerConfig: &event.EventerConfig{
AuditEnabled: true,
ObservationsEnabled: false,
Sinks: []*event.SinkConfig{
event.DefaultSink(),
},
},
},
{
name: "observations-enabled",
config: []string{`
events {
observations_enabled = true
}
`},
wantEventerConfig: &event.EventerConfig{
AuditEnabled: false,
ObservationsEnabled: true,
Sinks: []*event.SinkConfig{
event.DefaultSink(),
},
},
},
{
name: "no-sink-type-determined",
config: []string{
`events {
audit_enabled = false
observations_enabled = true
sink {
format = "cloudevents-json"
name = "configured-sink"
event_types = [ "audit", "observation" ]
}
}`,
},
wantErr: `error parsing "events": sink type could not be determined`,
},
{
name: "sinks-configured",
config: []string{
`events {
audit_enabled = false
observations_enabled = true
sink {
type = "file"
format = "cloudevents-json"
name = "configured-sink"
event_types = [ "audit", "observation" ]
file {
file_name = "file-name"
rotate_duration = "2m"
}
}
sink {
type = "stderr"
format = "hclog-text"
name = "stderr-sink"
event_types = [ "error" ]
}
}`,
`events {
audit_enabled = false
observations_enabled = true
sink "file" {
format = "cloudevents-json"
name = "configured-sink"
event_types = [ "audit", "observation" ]
file {
file_name = "file-name"
rotate_duration = "2m"
}
}
sink "stderr" {
format = "hclog-text"
name = "stderr-sink"
event_types = [ "error" ]
}
}`,
`events {
audit_enabled = false
observations_enabled = true
sink {
format = "cloudevents-json"
name = "configured-sink"
event_types = [ "audit", "observation" ]
file {
file_name = "file-name"
rotate_duration = "2m"
}
}
sink {
format = "hclog-text"
name = "stderr-sink"
event_types = [ "error" ]
stderr = {}
}
}`,
`{
"events": {
"audit_enabled": false,
"observations_enabled": true,
"sink": [
{
"type": "file",
"format": "cloudevents-json",
"name": "configured-sink",
"event_types": ["audit", "observation"],
"file": {
"file_name": "file-name",
"rotate_duration": "2m"
}
},
{
"format": "hclog-text",
"name": "stderr-sink",
"event_types": ["error"],
"stderr": {}
}
]
}
}`,
},
wantEventerConfig: &event.EventerConfig{
AuditEnabled: false,
ObservationsEnabled: true,
Sinks: []*event.SinkConfig{
{
Type: "file",
Name: "configured-sink",
Format: "cloudevents-json",
EventTypes: []event.Type{"audit", "observation"},
FileConfig: &event.FileSinkTypeConfig{
FileName: "file-name",
RotateDurationHCL: "2m",
RotateDuration: 2 * time.Minute,
},
},
{
Type: "stderr",
Name: "stderr-sink",
Format: "hclog-text",
EventTypes: []event.Type{"error"},
StderrConfig: &event.StderrSinkTypeConfig{},
},
},
},
},
{
name: "audit_config",
config: []string{
`events {
audit_enabled = true
sink {
name = "audit-sink"
format = "cloudevents-json"
event_types = ["audit"]
file {
file_name = "audit.log"
}
audit_config {
audit_filter_overrides {
sensitive = ""
secret = "hmac-sha256"
}
}
}
}`,
},
wantEventerConfig: &event.EventerConfig{
AuditEnabled: true,
Sinks: []*event.SinkConfig{
{
Type: "file",
Name: "audit-sink",
Format: "cloudevents-json",
EventTypes: []event.Type{"audit"},
FileConfig: &event.FileSinkTypeConfig{
FileName: "audit.log",
},
AuditConfig: &event.AuditConfig{
FilterOverridesHCL: map[string]string{
"sensitive": "",
"secret": "hmac-sha256",
},
FilterOverrides: event.AuditFilterOperations{
event.SensitiveClassification: event.NoOperation,
event.SecretClassification: event.HmacSha256Operation,
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
for i, conf := range tt.config {
c, err := Parse(conf)
if tt.wantErr != "" {
require.Error(err)
assert.Empty(c)
assert.Equal(tt.wantErr, err.Error(), "config %d want %q and got %q", i, tt.wantErr, err.Error())
return
}
require.NoError(err)
assert.NotEmpty(c)
assert.Equal(tt.wantEventerConfig, c.Eventing)
}
})
}
}
func TestWorkerUpstreams(t *testing.T) {
tests := []struct {
name string
in string
stateFn func(t *testing.T)
expWorkerUpstreams []string
expErr bool
expErrIs error
expErrStr string
}{
{
name: "No Upstreams",
in: `
worker {
name = "test"
}
`,
expWorkerUpstreams: nil,
expErr: false,
},
{
name: "ipv4 Upstream",
in: `
worker {
name = "test"
initial_upstreams = ["127.0.0.1"]
}
`,
expWorkerUpstreams: []string{"127.0.0.1"},
expErr: false,
},
{
name: "ipv6 Upstream",
in: `
worker {
name = "test"
initial_upstreams = ["[2001:4860:4860:0:0:0:0:8888]"]
}
`,
expWorkerUpstreams: []string{"[2001:4860:4860:0:0:0:0:8888]"},
expErr: false,
},
{
name: "abbreviated ipv6 Upstream",
in: `
worker {
name = "test"
initial_upstreams = ["[2001:4860:4860::8888]"]
}
`,
expWorkerUpstreams: []string{"[2001:4860:4860::8888]"},
expErr: false,
},
{
name: "Multiple Upstreams",
in: `
worker {
name = "test"
initial_upstreams = ["127.0.0.1", "127.0.0.2", "127.0.0.3"]
}
`,
expWorkerUpstreams: []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"},
expErr: false,
},
{
name: "Using env var",
in: `
worker {
name = "test"
initial_upstreams = "env://BOUNDARY_WORKER_UPSTREAMS"
}
`,
stateFn: func(t *testing.T) { t.Setenv("BOUNDARY_WORKER_UPSTREAMS", `["127.0.0.1", "127.0.0.2", "127.0.0.3"]`) },
expWorkerUpstreams: []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"},
expErr: false,
},
{
name: "Using env var - invalid input 1",
in: `
worker {
name = "test"
initial_upstreams = "env://BOUNDARY_WORKER_UPSTREAMS"
}
`,
stateFn: func(t *testing.T) {
upstreams := `
worker {
initial_upstreams = ["127.0.0.1"]
}
`
t.Setenv("BOUNDARY_WORKER_UPSTREAMS", upstreams)
},
expWorkerUpstreams: nil,
expErr: true,
expErrStr: "Failed to parse worker upstreams: failed to unmarshal env/file contents: invalid character 'w' looking for beginning of value",
},
{
name: "Using env var - invalid input 2",
in: `
worker {
name = "test"
initial_upstreams = "env://BOUNDARY_WORKER_UPSTREAMS"
}
`,
stateFn: func(t *testing.T) { t.Setenv("BOUNDARY_WORKER_UPSTREAMS", `initial_upstreams = ["127.0.0.1"]`) },
expWorkerUpstreams: nil,
expErr: true,
expErrStr: "Failed to parse worker upstreams: failed to unmarshal env/file contents: invalid character 'i' looking for beginning of value",
},
{
name: "Unsupported object",
in: `
worker {
name = "test"
initial_upstreams = {
ip = "127.0.0.1"
ip = "127.0.0.2"
}
}
`,
expWorkerUpstreams: nil,
expErr: true,
expErrStr: "Failed to parse worker upstreams: unexpected type \"[]map[string]interface {}\"",
},
{
name: "Worker initial_upstreams set to invalid url",
in: `
worker {
name = "test"
initial_upstreams = "env://\x00"
}`,
expWorkerUpstreams: nil,
expErr: true,
expErrIs: parseutil.ErrNotAUrl,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.stateFn != nil {
tt.stateFn(t)
}
c, err := Parse(tt.in)
if tt.expErr {
if tt.expErrIs != nil {
require.ErrorIs(t, err, tt.expErrIs)
} else {
require.EqualError(t, err, tt.expErrStr)
}
require.Nil(t, c)
return
}
require.NoError(t, err)
require.NotNil(t, c)
require.NotNil(t, c.Worker)
require.EqualValues(t, tt.expWorkerUpstreams, c.Worker.InitialUpstreams)
})
}
}
func TestControllerDescription(t *testing.T) {
tests := []struct {
name string
in string
envDescription string
expDescription string
expErr bool
expErrStr string
}{
{
name: "Valid controller description from env var",
in: `
controller {
description = "env://CONTROLLER_DESCRIPTION"
}`,
envDescription: "Test controller description",
expDescription: "Test controller description",
expErr: false,
}, {
name: "Invalid controller description from env var",
in: `
controller {
description = "\uTest controller description"
}`,
expErr: true,
expErrStr: "At 3:22: illegal char escape",
}, {
name: "Not a URL, non-printable description",
in: `
controller {
description = "\x00"
}`,
expErr: true,
expErrStr: "Controller description contains non-printable characters",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("CONTROLLER_DESCRIPTION", tt.envDescription)
c, err := Parse(tt.in)
if tt.expErr {
require.EqualError(t, err, tt.expErrStr)
require.Nil(t, c)
return
}
require.NoError(t, err)
require.NotNil(t, c)
require.NotNil(t, c.Controller)
require.Equal(t, tt.expDescription, c.Controller.Description)
})
}
}
func TestControllerApiRateLimits(t *testing.T) {
tests := []struct {
name string
in string
expLimits ratelimit.Configs
expErr bool
expErrStr string
}{
{
name: "Single Rate limit",
in: `
controller {
api_rate_limit {
resources = ["*"]
actions = ["*"]
per = "total"
limit = 50
period = "1m"
}
}`,
expLimits: ratelimit.Configs{
{
Resources: []string{"*"},
Actions: []string{"*"},
Per: "total",
Limit: 50,
PeriodHCL: "1m",
Period: time.Minute,
Unlimited: false,
},
},
expErr: false,
},
{
name: "Single Rate with name",
in: `
controller {
api_rate_limit "default" {
resources = ["*"]
actions = ["*"]
per = "total"
limit = 50
period = "1m"
}
}`,
expLimits: ratelimit.Configs{
{
Resources: []string{"*"},
Actions: []string{"*"},
Per: "total",
Limit: 50,
PeriodHCL: "1m",
Period: time.Minute,
Unlimited: false,
},
},
expErr: false,
},
{
name: "Multiple Rate limit",
in: `
controller {
api_rate_limit {
resources = ["*"]
actions = ["*"]
per = "total"
limit = 50
period = "1m"
}
api_rate_limit {
resources = ["*"]
actions = ["list"]
per = "total"
limit = 20
period = "1m"
}
}`,
expLimits: ratelimit.Configs{
{
Resources: []string{"*"},
Actions: []string{"*"},
Per: "total",
Limit: 50,
PeriodHCL: "1m",
Period: time.Minute,
Unlimited: false,
},
{
Resources: []string{"*"},
Actions: []string{"list"},
Per: "total",
Limit: 20,
PeriodHCL: "1m",
Period: time.Minute,
Unlimited: false,
},
},
expErr: false,
},
{
name: "Multiple Rate limit with names",
in: `
controller {
api_rate_limit "default" {
resources = ["*"]
actions = ["*"]
per = "total"
limit = 50
period = "1m"
}
api_rate_limit "default-list" {
resources = ["*"]
actions = ["list"]
per = "total"
limit = 20
period = "1m"
}
}`,
expLimits: ratelimit.Configs{
{
Resources: []string{"*"},
Actions: []string{"*"},
Per: "total",
Limit: 50,
PeriodHCL: "1m",
Period: time.Minute,
Unlimited: false,
},
{
Resources: []string{"*"},
Actions: []string{"list"},
Per: "total",
Limit: 20,
PeriodHCL: "1m",
Period: time.Minute,
Unlimited: false,
},
},
expErr: false,
},
{
name: "Multiple Rate limit with name and no name",
in: `
controller {
api_rate_limit {
resources = ["*"]
actions = ["*"]
per = "total"
limit = 50
period = "1m"
}
api_rate_limit "list" {
resources = ["*"]
actions = ["list"]
per = "total"
limit = 20
period = "1m"
}
}`,
expLimits: ratelimit.Configs{
{
Resources: []string{"*"},
Actions: []string{"*"},
Per: "total",
Limit: 50,
PeriodHCL: "1m",
Period: time.Minute,
Unlimited: false,
},
{
Resources: []string{"*"},
Actions: []string{"list"},
Per: "total",
Limit: 20,
PeriodHCL: "1m",
Period: time.Minute,
Unlimited: false,
},
},
expErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c, err := Parse(tt.in)
if tt.expErr {
require.EqualError(t, err, tt.expErrStr)
require.Nil(t, c)
return
}
require.NoError(t, err)
require.NotNil(t, c)
require.NotNil(t, c.Controller)
require.Equal(t, tt.expLimits, c.Controller.ApiRateLimits)
})
}
}
func TestWorkerDescription(t *testing.T) {
tests := []struct {
name string
in string
envDescription string
expDescription string
expErr bool
expErrStr string
}{
{
name: "Valid worker description from env var",
in: `
worker {
description = "env://WORKER_DESCRIPTION"
}`,
envDescription: "Test worker description",
expDescription: "Test worker description",
expErr: false,
}, {
name: "Invalid worker description",
in: `
worker {
description = "\uTest worker description"
}`,
expErr: true,
expErrStr: "At 3:22: illegal char escape",
}, {
name: "Not a URL, non-printable description",
in: `
worker {
description = "\x00"
}`,
expErr: true,
expErrStr: "Worker description contains non-printable characters",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("WORKER_DESCRIPTION", tt.envDescription)
c, err := Parse(tt.in)
if tt.expErr {
require.EqualError(t, err, tt.expErrStr)
require.Nil(t, c)
return
}
require.NoError(t, err)
require.NotNil(t, c)
require.NotNil(t, c.Worker)
require.Equal(t, tt.expDescription, c.Worker.Description)
})
}
}
func TestPluginExecutionDir(t *testing.T) {
tests := []struct {
name string
in string
envPluginExecutionDir string
expPluginExecutionDir string
expErr bool
expErrStr string
}{
{
name: "Valid plugin execution dir from env var",
in: `
plugins {
execution_dir = "env://PLUGIN_EXEC_DIR"
}`,
envPluginExecutionDir: `/var/run/boundary/plugin-exec`,
expPluginExecutionDir: `/var/run/boundary/plugin-exec`,
expErr: false,
}, {
name: "Invalid plugin execution dir from env var",
in: `
plugins {
execution_dir ="\ubad plugin directory"
}`,
expErr: true,
expErrStr: "At 3:28: illegal char escape",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("PLUGIN_EXEC_DIR", tt.envPluginExecutionDir)
p, err := Parse(tt.in)
if tt.expErr {
require.EqualError(t, err, tt.expErrStr)
require.Nil(t, p)
return
}
require.NoError(t, err)
require.NotNil(t, p)
require.NotNil(t, p.Plugins)
require.Equal(t, tt.expPluginExecutionDir, p.Plugins.ExecutionDir)
})
}
}
func TestDatabaseMaxConnections(t *testing.T) {
tests := []struct {
name string
in string
envMaxOpenConnections string
expMaxOpenConnections int
expErr bool
expErrStr string
}{
{
name: "Valid integer value",
in: `
controller {
name = "example-controller"
database {
max_open_connections = 5
}
}`,
expMaxOpenConnections: 5,
expErr: false,
},
{
name: "Valid string value",
in: `
controller {
name = "example-controller"
database {
max_open_connections = "5"
}
}`,
expMaxOpenConnections: 5,
expErr: false,
},
{
name: "Invalid value string",
in: `
controller {
name = "example-controller"
database {
max_open_connections = "string bad"
}
}`,
expErr: true,
expErrStr: "Database max open connections value is not an int: " +
"strconv.Atoi: parsing \"string bad\": invalid syntax",
},
{
name: "Invalid value type",
in: `
controller {
name = "example-controller"
database {
max_open_connections = false
}
}`,
expErr: true,
expErrStr: "Database max open connections: unsupported type \"bool\"",
},
{
name: "Valid env var",
in: `
controller {
name = "example-controller"
database {
max_open_connections = "env://ENV_MAX_CONN"
}
}`,
expMaxOpenConnections: 8,
envMaxOpenConnections: "8",
expErr: false,
},
{
name: "Invalid env var",
in: `
controller {
name = "example-controller"
database {
max_open_connections = "env://ENV_MAX_CONN"
}
}`,
envMaxOpenConnections: "bogus value",
expErr: true,
expErrStr: "Database max open connections value is not an int: " +
"strconv.Atoi: parsing \"bogus value\": invalid syntax",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("ENV_MAX_CONN", tt.envMaxOpenConnections)
c, err := Parse(tt.in)
if tt.expErr {
require.EqualError(t, err, tt.expErrStr)
require.Nil(t, c)
return
}
require.NoError(t, err)
require.NotNil(t, c)
require.NotNil(t, c.Controller)
require.NotNil(t, c.Controller.Database)
require.Equal(t, tt.expMaxOpenConnections, c.Controller.Database.MaxOpenConnections)
})
}
}
func TestDatabaseMaxIdleConnections(t *testing.T) {
tests := []struct {
name string
in string
envMaxIdleConnections string
expMaxIdleConnections int
expErr bool
expErrStr string
}{
{
name: "Valid integer value",
in: `
controller {
name = "example-controller"
database {
max_idle_connections = 5
}
}`,
expMaxIdleConnections: 5,
expErr: false,
},
{
name: "Valid integer string",
in: `
controller {
name = "example-controller"
database {
max_idle_connections = "5"
}
}`,
expMaxIdleConnections: 5,
expErr: false,
},
{
name: "Invalid value string",
in: `
controller {
name = "example-controller"
database {
max_idle_connections = "string bad"
}
}`,
expErr: true,
expErrStr: "Database max idle connections value is not a uint: " +
"strconv.Atoi: parsing \"string bad\": invalid syntax",
},
{
name: "Invalid value type",
in: `
controller {
name = "example-controller"
database {
max_idle_connections = false
}
}`,
expErr: true,
expErrStr: "Database max idle connections: unsupported type \"bool\"",
},
{
name: "Valid env var",
in: `
controller {
name = "example-controller"
database {
max_idle_connections = "env://ENV_MAX_IDLE_CONN"
}
}`,
expMaxIdleConnections: 8,
envMaxIdleConnections: "8",
expErr: false,
},
{
name: "Invalid env var",
in: `
controller {
name = "example-controller"
database {
max_idle_connections = "env://ENV_MAX_IDLE_CONN"
}
}`,
envMaxIdleConnections: "bogus value",
expErr: true,
expErrStr: "Database max idle connections value is not a uint: " +
"strconv.Atoi: parsing \"bogus value\": invalid syntax",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("ENV_MAX_IDLE_CONN", tt.envMaxIdleConnections)
c, err := Parse(tt.in)
if tt.expErr {
require.EqualError(t, err, tt.expErrStr)
require.Nil(t, c)
return
}
require.NoError(t, err)
require.NotNil(t, c)
require.NotNil(t, c.Controller)
require.NotNil(t, c.Controller.Database)
require.Equal(t, tt.expMaxIdleConnections, *c.Controller.Database.MaxIdleConnections)
})
}
}
func TestDatabaseConnMaxIdleTimeDuration(t *testing.T) {
tests := []struct {
name string
in string
envConnMaxIdleTimeDuration string
expConnMaxIdleTimeDuration time.Duration
expErr bool
expErrStr string
}{
{
name: "Valid duration value",
in: `
controller {
name = "example-controller"
database {
max_idle_time = "5m"
}
}`,
expConnMaxIdleTimeDuration: time.Minute * 5,
expErr: false,
},
{
name: "Valid env var value",
envConnMaxIdleTimeDuration: "5m",
in: `
controller {
name = "example-controller"
database {
max_idle_time = "env://ENV_CONN_MAX_IDLE_TIME"
}
}`,
expConnMaxIdleTimeDuration: time.Minute * 5,
expErr: false,
},
{
name: "Invalid value string",
in: `
controller {
name = "example-controller"
database {
max_idle_time = "string bad"
}
}`,
expErr: true,
expErrStr: "Connection max idle time is not a duration: " +
"strconv.ParseInt: parsing \"string ba\": invalid syntax",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("ENV_CONN_MAX_IDLE_TIME", tt.envConnMaxIdleTimeDuration)
c, err := Parse(tt.in)
if tt.expErr {
require.EqualError(t, err, tt.expErrStr)
require.Nil(t, c)
return
}
require.NoError(t, err)
require.NotNil(t, c)
require.NotNil(t, c.Controller)
require.NotNil(t, c.Controller.Database)
require.Equal(t, tt.expConnMaxIdleTimeDuration, *c.Controller.Database.ConnMaxIdleTimeDuration)
})
}
}
func TestDatabaseSkipSharedLockAcquisition(t *testing.T) {
tests := []struct {
name string
in string
expSkipSharedLockAcquisition bool
}{
{
name: "not set",
in: `
controller {
name = "example-controller"
database {
}
}`,
expSkipSharedLockAcquisition: false,
},
{
name: "set",
in: `
controller {
name = "example-controller"
database {
skip_shared_lock_acquisition = true
}
}`,
expSkipSharedLockAcquisition: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c, err := Parse(tt.in)
require.NoError(t, err)
require.NotNil(t, c)
require.NotNil(t, c.Controller)
require.NotNil(t, c.Controller.Database)
require.Equal(t, tt.expSkipSharedLockAcquisition, c.Controller.Database.SkipSharedLockAcquisition)
})
}
}
func TestSetupControllerPublicClusterAddress(t *testing.T) {
tests := []struct {
name string
inputConfig *Config
inputFlagValue string
stateFn func(t *testing.T)
expErr bool
expErrStr string
expPublicClusterAddress string
}{
{
name: "nil controller",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: nil,
},
inputFlagValue: "",
expErr: false,
expErrStr: "",
expPublicClusterAddress: ":9201",
},
{
name: "setting public cluster address directly with ipv4",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{
PublicClusterAddr: "127.0.0.1",
},
},
inputFlagValue: "",
expErr: false,
expErrStr: "",
expPublicClusterAddress: "127.0.0.1:9201",
},
{
name: "setting public cluster address directly with ipv4:port",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{
PublicClusterAddr: "127.0.0.1:8080",
},
},
inputFlagValue: "",
expErr: false,
expErrStr: "",
expPublicClusterAddress: "127.0.0.1:8080",
},
{
name: "setting public cluster address directly with ipv6",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{
PublicClusterAddr: "[2001:4860:4860:0:0:0:0:8888]",
},
},
inputFlagValue: "",
expErr: false,
expErrStr: "",
expPublicClusterAddress: "[2001:4860:4860:0:0:0:0:8888]:9201",
},
{
name: "setting public cluster address directly with ipv6:port",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{
PublicClusterAddr: "[2001:4860:4860:0:0:0:0:8888]:8080",
},
},
inputFlagValue: "",
expErr: false,
expErrStr: "",
expPublicClusterAddress: "[2001:4860:4860:0:0:0:0:8888]:8080",
},
{
name: "setting public cluster address directly with abbreviated ipv6",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{
PublicClusterAddr: "[2001:4860:4860::8888]",
},
},
inputFlagValue: "",
expErr: false,
expErrStr: "",
expPublicClusterAddress: "[2001:4860:4860::8888]:9201",
},
{
name: "setting public cluster address directly with abbreviated ipv6:port",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{
PublicClusterAddr: "[2001:4860:4860::8888]:8080",
},
},
inputFlagValue: "",
expErr: false,
expErrStr: "",
expPublicClusterAddress: "[2001:4860:4860::8888]:8080",
},
{
name: "setting public cluster address to env var",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{
PublicClusterAddr: "env://TEST_ENV_VAR_FOR_CONTROLLER_ADDR",
},
},
inputFlagValue: "",
stateFn: func(t *testing.T) {
t.Setenv("TEST_ENV_VAR_FOR_CONTROLLER_ADDR", "127.0.0.1:8080")
},
expErr: false,
expErrStr: "",
expPublicClusterAddress: "127.0.0.1:8080",
},
{
name: "setting public cluster address to env var that points to template",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{
PublicClusterAddr: "env://TEST_ENV_VAR_FOR_CONTROLLER_ADDR",
},
},
inputFlagValue: "",
stateFn: func(t *testing.T) {
t.Setenv("TEST_ENV_VAR_FOR_CONTROLLER_ADDR", `{{ GetAllInterfaces | include "flags" "loopback" | include "type" "IPV4" | join "address" " " }}`)
},
expErr: false,
expErrStr: "",
expPublicClusterAddress: "127.0.0.1:9201",
},
{
name: "setting public cluster address to ip template",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{
PublicClusterAddr: `{{ GetAllInterfaces | include "flags" "loopback" | include "type" "IPV4" | join "address" " " }}`,
},
},
inputFlagValue: "",
expErr: false,
expErrStr: "",
expPublicClusterAddress: "127.0.0.1:9201",
},
{
name: "setting public cluster address to multiline ip template",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{
PublicClusterAddr: `{{ with $local := GetAllInterfaces | include "flags" "loopback" | include "type" "IPV4" -}}
{{- $local | join "address" " " -}}
{{- end }}`,
},
},
inputFlagValue: "",
expErr: false,
expErrStr: "",
expPublicClusterAddress: "127.0.0.1:9201",
},
{
name: "using flag value with ip only",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{},
},
inputFlagValue: "127.0.0.1",
expErr: false,
expErrStr: "",
expPublicClusterAddress: "127.0.0.1:9201",
},
{
name: "using flag value with ip:port",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{},
},
inputFlagValue: "127.0.0.1:8080",
expErr: false,
expErrStr: "",
expPublicClusterAddress: "127.0.0.1:8080",
},
{
name: "using flag value with ip template",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{},
},
inputFlagValue: `{{ GetAllInterfaces | include "flags" "loopback" | include "type" "IPV4" | join "address" " " }}`,
expErr: false,
expErrStr: "",
expPublicClusterAddress: "127.0.0.1:9201",
},
{
name: "using flag value with multiline ip template",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{},
},
inputFlagValue: `{{ with $local := GetAllInterfaces | include "flags" "loopback" | include "type" "IPV4" -}}
{{- $local | join "address" " " -}}
{{- end }}`,
expErr: false,
expErrStr: "",
expPublicClusterAddress: "127.0.0.1:9201",
},
{
name: "using flag value to point to env var with ip only",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{},
},
inputFlagValue: "env://TEST_ENV_VAR_FOR_CONTROLLER_ADDR",
stateFn: func(t *testing.T) {
t.Setenv("TEST_ENV_VAR_FOR_CONTROLLER_ADDR", "127.0.0.1")
},
expErr: false,
expErrStr: "",
expPublicClusterAddress: "127.0.0.1:9201",
},
{
name: "using flag value to point to env var with ip:port",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{},
},
inputFlagValue: "env://TEST_ENV_VAR_FOR_CONTROLLER_ADDR",
stateFn: func(t *testing.T) {
t.Setenv("TEST_ENV_VAR_FOR_CONTROLLER_ADDR", "127.0.0.1:8080")
},
expErr: false,
expErrStr: "",
expPublicClusterAddress: "127.0.0.1:8080",
},
{
name: "read address from listeners ipv4 only",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{
{Purpose: []string{"cluster"}, Address: "127.0.0.1"},
},
},
Controller: &Controller{},
},
expErr: false,
expErrStr: "",
expPublicClusterAddress: "127.0.0.1:9201",
},
{
name: "read address from listeners ipv4:port",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{
{Purpose: []string{"cluster"}, Address: "127.0.0.1:8080"},
},
},
Controller: &Controller{},
},
expErr: false,
expErrStr: "",
expPublicClusterAddress: "127.0.0.1:8080",
},
{
name: "read address from listeners ipv6 only",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{
{Purpose: []string{"cluster"}, Address: "[2001:4860:4860:0:0:0:0:8888]"},
},
},
Controller: &Controller{},
},
expErr: false,
expErrStr: "",
expPublicClusterAddress: "[2001:4860:4860:0:0:0:0:8888]:9201",
},
{
name: "read address from listeners ipv6:port",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{
{Purpose: []string{"cluster"}, Address: "[2001:4860:4860:0:0:0:0:8888]:8080"},
},
},
Controller: &Controller{},
},
expErr: false,
expErrStr: "",
expPublicClusterAddress: "[2001:4860:4860:0:0:0:0:8888]:8080",
},
{
name: "read address from listeners abbreviated ipv6 only",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{
{Purpose: []string{"cluster"}, Address: "[2001:4860:4860::8888]"},
},
},
Controller: &Controller{},
},
expErr: false,
expErrStr: "",
expPublicClusterAddress: "[2001:4860:4860::8888]:9201",
},
{
name: "read address from listeners abbreviated ipv6:port",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{
{Purpose: []string{"cluster"}, Address: "[2001:4860:4860::8888]:8080"},
},
},
Controller: &Controller{},
},
expErr: false,
expErrStr: "",
expPublicClusterAddress: "[2001:4860:4860::8888]:8080",
},
{
name: "read address from listeners is ignored on different purpose",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{
{Purpose: []string{"somethingelse"}, Address: "127.0.0.1:8080"},
},
},
Controller: &Controller{},
},
expErr: false,
expErrStr: "",
expPublicClusterAddress: ":9201",
},
{
name: "using flag value to point to nonexistent file",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{},
},
inputFlagValue: "file://this_doesnt_exist_for_sure",
expErr: true,
expErrStr: "Error parsing public cluster addr: error reading file at file://this_doesnt_exist_for_sure: open this_doesnt_exist_for_sure: no such file or directory",
expPublicClusterAddress: "",
},
{
name: "using flag value to provoke error in SplitHostPort",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{},
},
inputFlagValue: "abc::123",
expErr: true,
expErrStr: "Error splitting public cluster adddress host/port: address abc::123: too many colons in address",
expPublicClusterAddress: "",
},
{
name: "bad ip template",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{},
},
inputFlagValue: "{{ somethingthatdoesntexist }}",
expErr: true,
expErrStr: "Error parsing IP template on controller public cluster addr: unable to parse address template \"{{ somethingthatdoesntexist }}\": unable to parse template \"{{ somethingthatdoesntexist }}\": template: sockaddr.Parse:1: function \"somethingthatdoesntexist\" not defined",
expPublicClusterAddress: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.stateFn != nil {
tt.stateFn(t)
}
err := tt.inputConfig.SetupControllerPublicClusterAddress(tt.inputFlagValue)
if tt.expErr {
require.EqualError(t, err, tt.expErrStr)
return
}
require.NoError(t, err)
require.NotNil(t, tt.inputConfig.Controller)
require.Equal(t, tt.expPublicClusterAddress, tt.inputConfig.Controller.PublicClusterAddr)
})
}
}
func TestSetupWorkerInitialUpstreams(t *testing.T) {
t.Parallel()
tests := []struct {
name string
inputConfig *Config
stateFn func(t *testing.T)
expErr bool
expErrStr string
expInitialUpstreams []string
}{
{
name: "NilController",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: nil,
Worker: &Worker{
InitialUpstreams: []string{"192.168.0.2:9201"},
},
},
expErr: false,
expErrStr: "",
expInitialUpstreams: []string{"192.168.0.2:9201"},
},
{
name: "NilWorker",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{
PublicClusterAddr: "192.168.0.3:9201",
},
Worker: nil,
},
expErr: false,
expErrStr: "",
expInitialUpstreams: nil,
},
{
name: "ipv4 PublicClusterAddr",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{
PublicClusterAddr: "192.168.0.4:9201",
},
Worker: &Worker{},
},
expErr: false,
expErrStr: "",
expInitialUpstreams: []string{"192.168.0.4:9201"},
},
{
name: "ipv6 PublicClusterAddr",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{
PublicClusterAddr: "[2001:4860:4860:0:0:0:0:8888]:9201",
},
Worker: &Worker{},
},
expErr: false,
expErrStr: "",
expInitialUpstreams: []string{"[2001:4860:4860:0:0:0:0:8888]:9201"},
},
{
name: "abbreviated ipv6 PublicClusterAddr",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{
PublicClusterAddr: "[2001:4860:4860::8888]:9201",
},
Worker: &Worker{},
},
expErr: false,
expErrStr: "",
expInitialUpstreams: []string{"[2001:4860:4860::8888]:9201"},
},
{
name: "ListenerNoAddr",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{
{
Purpose: []string{"cluster"},
},
},
},
Controller: &Controller{},
Worker: &Worker{},
},
expErr: false,
expErrStr: "",
expInitialUpstreams: []string{"127.0.0.1:9201"},
},
{
name: "ipv4 ListenerAddr",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{
{
Purpose: []string{"cluster"},
Address: "192.168.0.5:9201",
},
},
},
Controller: &Controller{},
Worker: &Worker{},
},
expErr: false,
expErrStr: "",
expInitialUpstreams: []string{"192.168.0.5:9201"},
},
{
name: "ipv6 ListenerAddr",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{
{
Purpose: []string{"cluster"},
Address: "[2001:4860:4860:0:0:0:0:8888]:9201",
},
},
},
Controller: &Controller{},
Worker: &Worker{},
},
expErr: false,
expErrStr: "",
expInitialUpstreams: []string{"[2001:4860:4860:0:0:0:0:8888]:9201"},
},
{
name: "abbreviated ipv6 ListenerAddr",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{
{
Purpose: []string{"cluster"},
Address: "[2001:4860:4860::8888]:9201",
},
},
},
Controller: &Controller{},
Worker: &Worker{},
},
expErr: false,
expErrStr: "",
expInitialUpstreams: []string{"[2001:4860:4860::8888]:9201"},
},
{
name: "ListenerAddrDomain",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{
{
Purpose: []string{"cluster"},
Address: "foo.test",
},
},
},
Controller: &Controller{},
Worker: &Worker{},
},
expErr: false,
expErrStr: "",
expInitialUpstreams: []string{"foo.test"},
},
{
name: "ListenerAddrMultiplePurpose",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{
{
Purpose: []string{"cluster", "api"},
},
},
},
Controller: &Controller{},
Worker: &Worker{},
},
expErr: true,
expErrStr: "Specifying a listener with more than one purpose is not supported",
expInitialUpstreams: nil,
},
{
name: "ListenerAddrNoPurposes",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{
{
Purpose: []string{},
},
},
},
Controller: &Controller{},
Worker: &Worker{},
},
expErr: true,
expErrStr: "Listener specified without a purpose",
expInitialUpstreams: nil,
},
{
name: "ListenerAddrMismatchAddress",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{
{
Purpose: []string{"cluster"},
Address: "192.168.0.5:9201",
},
},
},
Controller: &Controller{},
Worker: &Worker{
InitialUpstreams: []string{"192.168.0.2:9201"},
},
},
expErr: true,
expErrStr: `When running a combined controller and worker, it's invalid to specify a "initial_upstreams" or "controllers" key in the worker block with any values other than the controller cluster or upstream worker address/port when using IPs rather than DNS names`,
expInitialUpstreams: nil,
},
{
name: "ClusterAddrMismatchAddress",
inputConfig: &Config{
SharedConfig: &configutil.SharedConfig{
Listeners: []*listenerutil.ListenerConfig{},
},
Controller: &Controller{
PublicClusterAddr: "192.168.0.3:9201",
},
Worker: &Worker{
InitialUpstreams: []string{"192.168.0.2:9201"},
},
},
expErr: true,
expErrStr: `When running a combined controller and worker, it's invalid to specify a "initial_upstreams" or "controllers" key in the worker block with any values other than the controller cluster or upstream worker address/port when using IPs rather than DNS names`,
expInitialUpstreams: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.stateFn != nil {
tt.stateFn(t)
}
err := tt.inputConfig.SetupWorkerInitialUpstreams()
if tt.expErr {
require.EqualError(t, err, tt.expErrStr)
return
}
require.NoError(t, err)
if tt.inputConfig.Worker == nil {
require.Empty(t, tt.expInitialUpstreams)
} else {
require.ElementsMatch(t, tt.expInitialUpstreams, tt.inputConfig.Worker.InitialUpstreams)
}
})
}
}
func TestGetDownstreamWorkersTimeout(t *testing.T) {
tests := []struct {
name string
in string
wantController bool
wantWorker bool
wantControllerTimeout time.Duration
wantWorkerTimeout time.Duration
assertErr func(*testing.T, error)
}{
{
name: "controller_valid_time_value",
in: `
controller {
name = "example-controller"
get_downstream_workers_timeout = "10s"
}`,
wantControllerTimeout: 10 * time.Second,
wantWorkerTimeout: 0,
wantController: true,
wantWorker: false,
assertErr: nil,
},
{
name: "worker_valid_time_value",
in: `
worker {
name = "example-worker"
get_downstream_workers_timeout = "5s"
}`,
wantControllerTimeout: 0,
wantWorkerTimeout: 5 * time.Second,
wantController: false,
wantWorker: true,
assertErr: nil,
},
{
name: "both_valid_time_value",
in: `
controller {
name = "example-controller"
get_downstream_workers_timeout = "5s"
}
worker {
name = "example-worker"
get_downstream_workers_timeout = "500ms"
}`,
wantControllerTimeout: 5 * time.Second,
wantWorkerTimeout: 500 * time.Millisecond,
wantController: true,
wantWorker: true,
assertErr: nil,
},
{
name: "both_unspecified_defaults_to_zero",
in: `
controller {
name = "example-controller"
}
worker {
name = "example-worker"
}`,
wantController: true,
wantWorker: true,
wantControllerTimeout: 0,
wantWorkerTimeout: 0,
assertErr: nil,
},
{
name: "controller_int_value_no_unit_assumes_seconds",
in: `
controller {
name = "example-controller"
get_downstream_workers_timeout = 100
}`,
wantController: true,
wantWorker: false,
wantControllerTimeout: 100 * time.Second,
wantWorkerTimeout: 0,
},
{
name: "worker_int_value_no_unit_assumes_seconds",
in: `
worker {
name = "example-worker"
get_downstream_workers_timeout = 30
}`,
wantController: false,
wantWorker: true,
wantWorkerTimeout: 30 * time.Second,
},
{
name: "controller_invalid_bool_value",
in: `
controller {
name = "example-controller"
get_downstream_workers_timeout = true
}`,
wantController: true,
wantWorker: false,
assertErr: func(t *testing.T, err error) {
require.Error(t, err)
require.ErrorContains(t, err, `error trying to parse controller get_downstream_workers_timeout`)
},
},
{
name: "worker_invalid_bool_value",
in: `
worker {
name = "example-worker"
get_downstream_workers_timeout = false
}`,
wantController: false,
wantWorker: true,
assertErr: func(t *testing.T, err error) {
require.Error(t, err)
require.ErrorContains(t, err, `error trying to parse worker get_downstream_workers_timeout`)
},
},
{
name: "controller_invalid_empty_value",
in: `
controller {
name = "example-controller"
get_downstream_workers_timeout = ""
}`,
wantController: true,
wantWorker: false,
wantControllerTimeout: 0,
},
{
name: "worker_invalid_empty_value",
in: `
worker {
name = "example-worker"
get_downstream_workers_timeout = ""
}`,
wantController: false,
wantWorker: true,
wantWorkerTimeout: 0,
},
{
name: "controller_invalid_zero_value",
in: `
controller {
name = "example-controller"
get_downstream_workers_timeout = "0s"
}`,
wantController: true,
wantWorker: false,
wantControllerTimeout: 0,
},
{
name: "worker_invalid_zero_value",
in: `
worker {
name = "example-worker"
get_downstream_workers_timeout = "0s"
}`,
wantController: false,
wantWorker: true,
wantWorkerTimeout: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c, err := Parse(tt.in)
if tt.assertErr != nil {
tt.assertErr(t, err)
return
}
require.NoError(t, err)
if tt.wantController {
require.NotNil(t, c.Controller)
require.Equal(t, tt.wantControllerTimeout, c.Controller.GetDownstreamWorkersTimeoutDuration)
}
if tt.wantWorker {
require.NotNil(t, c.Worker)
require.Equal(t, tt.wantWorkerTimeout, c.Worker.GetDownstreamWorkersTimeoutDuration)
}
})
}
}
func TestMaxPageSize(t *testing.T) {
tests := []struct {
name string
in string
envMaxPageSize string
expMaxPageSize uint
expErr bool
expErrStr string
}{
{
name: "Valid integer value",
in: `
controller {
name = "example-controller"
max_page_size = 5
}`,
expMaxPageSize: 5,
expErr: false,
},
{
name: "Valid string value",
in: `
controller {
name = "example-controller"
max_page_size = "5"
}`,
expMaxPageSize: 5,
expErr: false,
},
{
name: "Invalid value integer",
in: `
controller {
name = "example-controller"
max_page_size = 0
}`,
expErr: true,
expErrStr: "Max page size value must be at least 1, was 0",
},
{
name: "Invalid value string",
in: `
controller {
name = "example-controller"
max_page_size = "string bad"
}`,
expErr: true,
expErrStr: "Max page size value is not an int: " +
"strconv.Atoi: parsing \"string bad\": invalid syntax",
},
{
name: "Invalid value type",
in: `
controller {
name = "example-controller"
max_page_size = false
}`,
expErr: true,
expErrStr: "Max page size: unsupported type \"bool\"",
},
{
name: "Valid env var",
in: `
controller {
name = "example-controller"
max_page_size = "env://ENV_MAX_PAGE_SIZE"
}`,
expMaxPageSize: 8,
envMaxPageSize: "8",
expErr: false,
},
{
name: "Invalid env var",
in: `
controller {
name = "example-controller"
max_page_size = "env://ENV_MAX_PAGE_SIZE"
}`,
envMaxPageSize: "bogus value",
expErr: true,
expErrStr: "Max page size value is not an int: " +
"strconv.Atoi: parsing \"bogus value\": invalid syntax",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("ENV_MAX_PAGE_SIZE", tt.envMaxPageSize)
c, err := Parse(tt.in)
if tt.expErr {
require.EqualError(t, err, tt.expErrStr)
require.Nil(t, c)
return
}
require.NoError(t, err)
require.NotNil(t, c)
require.NotNil(t, c.Controller)
require.Equal(t, tt.expMaxPageSize, c.Controller.MaxPageSize)
})
}
}
func TestConcurrentPasswordHashWorkers(t *testing.T) {
tests := []struct {
name string
in string
envConcurrentPasswordHashWorkers string
expConcurrentPasswordHashWorkers uint
expErr bool
expErrStr string
}{
{
name: "Valid integer value",
in: `
controller {
name = "example-controller"
concurrent_password_hash_workers = 5
}`,
expConcurrentPasswordHashWorkers: 5,
expErr: false,
},
{
name: "Valid string value",
in: `
controller {
name = "example-controller"
concurrent_password_hash_workers = "5"
}`,
expConcurrentPasswordHashWorkers: 5,
expErr: false,
},
{
name: "Invalid value integer",
in: `
controller {
name = "example-controller"
concurrent_password_hash_workers = 0
}`,
expErr: true,
expErrStr: "Concurrent password hash workers value must be at least 1, was 0",
},
{
name: "Invalid value string",
in: `
controller {
name = "example-controller"
concurrent_password_hash_workers = "string bad"
}`,
expErr: true,
expErrStr: "Concurrent password hash workers value is not an int: " +
"strconv.Atoi: parsing \"string bad\": invalid syntax",
},
{
name: "Invalid value string integer",
in: `
controller {
name = "example-controller"
concurrent_password_hash_workers = "-1"
}`,
expErr: true,
expErrStr: "Concurrent password hash workers value must be at least 1, was -1",
},
{
name: "Invalid value type",
in: `
controller {
name = "example-controller"
concurrent_password_hash_workers = false
}`,
expErr: true,
expErrStr: "Concurrent password hash workers: unsupported type \"bool\"",
},
{
name: "Valid env var",
in: `
controller {
name = "example-controller"
concurrent_password_hash_workers = "env://ENV_MAX_PW_WORKERS"
}`,
expConcurrentPasswordHashWorkers: 8,
envConcurrentPasswordHashWorkers: "8",
expErr: false,
},
{
name: "Invalid env var",
in: `
controller {
name = "example-controller"
concurrent_password_hash_workers = "env://ENV_MAX_PW_WORKERS"
}`,
envConcurrentPasswordHashWorkers: "bogus value",
expErr: true,
expErrStr: "Concurrent password hash workers value is not an int: " +
"strconv.Atoi: parsing \"bogus value\": invalid syntax",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("ENV_MAX_PW_WORKERS", tt.envConcurrentPasswordHashWorkers)
c, err := Parse(tt.in)
if tt.expErr {
require.EqualError(t, err, tt.expErrStr)
require.Nil(t, c)
return
}
require.NoError(t, err)
require.NotNil(t, c)
require.NotNil(t, c.Controller)
require.Equal(t, tt.expConcurrentPasswordHashWorkers, c.Controller.ConcurrentPasswordHashWorkers)
})
}
t.Run("using environment variable", func(t *testing.T) {
t.Setenv("BOUNDARY_CONTROLLER_CONCURRENT_PASSWORD_HASH_WORKERS", "2")
in := `
controller {
name = "example-controller"
}`
c, err := Parse(in)
require.NoError(t, err)
require.NotNil(t, c)
require.NotNil(t, c.Controller)
require.EqualValues(t, 2, c.Controller.ConcurrentPasswordHashWorkers)
})
t.Run("using environment variable with invalid value", func(t *testing.T) {
t.Setenv("BOUNDARY_CONTROLLER_CONCURRENT_PASSWORD_HASH_WORKERS", "invalid")
in := `
controller {
name = "example-controller"
}`
c, err := Parse(in)
require.Error(t, err)
require.Nil(t, c)
require.Contains(t, err.Error(), "BOUNDARY_CONTROLLER_CONCURRENT_PASSWORD_HASH_WORKERS value is not an int")
})
t.Run("using environment variable and config value uses config value", func(t *testing.T) {
t.Setenv("BOUNDARY_CONTROLLER_CONCURRENT_PASSWORD_HASH_WORKERS", "2")
in := `
controller {
name = "example-controller"
concurrent_password_hash_workers = 3
}`
c, err := Parse(in)
require.NoError(t, err)
require.NotNil(t, c)
require.NotNil(t, c.Controller)
require.EqualValues(t, 3, c.Controller.ConcurrentPasswordHashWorkers)
})
}