Add worker tagging (#862)

This allows workers to have tags assigned to them, which can then be filtered to control which targets they are allowed to serve.
This commit is contained in:
Jeff Mitchell authored and GitHub committed 2021-01-21 16:27:20 -05:00
1 parent 595a86508e
commit 717a3b52ee
54 files changed
+2150 -666

No files matched your search

+2 -2
View File
@@ -544,7 +544,7 @@ func (c *Command) WaitForInterrupt() int {
shutdownTriggered = true
case <-c.SighupCh:
c.UI.Output("==> Boundary controller reload triggered")
c.UI.Output("==> Boundary server reload triggered")
// Check for new log level
var level hclog.Level
@@ -589,7 +589,7 @@ func (c *Command) WaitForInterrupt() int {
RUNRELOADFUNCS:
if err := c.Reload(); err != nil {
c.UI.Error(fmt.Errorf("Error(s) were encountered during controller reload: %w", err).Error())
c.UI.Error(fmt.Errorf("Error(s) were encountered during server reload: %w", err).Error())
}
case <-c.SigUSR2Ch:
+3
View File
@@ -41,6 +41,9 @@ func generateTargetTableOutput(in *targets.Target) string {
if in.Description != "" {
nonAttributeMap["Description"] = in.Description
}
if in.WorkerFilter != "" {
nonAttributeMap["Worker Filter"] = in.WorkerFilter
}
maxLength := base.MaxAttributesLength(nonAttributeMap, in.Attributes, keySubstMap)
+22 -2
View File
@@ -11,6 +11,7 @@ import (
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/hashicorp/boundary/internal/cmd/common"
"github.com/hashicorp/boundary/sdk/strutil"
"github.com/hashicorp/go-bexpr"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
@@ -27,6 +28,7 @@ type TcpCommand struct {
flagDefaultPort string
flagSessionMaxSeconds string
flagSessionConnectionLimit string
flagWorkerFilter string
}
func (c *TcpCommand) Synopsis() string {
@@ -34,8 +36,8 @@ func (c *TcpCommand) Synopsis() string {
}
var tcpFlagsMap = map[string][]string{
"create": {"scope-id", "name", "description", "default-port", "session-max-seconds", "session-connection-limit"},
"update": {"id", "name", "description", "version", "default-port", "session-max-seconds", "session-connection-limit"},
"create": {"scope-id", "name", "description", "default-port", "session-max-seconds", "session-connection-limit", "worker-filter"},
"update": {"id", "name", "description", "version", "default-port", "session-max-seconds", "session-connection-limit", "worker-filter"},
}
func (c *TcpCommand) Help() string {
@@ -91,6 +93,12 @@ func (c *TcpCommand) Flags() *base.FlagSets {
Target: &c.flagSessionConnectionLimit,
Usage: "The maximum number of connections allowed for a session. -1 means unlimited.",
})
case "worker-filter":
f.StringVar(&base.StringVar{
Name: "worker-filter",
Target: &c.flagWorkerFilter,
Usage: "A boolean expression to filter which workers can handle sessions for this target.",
})
}
}
@@ -196,6 +204,18 @@ func (c *TcpCommand) Run(args []string) int {
opts = append(opts, targets.WithSessionConnectionLimit(int32(limit)))
}
switch c.flagWorkerFilter {
case "":
case "null":
opts = append(opts, targets.DefaultWorkerFilter())
default:
if _, err := bexpr.CreateEvaluator(c.flagWorkerFilter); err != nil {
c.UI.Error(fmt.Sprintf("Unable to successfully parse filter expression: %s", err))
return 1
}
opts = append(opts, targets.WithWorkerFilter(c.flagWorkerFilter))
}
targetClient := targets.NewClient(client)
// Perform check-and-set when needed
+80
View File
@@ -13,10 +13,12 @@ import (
"strings"
"time"
"github.com/hashicorp/boundary/sdk/strutil"
wrapping "github.com/hashicorp/go-kms-wrapping"
"github.com/hashicorp/hcl"
"github.com/hashicorp/shared-secure-libs/configutil"
"github.com/hashicorp/vault/sdk/helper/parseutil"
"github.com/mitchellh/mapstructure"
)
const (
@@ -77,6 +79,9 @@ worker {
name = "dev-worker"
description = "A default worker created in dev mode"
controllers = ["127.0.0.1"]
tags {
type = ["dev", "local"]
}
}
`
)
@@ -117,6 +122,12 @@ type Worker struct {
Description string `hcl:"description"`
Controllers []string `hcl:"controllers"`
PublicAddr string `hcl:"public_addr"`
// 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. This is trued up in the Parse function below.
TagsRaw interface{} `hcl:"tags"`
Tags map[string][]string `hcl:"-"`
}
type Database struct {
@@ -224,6 +235,12 @@ func Parse(d string) (*Config, error) {
// Perform controller configuration overrides for auth token settings
if result.Controller != nil {
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")
}
if result.Controller.AuthTokenTimeToLive != "" {
t, err := parseutil.ParseDurationSecond(result.Controller.AuthTokenTimeToLive)
if err != nil {
@@ -241,6 +258,69 @@ func Parse(d string) (*Config, error) {
}
}
// Parse worker tags
if result.Worker != nil {
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")
}
if result.Worker.TagsRaw != nil {
switch t := result.Worker.TagsRaw.(type) {
// 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]interface{}:
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 []interface{}:
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)
}
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)
}
}
}
}
sharedConfig, err := configutil.ParseConfig(d)
if err != nil {
return nil, err
+75 -1
View File
@@ -108,10 +108,84 @@ func TestDevWorker(t *testing.T) {
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)
// 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)
}