diff --git a/CHANGELOG.md b/CHANGELOG.md index c8773ef153..7b98c00395 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,16 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. ## Next ### New and Improved + * config: The `execution_dir` field for plugins now supports being set from environment variables or a file on disk.([PR](https://github.com/hashicorp/boundary/pull/1772)) +* config: Add support for reading worker controllers off of environment + variables as well as files. ([PR](https://github.com/hashicorp/boundary/pull/1765)) * config: The `description` field for controllers now supports being set from environment variables or a file on disk ([PR](https://github.com/hashicorp/boundary/pull/1766)) * config: Add support for reading worker tags off of environment variables -as well as files. ([PR](https://github.com/hashicorp/boundary/pull/1758)) + as well as files. ([PR](https://github.com/hashicorp/boundary/pull/1758)) * config: Add support for go-sockaddr templates to Worker and Controller addresses. ([PR](https://github.com/hashicorp/boundary/pull/1731)) * host: Plugin-based host catalogs will now schedule updates for all diff --git a/internal/cmd/config/config.go b/internal/cmd/config/config.go index 63d62f767f..e877691049 100644 --- a/internal/cmd/config/config.go +++ b/internal/cmd/config/config.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "io/ioutil" + "reflect" "strings" "time" @@ -151,10 +152,15 @@ func (c *Controller) InitNameIfEmpty() (string, error) { } type Worker struct { - Name string `hcl:"name"` - Description string `hcl:"description"` - Controllers []string `hcl:"controllers"` - PublicAddr string `hcl:"public_addr"` + Name string `hcl:"name"` + Description string `hcl:"description"` + PublicAddr string `hcl:"public_addr"` + + // We use a raw interface here so that we can take in a string + // value pointing to an env var or file. We then resolve that + // and get the actual controller addresses. + Controllers []string `hcl:"-"` + ControllersRaw interface{} `hcl:"controllers"` // We use a raw interface for parsing so that people can use JSON-like // syntax that maps directly to the filter input or possibly more familiar @@ -448,6 +454,11 @@ func Parse(d string) (*Config, error) { } } } + + result.Worker.Controllers, err = parseWorkerControllers(result) + if err != nil { + return nil, fmt.Errorf("Failed to parse worker controllers: %w", err) + } } sharedConfig, err := configutil.ParseConfig(d) @@ -506,6 +517,42 @@ func Parse(d string) (*Config, error) { return result, nil } +func parseWorkerControllers(c *Config) ([]string, error) { + if c == nil || c.Worker == nil { + return nil, fmt.Errorf("config or worker field is nil") + } + if c.Worker.ControllersRaw == nil { + return nil, nil + } + + switch t := c.Worker.ControllersRaw.(type) { + case []interface{}: // An array was configured directly in Boundary's HCL Config file. + var controllers []string + err := mapstructure.WeakDecode(c.Worker.ControllersRaw, &controllers) + if err != nil { + return nil, fmt.Errorf("failed to decode worker controllers block into config field: %w", err) + } + return controllers, nil + + case string: + controllersStr, err := parseutil.ParsePath(t) + if err != nil { + return nil, fmt.Errorf("bad env var or file pointer: %w", err) + } + + var addrs []string + err = json.Unmarshal([]byte(controllersStr), &addrs) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal env/file contents: %w", err) + } + return addrs, nil + + default: + typ := reflect.TypeOf(t) + return nil, fmt.Errorf("unexpected type %q", typ.String()) + } +} + func parseEventing(eventObj *ast.ObjectItem) (*event.EventerConfig, error) { // Decode the outside struct var result event.EventerConfig diff --git a/internal/cmd/config/config_test.go b/internal/cmd/config/config_test.go index 915c7b12f0..cc57a163e0 100644 --- a/internal/cmd/config/config_test.go +++ b/internal/cmd/config/config_test.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/boundary/internal/observability/event" "github.com/hashicorp/go-secure-stdlib/configutil" "github.com/hashicorp/go-secure-stdlib/listenerutil" + "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -173,9 +174,10 @@ func TestDevWorker(t *testing.T) { }, }, Worker: &Worker{ - Name: "dev-worker", - Description: "A default worker created in dev mode", - Controllers: []string{"127.0.0.1"}, + Name: "dev-worker", + Description: "A default worker created in dev mode", + Controllers: []string{"127.0.0.1"}, + ControllersRaw: []interface{}{"127.0.0.1"}, Tags: map[string][]string{ "type": {"dev", "local"}, }, @@ -811,6 +813,146 @@ func TestController_EventingConfig(t *testing.T) { } } +func TestWorkerControllers(t *testing.T) { + tests := []struct { + name string + in string + stateFn func(t *testing.T) + expWorkerControllers []string + expErr bool + expErrIs error + expErrStr string + }{ + { + name: "No Controllers", + in: ` + worker { + name = "test" + } + `, + expWorkerControllers: nil, + expErr: false, + }, + { + name: "One Controller", + in: ` + worker { + name = "test" + controllers = ["127.0.0.1"] + } + `, + expWorkerControllers: []string{"127.0.0.1"}, + expErr: false, + }, + { + name: "Multiple controllers", + in: ` + worker { + name = "test" + controllers = ["127.0.0.1", "127.0.0.2", "127.0.0.3"] + } + `, + expWorkerControllers: []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, + expErr: false, + }, + { + name: "Using env var", + in: ` + worker { + name = "test" + controllers = "env://BOUNDARY_WORKER_CONTROLLERS" + } + `, + stateFn: func(t *testing.T) { t.Setenv("BOUNDARY_WORKER_CONTROLLERS", `["127.0.0.1", "127.0.0.2", "127.0.0.3"]`) }, + expWorkerControllers: []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" + controllers = "env://BOUNDARY_WORKER_CONTROLLERS" + } + `, + stateFn: func(t *testing.T) { + controllers := ` + worker { + controllers = ["127.0.0.1"] + } + ` + t.Setenv("BOUNDARY_WORKER_CONTROLLERS", controllers) + }, + expWorkerControllers: nil, + expErr: true, + expErrStr: "Failed to parse worker controllers: 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" + controllers = "env://BOUNDARY_WORKER_CONTROLLERS" + } + `, + stateFn: func(t *testing.T) { t.Setenv("BOUNDARY_WORKER_CONTROLLERS", `controllers = ["127.0.0.1"]`) }, + expWorkerControllers: nil, + expErr: true, + expErrStr: "Failed to parse worker controllers: failed to unmarshal env/file contents: invalid character 'c' looking for beginning of value", + }, + { + name: "Unsupported object", + in: ` + worker { + name = "test" + controllers = { + ip = "127.0.0.1" + ip = "127.0.0.2" + } + } + `, + expWorkerControllers: nil, + expErr: true, + expErrStr: "Failed to parse worker controllers: unexpected type \"[]map[string]interface {}\"", + }, + { + name: "worker controllers set to invalid url", + in: ` + worker { + name = "test" + controllers = "env://\x00" + }`, + expWorkerControllers: nil, + expErr: true, + expErrIs: parseutil.ErrNotAUrl, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.stateFn != nil { + tt.stateFn(t) + } + + c, err := Parse(tt.in) + if tt.expErr { + if tt.expErrIs != nil { + require.ErrorIs(t, err, tt.expErrIs) + } else { + require.EqualError(t, err, tt.expErrStr) + } + require.Nil(t, c) + return + } + + require.NoError(t, err) + require.NotNil(t, c) + require.NotNil(t, c.Worker) + require.EqualValues(t, tt.expWorkerControllers, c.Worker.Controllers) + }) + } +} + func TestControllerDescription(t *testing.T) { tests := []struct { name string diff --git a/website/content/docs/configuration/worker.mdx b/website/content/docs/configuration/worker.mdx index 93e1e6d172..eec61e3a76 100644 --- a/website/content/docs/configuration/worker.mdx +++ b/website/content/docs/configuration/worker.mdx @@ -34,7 +34,11 @@ worker { or a [go-sockaddr template](https://godoc.org/github.com/hashicorp/go-sockaddr/template). - `controllers` - A list of hosts/IP addresses and optionally ports for reaching - controllers. The port will default to :9201 if not specified. + controllers. The port will default to :9201 if not specified. This value can be + a direct access string array with the addresses, or it can refer to a file on + disk (file://) from which the addresses will be read, or an env var (env://) from + which the addresses will be read. When using env or file, their contents + must formatted as a JSON array: `["127.0.0.1", "192.168.0.1", "10.0.0.1"]` - `tags` - A map of key-value pairs where values are an array of strings. Most commonly used for [filtering](/docs/concepts/filtering) targets a worker can