* oss: feat(proto): statistics rpc added to server coordination service * oss: feat(session): add update statistics status service * oss: feat(handlers): add Statistics method to worker service server * oss: feat(worker): add statistics ticking * oss: internal/proto: define the new RoutingInfo RPC * oss: internal/server: repository changes for RoutingInfo RPC * oss: internal/daemon: add RoutingInfo handler to worker service The new RoutingInfo RPC is used to get information from the worker that is used to make routing decisions on the controller. It also handles the initial request from the worker. * oss: feat(db): server_worker_session_info table * oss: feat(proto): session info rpc for server coordination service * oss: feat(server): worker session info request data type * oss: feat(worker): session info ticker * oss: feat(handler): server coordination handler for session info * oss: feat(session): utilize session info request time for session cleanup * oss: proto: fix some gotags comments Some of these fields were missing comments, while others had incorrectly formatted comments. * oss: worker: send connection status to server This is required for the correct tracking of orphaned connections * oss: internal/daemon/worker: add function to wait for Statistics RPC * oss: internal/daemon/worker: add routing info RPC The new RoutingInfo RPC replaces part of the Status RPC, namely the part which is used by the controller and worker to perform decisions around the establishment of new sessions and new worker connections. * oss: internal/daemon/worker: remove unnecessary error check * oss: internal/daemon/worker: lock access to config and upstreams The new confLock protects access to the conf value and to the addressReceivers slice. * oss: worker: replace Status RPC with RoutingInfo * oss: worker: deprecate the Status RPC and related types The Status RPC and related messages have been replaced by the RoutingInfo/SessionInfo/Statistics RPCs. The controller will continue to support the Status RPC until two new minor versions have been released. * oss: internal/event: add default deny for new worker RPCs * oss: internal/daemon/worker: allow RPC interval to be set The new SessionInfo/RoutingInfo/Statistics RPCs run at long intervals, which make some of our cluster tests take a long time to complete. This allows their interval to be configured in tests. * oss: all: use TestWorkerRPCInterval in slow tests This should speed up these tests significantly. * oss: internal/tests/cluster: reset sys eventer in test --------- Co-authored-by: Damian Debkowski <damian.debkowski@hashicorp.com>
1442 lines
50 KiB
Go
1442 lines
50 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/boundary/globals"
|
|
"github.com/hashicorp/boundary/internal/db"
|
|
"github.com/hashicorp/boundary/internal/event"
|
|
"github.com/hashicorp/boundary/internal/ratelimit"
|
|
"github.com/hashicorp/boundary/internal/storage"
|
|
"github.com/hashicorp/boundary/internal/util"
|
|
kms_plugin_assets "github.com/hashicorp/boundary/plugins/kms"
|
|
"github.com/hashicorp/boundary/sdk/wrapper"
|
|
"github.com/hashicorp/go-hclog"
|
|
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
|
|
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/hashicorp/go-secure-stdlib/pluginutil/v2"
|
|
"github.com/hashicorp/go-secure-stdlib/strutil"
|
|
"github.com/hashicorp/hcl"
|
|
"github.com/hashicorp/hcl/hcl/ast"
|
|
"github.com/mitchellh/mapstructure"
|
|
)
|
|
|
|
var extraParsingFuncs []func(*Config) error
|
|
|
|
const (
|
|
desktopCorsOrigin = "serve://boundary"
|
|
|
|
devConfig = `
|
|
disable_mlock = true
|
|
|
|
telemetry {
|
|
prometheus_retention_time = "24h"
|
|
disable_hostname = true
|
|
}
|
|
`
|
|
|
|
devControllerExtraConfig = `
|
|
controller {
|
|
name = "dev-controller"
|
|
description = "A default controller created in dev mode"
|
|
}
|
|
|
|
kms "aead" {
|
|
purpose = "root"
|
|
aead_type = "aes-gcm"
|
|
key = "%s"
|
|
key_id = "global_root"
|
|
}
|
|
|
|
kms "aead" {
|
|
purpose = "worker-auth"
|
|
aead_type = "aes-gcm"
|
|
key = "%s"
|
|
key_id = "global_worker-auth"
|
|
}
|
|
|
|
kms "aead" {
|
|
purpose = "bsr"
|
|
aead_type = "aes-gcm"
|
|
key = "%s"
|
|
key_id = "global_bsr"
|
|
}
|
|
|
|
kms "aead" {
|
|
purpose = "recovery"
|
|
aead_type = "aes-gcm"
|
|
key = "%s"
|
|
key_id = "global_recovery"
|
|
}
|
|
|
|
listener "tcp" {
|
|
purpose = "api"
|
|
tls_disable = true
|
|
cors_enabled = true
|
|
cors_allowed_origins = ["*"]
|
|
}
|
|
|
|
listener "tcp" {
|
|
purpose = "cluster"
|
|
}
|
|
|
|
listener "tcp" {
|
|
purpose = "ops"
|
|
tls_disable = true
|
|
}
|
|
`
|
|
|
|
devIpv6ControllerExtraConfig = `
|
|
controller {
|
|
name = "dev-controller"
|
|
description = "A default controller created in dev mode"
|
|
}
|
|
|
|
kms "aead" {
|
|
purpose = "root"
|
|
aead_type = "aes-gcm"
|
|
key = "%s"
|
|
key_id = "global_root"
|
|
}
|
|
|
|
kms "aead" {
|
|
purpose = "worker-auth"
|
|
aead_type = "aes-gcm"
|
|
key = "%s"
|
|
key_id = "global_worker-auth"
|
|
}
|
|
|
|
kms "aead" {
|
|
purpose = "bsr"
|
|
aead_type = "aes-gcm"
|
|
key = "%s"
|
|
key_id = "global_bsr"
|
|
}
|
|
|
|
kms "aead" {
|
|
purpose = "recovery"
|
|
aead_type = "aes-gcm"
|
|
key = "%s"
|
|
key_id = "global_recovery"
|
|
}
|
|
|
|
listener "tcp" {
|
|
address = "[::1]"
|
|
purpose = "api"
|
|
tls_disable = true
|
|
cors_enabled = true
|
|
cors_allowed_origins = ["*"]
|
|
}
|
|
|
|
listener "tcp" {
|
|
address = "[::1]"
|
|
purpose = "cluster"
|
|
}
|
|
|
|
listener "tcp" {
|
|
address = "[::1]"
|
|
purpose = "ops"
|
|
tls_disable = true
|
|
}
|
|
`
|
|
|
|
devIpv6WorkerExtraConfig = `
|
|
listener "tcp" {
|
|
address = "[::1]"
|
|
purpose = "proxy"
|
|
}
|
|
|
|
worker {
|
|
name = "w_1234567890"
|
|
description = "A default worker created in dev mode"
|
|
public_addr = "[::1]"
|
|
initial_upstreams = ["[::1]"]
|
|
tags {
|
|
type = ["dev", "local"]
|
|
}
|
|
}
|
|
|
|
kms "aead" {
|
|
purpose = "worker-auth-storage"
|
|
aead_type = "aes-gcm"
|
|
key = "%s"
|
|
key_id = "worker-auth-storage"
|
|
}
|
|
`
|
|
|
|
devWorkerExtraConfig = `
|
|
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"]
|
|
}
|
|
}
|
|
|
|
kms "aead" {
|
|
purpose = "worker-auth-storage"
|
|
aead_type = "aes-gcm"
|
|
key = "%s"
|
|
key_id = "worker-auth-storage"
|
|
}
|
|
`
|
|
|
|
// We use a custom Content-Security-Policy because we need to add wasm-unsafe-eval
|
|
// as a script-src to support asciinema playback on the Admin UI. Users can still
|
|
// override this value via the configuration.
|
|
defaultCsp = "default-src 'none'; script-src 'self' 'wasm-unsafe-eval'; frame-src 'self'; font-src 'self'; connect-src 'self'; img-src 'self' data:; style-src 'self'; media-src 'self'; manifest-src 'self'; style-src-attr 'self'; frame-ancestors 'self'"
|
|
)
|
|
|
|
// Config is the configuration for the boundary controller
|
|
type Config struct {
|
|
*configutil.SharedConfig `hcl:"-"`
|
|
|
|
Worker *Worker `hcl:"worker"`
|
|
Controller *Controller `hcl:"controller"`
|
|
|
|
// Dev-related options
|
|
DevController bool `hcl:"-"`
|
|
DevUiPassthroughDir string `hcl:"-"`
|
|
DevControllerKey string `hcl:"-"`
|
|
DevWorkerAuthKey string `hcl:"-"`
|
|
DevWorkerAuthStorageKey string `hcl:"-"`
|
|
DevBsrKey string `hcl:"-"`
|
|
DevRecoveryKey string `hcl:"-"`
|
|
|
|
// Eventing configuration for the controller
|
|
Eventing *event.EventerConfig `hcl:"events"`
|
|
|
|
// Plugin-related options
|
|
Plugins Plugins `hcl:"plugins"`
|
|
|
|
// Internal field for use with HCP deployments. Used if controllers/
|
|
// initial_upstreams is not set.
|
|
HcpbClusterId string `hcl:"hcp_boundary_cluster_id"`
|
|
|
|
// Whether to log information from the worker authentication process. This
|
|
// shouldn't divulge any secrets but may contain information considered
|
|
// sensitive. This should only be enabled for debugging purposes, and can be
|
|
// toggled with SIGHUP.
|
|
EnableWorkerAuthDebugging bool `hcl:"enable_worker_auth_debugging"`
|
|
|
|
// For opting out of license utilization reporting
|
|
Reporting Reporting `hcl:"reporting"`
|
|
}
|
|
|
|
type Controller struct {
|
|
Name string `hcl:"name"`
|
|
Description string `hcl:"description"`
|
|
Database *Database `hcl:"database"`
|
|
PublicClusterAddr string `hcl:"public_cluster_addr"`
|
|
Scheduler Scheduler `hcl:"scheduler"`
|
|
|
|
// AuthTokenTimeToLive is the total valid lifetime of a token denoted by time.Duration
|
|
AuthTokenTimeToLive any `hcl:"auth_token_time_to_live"`
|
|
AuthTokenTimeToLiveDuration time.Duration `hcl:"-"`
|
|
|
|
// AuthTokenTimeToStale is the total time a token can go unused before becoming invalid
|
|
// denoted by time.Duration
|
|
AuthTokenTimeToStale any `hcl:"auth_token_time_to_stale"`
|
|
AuthTokenTimeToStaleDuration time.Duration `hcl:"-"`
|
|
|
|
// GracefulShutdownWait is the amount of time that we'll wait before actually
|
|
// starting the Controller shutdown. This allows the health endpoint to
|
|
// return a status code to indicate that the instance is shutting down.
|
|
GracefulShutdownWait any `hcl:"graceful_shutdown_wait_duration"`
|
|
GracefulShutdownWaitDuration time.Duration `hcl:"-"`
|
|
|
|
// WorkerRPCGracePeriod represents the period of time (as a duration)
|
|
// that the controller will wait before deciding a worker is disconnected
|
|
// and marking connections from it as canceling. It is also used to evaluate
|
|
// whether a worker is available for routing based on the time since its last routing info report.
|
|
// For backwards compatibility this is still called worker_status_grace_period,
|
|
// though it is now used with both the SessionInfo and RoutingInfo RPCs.
|
|
//
|
|
// TODO: This isn't documented (on purpose) because the right place for this
|
|
// is central configuration so you can't drift across controllers, but we
|
|
// don't have that yet.
|
|
WorkerRPCGracePeriod any `hcl:"worker_status_grace_period"`
|
|
WorkerRPCGracePeriodDuration time.Duration `hcl:"-"`
|
|
|
|
// LivenessTimeToStale represents the period of time (as a duration) after
|
|
// which it will consider other controllers to be no longer accessible,
|
|
// based on time since their last status update in the database
|
|
//
|
|
// TODO: This isn't documented (on purpose) because the right place for this
|
|
// is central configuration so you can't drift across controllers, but we
|
|
// don't have that yet.
|
|
LivenessTimeToStale any `hcl:"liveness_time_to_stale"`
|
|
LivenessTimeToStaleDuration time.Duration `hcl:"-"`
|
|
|
|
// TODO: This isn't documented (on purpose) because the right place for this
|
|
// is central configuration so you can't drift across controllers and workers
|
|
// but we don't have that yet.
|
|
GetDownstreamWorkersTimeout any `hcl:"get_downstream_workers_timeout"`
|
|
GetDownstreamWorkersTimeoutDuration time.Duration `hcl:"-"`
|
|
|
|
// SchedulerRunJobInterval is the time interval between waking up the
|
|
// scheduler to run pending jobs.
|
|
//
|
|
// TODO: This field is currently internal.
|
|
SchedulerRunJobInterval time.Duration `hcl:"-"`
|
|
|
|
ApiRateLimits ratelimit.Configs `hcl:"-"`
|
|
ApiRateLimiterMaxQuotas int `hcl:"api_rate_limit_max_quotas"`
|
|
ApiRateLimitDisable bool `hcl:"api_rate_limit_disable"`
|
|
|
|
// License is the license used by HCP builds
|
|
License string `hcl:"license"`
|
|
|
|
// MaxPageSize overrides the default and max page size.
|
|
// The default page size is what is used when the page size
|
|
// is not explicitly provided by the user. The max page size
|
|
// is the greatest number the page size can be set to before
|
|
// it is rejected by the controller.
|
|
MaxPageSizeRaw any `hcl:"max_page_size"`
|
|
MaxPageSize uint `hcl:"-"`
|
|
}
|
|
|
|
func (c *Controller) InitNameIfEmpty(ctx context.Context) error {
|
|
if c == nil {
|
|
return fmt.Errorf("controller config is empty")
|
|
}
|
|
if c.Name != "" {
|
|
return nil
|
|
}
|
|
|
|
var err error
|
|
c.Name, err = db.NewPublicId(ctx, "c")
|
|
if err != nil {
|
|
return fmt.Errorf("error auto-generating controller name: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type Worker struct {
|
|
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 upstream controller or worker addresses.
|
|
InitialUpstreams []string `hcl:"-"`
|
|
InitialUpstreamsRaw any `hcl:"initial_upstreams"`
|
|
|
|
// 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
|
|
// key=value syntax, as well as accepting a string denoting an env or file
|
|
// pointer. This is trued up in the Parse function below.
|
|
Tags map[string][]string `hcl:"-"`
|
|
TagsRaw any `hcl:"tags"`
|
|
|
|
// ControllerRPCCallTimeout represents the period of time (as a duration) that
|
|
// the worker will allow the SessionInfo, RoutingInfo and Statistics RPC calls to
|
|
// attempt to finish before canceling them to try again.
|
|
// For backwards compatibility this is still called status_call_timeout,
|
|
// though it is now used to control the SessionInfo, Statistics and RoutingInfo RPCs.
|
|
//
|
|
// TODO: This is currently not documented and considered internal.
|
|
ControllerRPCCallTimeout any `hcl:"status_call_timeout"`
|
|
ControllerRPCCallTimeoutDuration time.Duration `hcl:"-"`
|
|
|
|
// GetDownstreamWorkersTimeout represents the period of time (as a duration) timeout
|
|
// for GetDownstreamWorkers call in DownstreamWorkerTicker
|
|
//
|
|
// TODO: This is currently not documented and considered internal.
|
|
GetDownstreamWorkersTimeout any `hcl:"get_downstream_workers_timeout"`
|
|
GetDownstreamWorkersTimeoutDuration time.Duration `hcl:"-"`
|
|
|
|
// SuccessfulControllerRPCGracePeriod represents the period of time (as a duration)
|
|
// that the worker will wait before closing connections if it cannot
|
|
// successfully complete a session info report to a controller. It is also used
|
|
// to evaluate whether the upstreams need to be recalculated in case of repeated
|
|
// routing info report failures. This cannot be less than ControllerRPCCallTimeout.
|
|
// For backwards compatibility this is still called successful_status_grace_period,
|
|
// though it is now used to control the SessionInfo and RoutingInfo RPCs.
|
|
//
|
|
// TODO: This is currently not documented and considered internal.
|
|
SuccessfulControllerRPCGracePeriod any `hcl:"successful_status_grace_period"`
|
|
SuccessfulControllerRPCGracePeriodDuration time.Duration `hcl:"-"`
|
|
|
|
// AuthStoragePath represents the location a worker stores its node credentials, if set
|
|
AuthStoragePath string `hcl:"auth_storage_path"`
|
|
|
|
// RecordingStoragePath represents the location a worker caches session recordings before
|
|
// they are sync'ed to the corresponding storage bucket. The path must already exist.
|
|
RecordingStoragePath string `hcl:"recording_storage_path"`
|
|
|
|
// RecordingStorageMinimumAvailableCapacity represents the minimum amount of available
|
|
// disk space a worker needs in the path defined by RecordingStoragePath for processing
|
|
// sessions with recording enabled. The expected input value for this field is a
|
|
// “capacity string“. Supported suffixes are kb, kib, mb, mib, gb, gib, tb, tib, which
|
|
// are not case sensitive. We use a raw interface for parsing so that users can input a
|
|
// "capacity string" which is converted into a uint64 value that is measured in bytes.
|
|
RecordingStorageMinimumAvailableCapacity any `hcl:"recording_storage_minimum_available_capacity"`
|
|
RecordingStorageMinimumAvailableDiskSpace uint64 `hcl:"-"`
|
|
|
|
// ControllerGeneratedActivationToken is a controller-generated activation
|
|
// token used to register this worker to the cluster. It can be a path, env
|
|
// var, or direct value.
|
|
ControllerGeneratedActivationToken string `hcl:"controller_generated_activation_token"`
|
|
|
|
// UseDeprecatedKmsAuthMethod indicates that the worker should use the
|
|
// pre-0.13 method of using KMSes to authenticate. This is currently only
|
|
// supported to throw an error if used telling people they need to upgrade.
|
|
UseDeprecatedKmsAuthMethod bool `hcl:"use_deprecated_kms_auth_method"`
|
|
|
|
// TestWorkerRPCInterval represents the base period of time that
|
|
// the worker will wait between invoking the controller RPCs.
|
|
// This is not exposed to users and only used in tests.
|
|
TestWorkerRPCInterval time.Duration `hcl:"-"`
|
|
}
|
|
|
|
type Database struct {
|
|
Url string `hcl:"url"`
|
|
MigrationUrl string `hcl:"migration_url"`
|
|
MaxOpenConnections int `hcl:"-"`
|
|
MaxOpenConnectionsRaw any `hcl:"max_open_connections"`
|
|
MaxIdleConnections *int `hcl:"-"`
|
|
MaxIdleConnectionsRaw any `hcl:"max_idle_connections"`
|
|
ConnMaxIdleTime any `hcl:"max_idle_time"`
|
|
ConnMaxIdleTimeDuration *time.Duration `hcl:"-"`
|
|
|
|
// SkipSharedLockAcquisition allows skipping grabbing the database shared
|
|
// lock. This is dangerous unless you know what you're doing, and you should
|
|
// not set it unless you are the reason it's here in the first place, as not
|
|
// only it dangerous but it will be removed at some point in the future.
|
|
SkipSharedLockAcquisition bool `hcl:"skip_shared_lock_acquisition"`
|
|
}
|
|
|
|
// Scheduler is the configuration block that specifies the job scheduler behavior on the controller
|
|
type Scheduler struct {
|
|
// JobRunInterval is the time interval between waking up the
|
|
// scheduler to run pending jobs.
|
|
//
|
|
JobRunInterval any `hcl:"job_run_interval"`
|
|
JobRunIntervalDuration time.Duration
|
|
|
|
// MonitorInterval is the time interval between waking up the
|
|
// scheduler to monitor for jobs that are defunct.
|
|
//
|
|
MonitorInterval any `hcl:"monitor_interval"`
|
|
MonitorIntervalDuration time.Duration
|
|
}
|
|
|
|
type Plugins struct {
|
|
ExecutionDir string `hcl:"execution_dir"`
|
|
}
|
|
|
|
type Reporting struct {
|
|
License License `hcl:"license"`
|
|
}
|
|
|
|
type License struct {
|
|
Enabled bool `hcl:"enabled"`
|
|
}
|
|
|
|
// DevWorker is a Config that is used for dev mode of Boundary
|
|
// workers. Supported options: WithObservationsEnabled, WithSysEventsEnabled,
|
|
// WithAuditEventsEnabled, TestWithErrorEventsEnabled
|
|
func DevWorker(opt ...Option) (*Config, error) {
|
|
workerAuthStorageKey := DevKeyGeneration()
|
|
opts, err := getOpts(opt...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing options: %w", err)
|
|
}
|
|
hclStr := fmt.Sprintf(devConfig+devWorkerExtraConfig, workerAuthStorageKey)
|
|
if opts.withIPv6Enabled {
|
|
hclStr = fmt.Sprintf(devConfig+devIpv6WorkerExtraConfig, workerAuthStorageKey)
|
|
}
|
|
parsed, err := Parse(hclStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing dev config: %w", err)
|
|
}
|
|
parsed.Eventing.AuditEnabled = opts.withAuditEventsEnabled
|
|
parsed.Eventing.ObservationsEnabled = opts.withObservationsEnabled
|
|
parsed.Eventing.SysEventsEnabled = opts.withSysEventsEnabled
|
|
parsed.Eventing.ErrorEventsDisabled = !opts.testWithErrorEventsEnabled
|
|
return parsed, nil
|
|
}
|
|
|
|
func DevKeyGeneration() string {
|
|
var numBytes int64 = 32
|
|
randBuf := new(bytes.Buffer)
|
|
n, err := randBuf.ReadFrom(&io.LimitedReader{
|
|
R: rand.Reader,
|
|
N: numBytes,
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if n != numBytes {
|
|
panic(fmt.Errorf("expected to read 32 bytes, read %d", n))
|
|
}
|
|
devKey := base64.StdEncoding.EncodeToString(randBuf.Bytes())
|
|
return devKey
|
|
}
|
|
|
|
// DevController is a Config that is used for dev mode of Boundary
|
|
// controllers
|
|
func DevController(opt ...Option) (*Config, error) {
|
|
opts, err := getOpts(opt...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing options: %w", err)
|
|
}
|
|
|
|
controllerKey := DevKeyGeneration()
|
|
workerAuthKey := DevKeyGeneration()
|
|
bsrKey := DevKeyGeneration()
|
|
recoveryKey := DevKeyGeneration()
|
|
|
|
hclStr := fmt.Sprintf(devConfig+devControllerExtraConfig, controllerKey, workerAuthKey, bsrKey, recoveryKey)
|
|
if opts.withIPv6Enabled {
|
|
hclStr = fmt.Sprintf(devConfig+devIpv6ControllerExtraConfig, controllerKey, workerAuthKey, bsrKey, recoveryKey)
|
|
}
|
|
parsed, err := Parse(hclStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing dev config: %w", err)
|
|
}
|
|
parsed.DevController = true
|
|
parsed.DevControllerKey = controllerKey
|
|
parsed.DevWorkerAuthKey = workerAuthKey
|
|
parsed.DevBsrKey = bsrKey
|
|
parsed.DevRecoveryKey = recoveryKey
|
|
parsed.Eventing.AuditEnabled = opts.withAuditEventsEnabled
|
|
parsed.Eventing.ObservationsEnabled = opts.withObservationsEnabled
|
|
parsed.Eventing.SysEventsEnabled = opts.withSysEventsEnabled
|
|
parsed.Eventing.ErrorEventsDisabled = !opts.testWithErrorEventsEnabled
|
|
return parsed, nil
|
|
}
|
|
|
|
func DevCombined(opt ...Option) (*Config, error) {
|
|
opts, err := getOpts(opt...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing options: %w", err)
|
|
}
|
|
|
|
controllerKey := DevKeyGeneration()
|
|
workerAuthKey := DevKeyGeneration()
|
|
workerAuthStorageKey := DevKeyGeneration()
|
|
bsrKey := DevKeyGeneration()
|
|
recoveryKey := DevKeyGeneration()
|
|
|
|
hclStr := fmt.Sprintf(devConfig+devControllerExtraConfig+devWorkerExtraConfig, controllerKey, workerAuthKey, bsrKey, recoveryKey, workerAuthStorageKey)
|
|
if opts.withIPv6Enabled {
|
|
hclStr = fmt.Sprintf(devConfig+devIpv6ControllerExtraConfig+devIpv6WorkerExtraConfig, controllerKey, workerAuthKey, bsrKey, recoveryKey, workerAuthStorageKey)
|
|
}
|
|
parsed, err := Parse(hclStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing dev config: %w", err)
|
|
}
|
|
parsed.DevController = true
|
|
parsed.DevControllerKey = controllerKey
|
|
parsed.DevWorkerAuthKey = workerAuthKey
|
|
parsed.DevWorkerAuthStorageKey = workerAuthStorageKey
|
|
parsed.DevBsrKey = bsrKey
|
|
parsed.DevRecoveryKey = recoveryKey
|
|
return parsed, nil
|
|
}
|
|
|
|
func New() *Config {
|
|
return &Config{
|
|
SharedConfig: new(configutil.SharedConfig),
|
|
}
|
|
}
|
|
|
|
// Load will create a Config from the given config files. It concatenates the
|
|
// contents of the files together. This allows for composing a config from
|
|
// separate files, but notably does not attempt any merging of configs. Thus it
|
|
// is possible that the concatenation results in an invalid config file. This
|
|
// also supports decrypting the config, either with a kms block in one of the
|
|
// given files, or as a separate file in wrapperPath.
|
|
//
|
|
// Note that having multiple config files is only supported properly if they are
|
|
// all hcl files. If they are json files, only the first file is used. A mix of
|
|
// hcl and json will result in an error.
|
|
func Load(ctx context.Context, paths []string, wrapperPath string) (*Config, error) {
|
|
const op = "config.Load"
|
|
|
|
var err error
|
|
var cfg *Config
|
|
|
|
configStrs := make([]string, 0, len(paths))
|
|
for _, path := range paths {
|
|
fileBytes, err := os.ReadFile(path)
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("could not read config file", "path", path, "error", err))
|
|
return nil, err
|
|
}
|
|
configStrs = append(configStrs, string(fileBytes))
|
|
}
|
|
configString := strings.Join(configStrs, "\n")
|
|
|
|
wrapperString := configString
|
|
if wrapperPath != "" {
|
|
wrapperBytes, err := os.ReadFile(wrapperPath)
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("could not read kms config file", "path", wrapperPath, "error", err))
|
|
return nil, err
|
|
}
|
|
wrapperString = string(wrapperBytes)
|
|
}
|
|
|
|
var configWrapper wrapping.Wrapper
|
|
var ifWrapper wrapping.InitFinalizer
|
|
var cleanupFunc func() error
|
|
if wrapperString != "" {
|
|
configWrapper, cleanupFunc, err = wrapper.GetWrapperFromHcl(
|
|
ctx,
|
|
wrapperString,
|
|
globals.KmsPurposeConfig,
|
|
configutil.WithPluginOptions(
|
|
pluginutil.WithPluginsMap(kms_plugin_assets.BuiltinKmsPlugins()),
|
|
pluginutil.WithPluginsFilesystem(kms_plugin_assets.KmsPluginPrefix, kms_plugin_assets.FileSystem()),
|
|
),
|
|
// TODO: How would we want to expose this kind of log to users when
|
|
// using recovery configs? Generally with normal CLI commands we
|
|
// don't print out all of these logs. We may want a logger with a
|
|
// custom writer behind our existing gate where we print nothing
|
|
// unless there is an error, then dump all of it.
|
|
configutil.WithLogger(hclog.NewNullLogger()),
|
|
)
|
|
if err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("could not get kms wrapper from config", "path", wrapperPath))
|
|
return nil, err
|
|
}
|
|
if cleanupFunc != nil {
|
|
defer func() {
|
|
if err := cleanupFunc(); err != nil {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("could not clean up kms wrapper", "path", wrapperPath))
|
|
}
|
|
}()
|
|
}
|
|
if configWrapper != nil {
|
|
ifWrapper, _ = configWrapper.(wrapping.InitFinalizer)
|
|
}
|
|
}
|
|
if ifWrapper != nil {
|
|
if err := ifWrapper.Init(ctx); err != nil && !errors.Is(err, wrapping.ErrFunctionNotImplemented) {
|
|
event.WriteError(ctx, op, err, event.WithInfoMsg("could not initialize kms", "path", wrapperPath))
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if configWrapper != nil {
|
|
configString, err = configutil.EncryptDecrypt(configString, true, true, configWrapper)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
cfg, err = Parse(configString)
|
|
|
|
if ifWrapper != nil {
|
|
if err := ifWrapper.Finalize(context.Background()); err != nil && !errors.Is(err, wrapping.ErrFunctionNotImplemented) {
|
|
event.WriteError(context.Background(), op, err, event.WithInfoMsg("could not finalize kms", "path", wrapperPath))
|
|
return nil, err
|
|
}
|
|
}
|
|
return cfg, err
|
|
}
|
|
|
|
func Parse(d string) (*Config, error) {
|
|
obj, err := hcl.Parse(d)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := New()
|
|
if err := hcl.DecodeObject(result, obj); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Perform controller configuration overrides for auth token settings
|
|
if result.Controller != nil {
|
|
result.Controller.Name, err = parseutil.ParsePath(result.Controller.Name)
|
|
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
|
|
return nil, fmt.Errorf("Error parsing controller name: %w", err)
|
|
}
|
|
if result.Controller.Name != strings.ToLower(result.Controller.Name) {
|
|
return nil, errors.New("Controller name must be all lower-case")
|
|
}
|
|
if !strutil.Printable(result.Controller.Name) {
|
|
return nil, errors.New("Controller name contains non-printable characters")
|
|
}
|
|
result.Controller.Description, err = parseutil.ParsePath(result.Controller.Description)
|
|
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
|
|
return nil, fmt.Errorf("Error parsing controller description: %w", err)
|
|
}
|
|
if !strutil.Printable(result.Controller.Description) {
|
|
return nil, errors.New("Controller description contains non-printable characters")
|
|
}
|
|
if result.Controller.AuthTokenTimeToLive != "" {
|
|
t, err := parseutil.ParseDurationSecond(result.Controller.AuthTokenTimeToLive)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.Controller.AuthTokenTimeToLiveDuration = t
|
|
}
|
|
|
|
if result.Controller.AuthTokenTimeToStale != "" {
|
|
t, err := parseutil.ParseDurationSecond(result.Controller.AuthTokenTimeToStale)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.Controller.AuthTokenTimeToStaleDuration = t
|
|
}
|
|
|
|
if result.Controller.GracefulShutdownWait != "" {
|
|
t, err := parseutil.ParseDurationSecond(result.Controller.GracefulShutdownWait)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.Controller.GracefulShutdownWaitDuration = t
|
|
}
|
|
|
|
if result.Controller.Scheduler.JobRunInterval != "" {
|
|
t, err := parseutil.ParseDurationSecond(result.Controller.Scheduler.JobRunInterval)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.Controller.Scheduler.JobRunIntervalDuration = t
|
|
}
|
|
|
|
if result.Controller.Scheduler.MonitorInterval != "" {
|
|
t, err := parseutil.ParseDurationSecond(result.Controller.Scheduler.MonitorInterval)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.Controller.Scheduler.MonitorIntervalDuration = t
|
|
}
|
|
|
|
workerRPCGracePeriod := result.Controller.WorkerRPCGracePeriod
|
|
if util.IsNil(workerRPCGracePeriod) {
|
|
// For backwards compatibility this is still called BOUNDARY_CONTROLLER_WORKER_STATUS_GRACE_PERIOD,
|
|
// though it is now used to control the SessionInfo and RoutingInfo RPCs.
|
|
workerRPCGracePeriod = os.Getenv("BOUNDARY_CONTROLLER_WORKER_STATUS_GRACE_PERIOD")
|
|
}
|
|
if workerRPCGracePeriod != nil {
|
|
t, err := parseutil.ParseDurationSecond(workerRPCGracePeriod)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.Controller.WorkerRPCGracePeriodDuration = t
|
|
}
|
|
if result.Controller.WorkerRPCGracePeriodDuration < 0 {
|
|
return nil, errors.New("Controller worker RPC grace period value is negative")
|
|
}
|
|
|
|
livenessTimeToStale := result.Controller.LivenessTimeToStale
|
|
if util.IsNil(livenessTimeToStale) {
|
|
livenessTimeToStale = os.Getenv("BOUNDARY_CONTROLLER_LIVENESS_TIME_TO_STALE")
|
|
}
|
|
if livenessTimeToStale != nil {
|
|
t, err := parseutil.ParseDurationSecond(livenessTimeToStale)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.Controller.LivenessTimeToStaleDuration = t
|
|
}
|
|
if result.Controller.LivenessTimeToStaleDuration < 0 {
|
|
return nil, errors.New("Controller liveness time to stale value is negative")
|
|
}
|
|
|
|
getDownstreamWorkersTimeout := result.Controller.GetDownstreamWorkersTimeout
|
|
if util.IsNil(getDownstreamWorkersTimeout) {
|
|
getDownstreamWorkersTimeout = os.Getenv("BOUNDARY_CONTROLLER_GET_DOWNSTREAM_WORKERS_TIMEOUT")
|
|
}
|
|
if getDownstreamWorkersTimeout != nil {
|
|
t, err := parseutil.ParseDurationSecond(getDownstreamWorkersTimeout)
|
|
if err != nil {
|
|
return result, fmt.Errorf("error trying to parse controller get_downstream_workers_timeout: %w", err)
|
|
}
|
|
result.Controller.GetDownstreamWorkersTimeoutDuration = t
|
|
}
|
|
if result.Controller.GetDownstreamWorkersTimeoutDuration < 0 {
|
|
return nil, errors.New("get downstream workers timeout must be greater than 0")
|
|
}
|
|
|
|
if result.Controller.MaxPageSizeRaw != nil {
|
|
switch t := result.Controller.MaxPageSizeRaw.(type) {
|
|
case string:
|
|
maxPageSizeString, err := parseutil.ParsePath(t)
|
|
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
|
|
return nil, fmt.Errorf("Error parsing max page size: %w", err)
|
|
}
|
|
pageSize, err := strconv.Atoi(maxPageSizeString)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Max page size value is not an int: %w", err)
|
|
}
|
|
if pageSize <= 0 {
|
|
return nil, fmt.Errorf("Max page size value must be at least 1, was %d", pageSize)
|
|
}
|
|
result.Controller.MaxPageSize = uint(pageSize)
|
|
case int:
|
|
if t <= 0 {
|
|
return nil, fmt.Errorf("Max page size value must be at least 1, was %d", t)
|
|
}
|
|
result.Controller.MaxPageSize = uint(t)
|
|
default:
|
|
return nil, fmt.Errorf("Max page size: unsupported type %q", reflect.TypeOf(t).String())
|
|
}
|
|
}
|
|
|
|
if result.Controller.Database != nil {
|
|
if result.Controller.Database.MaxOpenConnectionsRaw != nil {
|
|
switch t := result.Controller.Database.MaxOpenConnectionsRaw.(type) {
|
|
case string:
|
|
maxOpenConnectionsString, err := parseutil.ParsePath(t)
|
|
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
|
|
return nil, fmt.Errorf("Error parsing database max open connections: %w", err)
|
|
}
|
|
result.Controller.Database.MaxOpenConnections, err = strconv.Atoi(maxOpenConnectionsString)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Database max open connections value is not an int: %w", err)
|
|
}
|
|
case int:
|
|
result.Controller.Database.MaxOpenConnections = t
|
|
default:
|
|
return nil, fmt.Errorf("Database max open connections: unsupported type %q",
|
|
reflect.TypeOf(t).String())
|
|
}
|
|
}
|
|
if result.Controller.Database.MaxIdleConnectionsRaw != nil {
|
|
switch t := result.Controller.Database.MaxIdleConnectionsRaw.(type) {
|
|
case string:
|
|
maxIdleConnectionsString, err := parseutil.ParsePath(t)
|
|
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
|
|
return nil, fmt.Errorf("Error parsing database max idle connections: %w", err)
|
|
}
|
|
idleConns, err := strconv.Atoi(maxIdleConnectionsString)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Database max idle connections value is not a uint: %w", err)
|
|
}
|
|
result.Controller.Database.MaxIdleConnections = &idleConns
|
|
case int:
|
|
result.Controller.Database.MaxIdleConnections = &t
|
|
default:
|
|
return nil, fmt.Errorf("Database max idle connections: unsupported type %q",
|
|
reflect.TypeOf(t).String())
|
|
}
|
|
}
|
|
if result.Controller.Database.ConnMaxIdleTime != nil {
|
|
switch t := result.Controller.Database.ConnMaxIdleTime.(type) {
|
|
case string:
|
|
durationString, err := parseutil.ParsePath(t)
|
|
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
|
|
return nil, fmt.Errorf("Error parsing connection max idle time: %w", err)
|
|
}
|
|
connMaxIdleTime, err := parseutil.ParseDurationSecond(durationString)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Connection max idle time is not a duration: %w", err)
|
|
}
|
|
result.Controller.Database.ConnMaxIdleTimeDuration = &connMaxIdleTime
|
|
default:
|
|
return nil, fmt.Errorf("Database connection max idle time: unsupported type %q",
|
|
reflect.TypeOf(t).String())
|
|
}
|
|
}
|
|
}
|
|
|
|
result.Controller.ApiRateLimits, err = parseApiRateLimits(obj.Node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if result.Controller.ApiRateLimiterMaxQuotas <= 0 {
|
|
result.Controller.ApiRateLimiterMaxQuotas = ratelimit.DefaultLimiterMaxQuotas()
|
|
}
|
|
}
|
|
|
|
// Parse worker tags
|
|
if result.Worker != nil {
|
|
if result.Worker.UseDeprecatedKmsAuthMethod {
|
|
return nil, fmt.Errorf("The flag 'use_deprecated_auth_method' is unsupported as of version 0.15.")
|
|
}
|
|
|
|
result.Worker.Name, err = parseutil.ParsePath(result.Worker.Name)
|
|
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
|
|
return nil, fmt.Errorf("Error parsing worker name: %w", err)
|
|
}
|
|
if result.Worker.Name != strings.ToLower(result.Worker.Name) {
|
|
return nil, errors.New("Worker name must be all lower-case")
|
|
}
|
|
if !strutil.Printable(result.Worker.Name) {
|
|
return nil, errors.New("Worker name contains non-printable characters")
|
|
}
|
|
|
|
result.Worker.Description, err = parseutil.ParsePath(result.Worker.Description)
|
|
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
|
|
return nil, fmt.Errorf("Error parsing worker description: %w", err)
|
|
}
|
|
if !strutil.Printable(result.Worker.Description) {
|
|
return nil, errors.New("Worker description contains non-printable characters")
|
|
}
|
|
|
|
result.Worker.ControllerGeneratedActivationToken, err = parseutil.ParsePath(result.Worker.ControllerGeneratedActivationToken)
|
|
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
|
|
return nil, fmt.Errorf("Error parsing worker activation token: %w", err)
|
|
}
|
|
|
|
statusCallTimeoutDuration := result.Worker.ControllerRPCCallTimeout
|
|
if util.IsNil(statusCallTimeoutDuration) {
|
|
// For backwards compatibility this is still called BOUNDARY_WORKER_STATUS_CALL_TIMEOUT,
|
|
// though it is now used to control the SessionInfo, Statistics and RoutingInfo RPCs.
|
|
statusCallTimeoutDuration = os.Getenv("BOUNDARY_WORKER_STATUS_CALL_TIMEOUT")
|
|
}
|
|
if statusCallTimeoutDuration != nil {
|
|
t, err := parseutil.ParseDurationSecond(statusCallTimeoutDuration)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.Worker.ControllerRPCCallTimeoutDuration = t
|
|
}
|
|
if result.Worker.ControllerRPCCallTimeoutDuration < 0 {
|
|
return nil, errors.New("Controller RPC timeout value is negative")
|
|
}
|
|
|
|
getDownstreamWorkersTimeoutDuration := result.Worker.GetDownstreamWorkersTimeout
|
|
if util.IsNil(getDownstreamWorkersTimeoutDuration) {
|
|
getDownstreamWorkersTimeoutDuration = os.Getenv("BOUNDARY_WORKER_GET_DOWNSTREAM_WORKERS_TIMEOUT")
|
|
}
|
|
if getDownstreamWorkersTimeoutDuration != nil {
|
|
t, err := parseutil.ParseDurationSecond(getDownstreamWorkersTimeoutDuration)
|
|
if err != nil {
|
|
return result, fmt.Errorf("error trying to parse worker get_downstream_workers_timeout: %w", err)
|
|
}
|
|
result.Worker.GetDownstreamWorkersTimeoutDuration = t
|
|
}
|
|
if result.Worker.GetDownstreamWorkersTimeoutDuration < 0 {
|
|
return nil, errors.New("get downstream workers timeout must be greater than 0")
|
|
}
|
|
|
|
successfulControllerRPCGracePeriod := result.Worker.SuccessfulControllerRPCGracePeriod
|
|
if util.IsNil(successfulControllerRPCGracePeriod) {
|
|
// For backwards compatibility this is still called BOUNDARY_WORKER_SUCCESSFUL_STATUS_GRACE_PERIOD,
|
|
// though it is now used to control the SessionInfo and RoutingInfo RPCs.
|
|
successfulControllerRPCGracePeriod = os.Getenv("BOUNDARY_WORKER_SUCCESSFUL_STATUS_GRACE_PERIOD")
|
|
}
|
|
if successfulControllerRPCGracePeriod != nil {
|
|
t, err := parseutil.ParseDurationSecond(successfulControllerRPCGracePeriod)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.Worker.SuccessfulControllerRPCGracePeriodDuration = t
|
|
}
|
|
if result.Worker.SuccessfulControllerRPCGracePeriodDuration < 0 {
|
|
return nil, errors.New("Successful controller RPC grace period value is negative")
|
|
}
|
|
|
|
if !util.IsNil(result.Worker.RecordingStorageMinimumAvailableCapacity) {
|
|
if result.Worker.RecordingStoragePath == "" {
|
|
return nil, errors.New("recording_storage_path cannot be empty when providing recording_storage_minimum_available_capacity")
|
|
}
|
|
recordingStorageMinimumAvailableDiskSpace, err := parseutil.ParseCapacityString(result.Worker.RecordingStorageMinimumAvailableCapacity)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.Worker.RecordingStorageMinimumAvailableDiskSpace = recordingStorageMinimumAvailableDiskSpace
|
|
}
|
|
// RecordingStorageMinimumAvailableDiskSpace defaults to 500MiB when not set by the user
|
|
if result.Worker.RecordingStoragePath != "" && result.Worker.RecordingStorageMinimumAvailableDiskSpace == 0 {
|
|
result.Worker.RecordingStorageMinimumAvailableDiskSpace = storage.DefaultMinimumAvailableDiskSpace
|
|
}
|
|
|
|
switch {
|
|
case result.Worker.ControllerRPCCallTimeoutDuration == 0 && result.Worker.SuccessfulControllerRPCGracePeriodDuration == 0:
|
|
// Nothing
|
|
case result.Worker.ControllerRPCCallTimeoutDuration != 0 && result.Worker.SuccessfulControllerRPCGracePeriodDuration != 0:
|
|
if result.Worker.ControllerRPCCallTimeoutDuration > result.Worker.SuccessfulControllerRPCGracePeriodDuration {
|
|
return nil, fmt.Errorf("Worker setting for controller rpc timeout duration must be less than or equal to successful controller rpc grace period duration")
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("Worker settings for controller rpc call timeout duration and successful controller rpc grace period duration must either both be set or both be empty")
|
|
}
|
|
|
|
if result.Worker.TagsRaw != nil {
|
|
switch t := result.Worker.TagsRaw.(type) {
|
|
// We allow `tags` to be a simple string containing a URL with schema.
|
|
// See: https://github.com/hashicorp/go-secure-stdlib/blob/main/parseutil/parsepath.go
|
|
case string:
|
|
rawTags, err := parseutil.ParsePath(t)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error parsing worker tags: %w", err)
|
|
}
|
|
|
|
var temp []map[string]any
|
|
err = hcl.Decode(&temp, rawTags)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error decoding raw worker tags: %w", err)
|
|
}
|
|
|
|
if err := mapstructure.WeakDecode(temp, &result.Worker.Tags); err != nil {
|
|
return nil, fmt.Errorf("Error decoding the worker's tags: %w", err)
|
|
}
|
|
|
|
// HCL allows multiple labeled blocks with the same name, turning it
|
|
// into a slice of maps, hence the slice here. This format is the
|
|
// one that ends up matching the JSON that we use in the expression.
|
|
case []map[string]any:
|
|
for _, m := range t {
|
|
for k, v := range m {
|
|
// We allow the user to pass in only the keys in HCL, and
|
|
// then set the values to point to a URL with schema.
|
|
valStr, ok := v.(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
parsed, err := parseutil.ParsePath(valStr)
|
|
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
|
|
return nil, fmt.Errorf("Error parsing worker tag values: %w", err)
|
|
}
|
|
if valStr == parsed {
|
|
// Nothing was found, ignore.
|
|
// WeakDecode will still parse it though as we
|
|
// don't know if this could be a valid tag.
|
|
continue
|
|
}
|
|
|
|
var tags []string
|
|
err = json.Unmarshal([]byte(parsed), &tags)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error unmarshaling env var/file contents: %w", err)
|
|
}
|
|
m[k] = tags
|
|
}
|
|
}
|
|
|
|
if err := mapstructure.WeakDecode(t, &result.Worker.Tags); err != nil {
|
|
return nil, fmt.Errorf("Error decoding the worker's %q section: %w", "tags", err)
|
|
}
|
|
|
|
// However for those that are used to other systems, we also accept
|
|
// key=value pairs
|
|
case []any:
|
|
var strs []string
|
|
if err := mapstructure.WeakDecode(t, &strs); err != nil {
|
|
return nil, fmt.Errorf("Error decoding the worker's %q section: %w", "tags", err)
|
|
}
|
|
result.Worker.Tags = make(map[string][]string, len(strs))
|
|
// Aggregate the values by key. We care about the first equal
|
|
// sign only, to allow equals to be in values if needed. This
|
|
// also means we don't support equal signs in keys.
|
|
for _, str := range strs {
|
|
splitStr := strings.SplitN(str, "=", 2)
|
|
switch len(splitStr) {
|
|
case 1:
|
|
return nil, fmt.Errorf("Error decoding tag %q from string: must be in key = value format", str)
|
|
case 2:
|
|
key := splitStr[0]
|
|
v := result.Worker.Tags[key]
|
|
if len(v) == 0 {
|
|
v = make([]string, 0, 1)
|
|
}
|
|
result.Worker.Tags[key] = append(v, splitStr[1])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for k, v := range result.Worker.Tags {
|
|
if k != strings.ToLower(k) {
|
|
return nil, fmt.Errorf("Tag key %q is not all lower-case letters", k)
|
|
}
|
|
if !strutil.Printable(k) {
|
|
return nil, fmt.Errorf("Tag key %q contains non-printable characters", k)
|
|
}
|
|
if strings.Contains(k, ",") {
|
|
return nil, fmt.Errorf("Tag key %q cannot contain commas", k)
|
|
}
|
|
for _, val := range v {
|
|
if val != strings.ToLower(val) {
|
|
return nil, fmt.Errorf("Tag value %q for tag key %q is not all lower-case letters", val, k)
|
|
}
|
|
if !strutil.Printable(k) {
|
|
return nil, fmt.Errorf("Tag value %q for tag key %q contains non-printable characters", v, k)
|
|
}
|
|
if strings.Contains(val, ",") {
|
|
return nil, fmt.Errorf("Tag value %q for tag key %q cannot contain commas", val, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
result.Worker.InitialUpstreams, err = parseWorkerUpstreams(result)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to parse worker upstreams: %w", err)
|
|
}
|
|
}
|
|
|
|
// Now that we can have multiple KMSes for downstream workers, allow an
|
|
// unlimited number of KMS blocks as we don't know how many might be defined
|
|
sharedConfig, err := configutil.ParseConfig(
|
|
d,
|
|
configutil.WithMaxKmsBlocks(-1),
|
|
configutil.WithListenerOptions(listenerutil.WithDefaultUiContentSecurityPolicyHeader(defaultCsp)),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result.SharedConfig = sharedConfig
|
|
|
|
for _, listener := range result.SharedConfig.Listeners {
|
|
if strutil.StrListContains(listener.Purpose, "api") &&
|
|
(listener.CorsDisableDefaultAllowedOriginValues == nil || !*listener.CorsDisableDefaultAllowedOriginValues) {
|
|
switch listener.CorsEnabled {
|
|
case nil:
|
|
// If CORS wasn't specified, enable default value of *, which allows
|
|
// both the admin UI (without the user having to explicitly set an
|
|
// origin) and the desktop origin
|
|
listener.CorsEnabled = new(bool)
|
|
*listener.CorsEnabled = true
|
|
listener.CorsAllowedOrigins = []string{"*"}
|
|
|
|
default:
|
|
// If not the wildcard and they haven't disabled us auto-adding
|
|
// origin values, add the desktop client origin
|
|
if *listener.CorsEnabled &&
|
|
!strutil.StrListContains(listener.CorsAllowedOrigins, "*") {
|
|
listener.CorsAllowedOrigins = strutil.AppendIfMissing(listener.CorsAllowedOrigins, desktopCorsOrigin)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
list, ok := obj.Node.(*ast.ObjectList)
|
|
if !ok {
|
|
return nil, fmt.Errorf("error parsing: file doesn't contain a root object")
|
|
}
|
|
|
|
eventList := list.Filter("events")
|
|
switch len(eventList.Items) {
|
|
case 0:
|
|
result.Eventing = event.DefaultEventerConfig()
|
|
case 1:
|
|
if result.Eventing, err = parseEventing(eventList.Items[0]); err != nil {
|
|
return nil, fmt.Errorf(`error parsing "events": %w`, err)
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf(`too many "events" nodes (max 1, got %d)`, len(eventList.Items))
|
|
}
|
|
|
|
if result.Plugins.ExecutionDir != "" {
|
|
result.Plugins.ExecutionDir, err = parseutil.ParsePath(result.Plugins.ExecutionDir)
|
|
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
|
|
return nil, fmt.Errorf("Error parsing plugins execution dir: %w", err)
|
|
}
|
|
}
|
|
|
|
for _, f := range extraParsingFuncs {
|
|
if err := f(result); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func parseApiRateLimits(node ast.Node) (ratelimit.Configs, error) {
|
|
list, ok := node.(*ast.ObjectList)
|
|
if !ok {
|
|
return nil, fmt.Errorf("error parsing: file doesn't contain a root object")
|
|
}
|
|
controllerList := list.Filter("controller")
|
|
|
|
configs := make(ratelimit.Configs, 0)
|
|
for _, item := range controllerList.Items {
|
|
controller, ok := item.Val.(*ast.ObjectType)
|
|
if !ok {
|
|
return nil, fmt.Errorf("error parsing: file doesn't contain controller object")
|
|
}
|
|
apiRateLimitsList := controller.List.Filter("api_rate_limit")
|
|
|
|
var err error
|
|
for i, item := range apiRateLimitsList.Items {
|
|
var a ratelimit.Config
|
|
if err := hcl.DecodeObject(&a, item.Val); err != nil {
|
|
return nil, fmt.Errorf("error decoding controller api_rate_limit entry %d", i)
|
|
}
|
|
a.Period, err = parseutil.ParseDurationSecond(a.PeriodHCL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error decoding controller api_rate_limit period for entry %d", i)
|
|
}
|
|
configs = append(configs, &a)
|
|
}
|
|
}
|
|
|
|
return configs, nil
|
|
}
|
|
|
|
func parseWorkerUpstreams(c *Config) ([]string, error) {
|
|
if c == nil || c.Worker == nil {
|
|
return nil, fmt.Errorf("config or worker field is nil")
|
|
}
|
|
if c.Worker.InitialUpstreamsRaw == nil {
|
|
// return nil here so that other address sources can be provided outside of config
|
|
return nil, nil
|
|
}
|
|
|
|
switch t := c.Worker.InitialUpstreamsRaw.(type) {
|
|
case []any:
|
|
var upstreams []string
|
|
err := mapstructure.WeakDecode(c.Worker.InitialUpstreamsRaw, &upstreams)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode worker initial_upstreams block into config field: %w", err)
|
|
}
|
|
return upstreams, nil
|
|
|
|
case string:
|
|
upstreamsStr, err := parseutil.ParsePath(t)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("bad env var or file pointer: %w", err)
|
|
}
|
|
|
|
var upstreams []string
|
|
err = json.Unmarshal([]byte(upstreamsStr), &upstreams)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal env/file contents: %w", err)
|
|
}
|
|
return upstreams, 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
|
|
if err := hcl.DecodeObject(&result, eventObj.Val); err != nil {
|
|
return nil, fmt.Errorf(`error decoding "events" node: %w`, err)
|
|
}
|
|
// Now, find the sinks
|
|
eventObjType, ok := eventObj.Val.(*ast.ObjectType)
|
|
if !ok {
|
|
return nil, fmt.Errorf(`error interpreting "events" node as an object type`)
|
|
}
|
|
list := eventObjType.List
|
|
sinkList := list.Filter("sink")
|
|
// Go through each sink and decode
|
|
for i, item := range sinkList.Items {
|
|
var s event.SinkConfig
|
|
if err := hcl.DecodeObject(&s, item.Val); err != nil {
|
|
return nil, fmt.Errorf("error decoding eventer sink entry %d", i)
|
|
}
|
|
|
|
// Fix up type and validate
|
|
switch {
|
|
case s.Type != "":
|
|
case len(item.Keys) == 1:
|
|
s.Type = event.SinkType(item.Keys[0].Token.Value().(string))
|
|
default:
|
|
switch {
|
|
case s.StderrConfig != nil:
|
|
// If we haven't found the type any other way, they _must_
|
|
// specify this block even though there are no config parameters
|
|
s.Type = event.StderrSink
|
|
case s.FileConfig != nil:
|
|
s.Type = event.FileSink
|
|
default:
|
|
return nil, fmt.Errorf("sink type could not be determined")
|
|
}
|
|
}
|
|
s.Type = event.SinkType(strings.ToLower(string(s.Type)))
|
|
|
|
if s.Type == event.StderrSink && s.StderrConfig == nil {
|
|
// StderrConfig is optional as it has no values, but ensure it's
|
|
// always populated if it's the type
|
|
s.StderrConfig = new(event.StderrSinkTypeConfig)
|
|
}
|
|
|
|
// parse the duration string specified in a file config into a time.Duration
|
|
if s.FileConfig != nil && s.FileConfig.RotateDurationHCL != "" {
|
|
var err error
|
|
s.FileConfig.RotateDuration, err = parseutil.ParseDurationSecond(s.FileConfig.RotateDurationHCL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can't parse rotation duration %s", s.FileConfig.RotateDurationHCL)
|
|
}
|
|
}
|
|
|
|
// parse map into event types
|
|
if s.AuditConfig != nil && s.AuditConfig.FilterOverridesHCL != nil {
|
|
s.AuditConfig.FilterOverrides = make(map[event.DataClassification]event.FilterOperation, len(s.AuditConfig.FilterOverridesHCL))
|
|
for k, v := range s.AuditConfig.FilterOverridesHCL {
|
|
s.AuditConfig.FilterOverrides[event.DataClassification(k)] = event.FilterOperation(v)
|
|
}
|
|
}
|
|
|
|
if err := s.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Append to result
|
|
result.Sinks = append(result.Sinks, &s)
|
|
}
|
|
if len(result.Sinks) == 0 {
|
|
result.Sinks = []*event.SinkConfig{event.DefaultSink()}
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
// Sanitized returns a copy of the config with all values that are considered
|
|
// sensitive stripped. It also strips all `*Raw` values that are mainly
|
|
// used for parsing.
|
|
//
|
|
// Specifically, the fields that this method strips are:
|
|
// - KMS.Config
|
|
// - Telemetry.CirconusAPIToken
|
|
func (c *Config) Sanitized() map[string]any {
|
|
// Create shared config if it doesn't exist (e.g. in tests) so that map
|
|
// keys are actually populated
|
|
if c.SharedConfig == nil {
|
|
c.SharedConfig = new(configutil.SharedConfig)
|
|
}
|
|
sharedResult := c.SharedConfig.Sanitized()
|
|
result := map[string]any{}
|
|
for k, v := range sharedResult {
|
|
result[k] = v
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// SetupControllerPublicClusterAddress will set the controller public address.
|
|
// If the flagValue is provided it will be used. Otherwise this will use the
|
|
// address from cluster listener. In either case it will check to see if no port
|
|
// is included, and if not it will set the default port of 9201.
|
|
//
|
|
// If there are any errors parsing the address from the flag or listener,
|
|
// and error is returned.
|
|
func (c *Config) SetupControllerPublicClusterAddress(flagValue string) error {
|
|
if c.Controller == nil {
|
|
c.Controller = new(Controller)
|
|
}
|
|
if flagValue != "" {
|
|
c.Controller.PublicClusterAddr = flagValue
|
|
}
|
|
if c.Controller.PublicClusterAddr == "" {
|
|
FindAddr:
|
|
for _, listener := range c.Listeners {
|
|
for _, purpose := range listener.Purpose {
|
|
if purpose == "cluster" {
|
|
c.Controller.PublicClusterAddr = listener.Address
|
|
break FindAddr
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
var err error
|
|
c.Controller.PublicClusterAddr, err = parseutil.ParsePath(c.Controller.PublicClusterAddr)
|
|
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
|
|
return fmt.Errorf("Error parsing public cluster addr: %w", err)
|
|
}
|
|
|
|
c.Controller.PublicClusterAddr, err = listenerutil.ParseSingleIPTemplate(c.Controller.PublicClusterAddr)
|
|
if err != nil {
|
|
return fmt.Errorf("Error parsing IP template on controller public cluster addr: %w", err)
|
|
}
|
|
}
|
|
|
|
host, port, err := util.SplitHostPort(c.Controller.PublicClusterAddr)
|
|
if err != nil {
|
|
return fmt.Errorf("Error splitting public cluster adddress host/port: %w", err)
|
|
}
|
|
if port == "" {
|
|
port = "9201"
|
|
}
|
|
c.Controller.PublicClusterAddr = util.JoinHostPort(host, port)
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetupWorkerInitialUpstreams will set the worker initial upstreams in cases
|
|
// where both a worker and controller stanza are provided. The initial upstreams
|
|
// will be:
|
|
// - The initialily provided value, if it is the same as the controller's cluster address
|
|
// - The controller's public cluster address if it it was set
|
|
// - The controller's cluster listener's address
|
|
//
|
|
// Any other value already set for iniital upstream will result in an error.
|
|
func (c *Config) SetupWorkerInitialUpstreams() error {
|
|
// nothing to do here
|
|
if c.Worker == nil || c.Controller == nil {
|
|
return nil
|
|
}
|
|
|
|
var clusterAddr string
|
|
for _, lnConfig := range c.Listeners {
|
|
switch len(lnConfig.Purpose) {
|
|
case 0:
|
|
return fmt.Errorf("Listener specified without a purpose")
|
|
case 1:
|
|
purpose := lnConfig.Purpose[0]
|
|
switch purpose {
|
|
case "cluster":
|
|
clusterAddr = lnConfig.Address
|
|
if clusterAddr == "" {
|
|
clusterAddr = "127.0.0.1:9201"
|
|
lnConfig.Address = clusterAddr
|
|
}
|
|
}
|
|
default:
|
|
return fmt.Errorf("Specifying a listener with more than one purpose is not supported")
|
|
}
|
|
}
|
|
|
|
switch len(c.Worker.InitialUpstreams) {
|
|
case 0:
|
|
if c.Controller.PublicClusterAddr != "" {
|
|
clusterAddr = c.Controller.PublicClusterAddr
|
|
}
|
|
c.Worker.InitialUpstreams = []string{clusterAddr}
|
|
case 1:
|
|
if c.Worker.InitialUpstreams[0] == clusterAddr {
|
|
break
|
|
}
|
|
if c.Controller.PublicClusterAddr != "" &&
|
|
c.Worker.InitialUpstreams[0] == c.Controller.PublicClusterAddr {
|
|
break
|
|
}
|
|
// Best effort see if it's a domain name and if not assume it must match
|
|
host, _, err := util.SplitHostPort(c.Worker.InitialUpstreams[0])
|
|
if err == nil {
|
|
ip := net.ParseIP(host)
|
|
if ip == nil {
|
|
// Assume it's a domain name
|
|
break
|
|
}
|
|
}
|
|
fallthrough
|
|
default:
|
|
return fmt.Errorf(`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`)
|
|
}
|
|
|
|
return nil
|
|
}
|