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

2479 lines
61 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package config
import (
"encoding/base64"
"fmt"
"net/http"
"os"
"testing"
"time"
"github.com/hashicorp/boundary/internal/event"
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",
},
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",
},
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 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 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: "One Upstream",
in: `
worker {
name = "test"
initial_upstreams = ["127.0.0.1"]
}
`,
expWorkerUpstreams: []string{"127.0.0.1"},
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,
},
{
name: "Worker using deprecated controllers field",
in: `
worker {
name = "test"
controllers = ["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: "Different values in controllers and initial_upstreams field",
in: `
worker {
name = "test"
controllers = ["127.0.0.1", "127.0.0.2", "127.0.0.3"]
initial_upstreams = ["127.0.0.1"]
}`,
expWorkerUpstreams: nil,
expErr: true,
expErrStr: "Failed to parse worker upstreams: both initial_upstreams and controllers fields are populated",
},
{
name: "Identical values in controllers and initial_upstreams field",
in: `
worker {
name = "test"
controllers = ["127.0.0.1"]
initial_upstreams = ["127.0.0.1"]
}`,
expWorkerUpstreams: nil,
expErr: true,
expErrStr: "Failed to parse worker upstreams: both initial_upstreams and controllers fields are populated",
},
}
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 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 ip",
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 ip: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 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 ip 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 ip: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 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: "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: "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: "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: "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)
}
})
}
}