Files
threshold/internal/cmd/config/config_test.go
T
Hugo Vieira 1576af70cc feat(config): Add env and file support to Worker Tags
This feature allows the user to set the `tags` field to a string
that points to either an environment variable or file.
We then dynamically pull out the value and parse it into the config.

This can also be done at the keys level, where you set your keys
directly in Boundary's config file, but point the values to env/file.
2021-12-07 19:21:04 +00:00

813 lines
18 KiB
Go

package config
import (
"fmt"
"os"
"testing"
"time"
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/go-secure-stdlib/configutil"
"github.com/hashicorp/go-secure-stdlib/listenerutil"
"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
exp := &Config{
Eventing: event.DefaultEventerConfig(),
SharedConfig: &configutil.SharedConfig{
DisableMlock: true,
Listeners: []*listenerutil.ListenerConfig{
{
Type: "tcp",
Purpose: []string{"api"},
TLSDisable: true,
CorsEnabled: truePointer,
CorsAllowedOrigins: []string{"*"},
},
{
Type: "tcp",
Purpose: []string{"cluster"},
},
},
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{"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.Listeners[0].RawConfig = actual.Listeners[0].RawConfig
exp.Listeners[1].RawConfig = actual.Listeners[1].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.DevControllerKey = actual.Seals[0].Config["key"]
exp.DevWorkerAuthKey = actual.Seals[1].Config["key"]
exp.DevRecoveryKey = actual.Seals[2].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()
if err != nil {
t.Fatal(err)
}
exp := &Config{
Eventing: event.DefaultEventerConfig(),
SharedConfig: &configutil.SharedConfig{
DisableMlock: true,
Listeners: []*listenerutil.ListenerConfig{
{
Type: "tcp",
Purpose: []string{"proxy"},
},
},
},
Worker: &Worker{
Name: "dev-worker",
Description: "A default worker created in dev mode",
Controllers: []string{"127.0.0.1"},
Tags: map[string][]string{
"type": {"dev", "local"},
},
},
}
exp.Listeners[0].RawConfig = actual.Listeners[0].RawConfig
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 = "dev-worker"
description = "A default worker created in dev mode"
controllers = ["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.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 = "dev-worker"
description = "A default worker created in dev mode"
controllers = ["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 = "dev-worker"
description = "A default worker created in dev mode"
controllers = ["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 = "dev-worker"
description = "A default worker created in dev mode"
controllers = ["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"
controllers = ["127.0.0.1"]
tags = ["type=dev", "type=local"]
}
`
_, err = Parse(devConfig + devWorkerKeyValueConfig)
assert.Error(t, err)
}
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: "barfoo",
expectedController: "foobar",
expectedWorker: "barfoo",
},
{
name: "env",
templateController: fmt.Sprintf("env://%s", controllerEnv),
templateWorker: fmt.Sprintf("env://%s", workerEnv),
envController: "foobar2",
envWorker: "barfoo2",
expectedController: "foobar2",
expectedWorker: "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 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 = "testworker"
}
`,
expWorkerTags: nil,
expErr: false,
},
{
name: "empty tags",
in: `
worker {
name = "testworker"
tags = {}
}
`,
expWorkerTags: map[string][]string{},
expErr: false,
},
{
name: "empty tags 2",
in: `
worker {
name = "testworker"
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: "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 unmarshalling 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{},
},
},
},
},
}
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)
}
})
}
}