Files
threshold/internal/cmd/config/config_test.go
T
Jeff Mitchell b000aa3068 Fix dev key generation (#3951)
In #2076 the refactor changed the position of a parenthesis, causing
the dev key to truncate the base64 representation instead of specifying
which bytes to base64 encode.

We don't need this slicing anymore anyways since we now do individual
key generation calls so just remove it.
2023-10-31 10:32:46 -04:00

2479 lines
61 KiB
Go

// 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)
}
})
}
}