diff --git a/CHANGELOG.md b/CHANGELOG.md index 97e23748e..8537606dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. * 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)) * 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 362a7412e..01abba652 100644 --- a/internal/cmd/config/config.go +++ b/internal/cmd/config/config.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/rand" "encoding/base64" + "encoding/json" "errors" "fmt" "io" @@ -157,9 +158,10 @@ type Worker struct { // 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"` + // 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 interface{} `hcl:"tags"` // StatusGracePeriod represents the period of time (as a duration) that the // worker will wait before disconnecting connections if it cannot make a @@ -344,12 +346,60 @@ func Parse(d string) (*Config, error) { 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) { + // 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]interface{} + 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]interface{}: + 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 unmarshalling 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) } @@ -381,6 +431,7 @@ func Parse(d string) (*Config, error) { } } } + 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) diff --git a/internal/cmd/config/config_test.go b/internal/cmd/config/config_test.go index e24b38a6b..0a0e7ddfe 100644 --- a/internal/cmd/config/config_test.go +++ b/internal/cmd/config/config_test.go @@ -337,6 +337,292 @@ func TestParsingName(t *testing.T) { } } +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() diff --git a/internal/host/plugin/host_catalog_secret.go b/internal/host/plugin/host_catalog_secret.go index 84eda4867..d618b2bbf 100644 --- a/internal/host/plugin/host_catalog_secret.go +++ b/internal/host/plugin/host_catalog_secret.go @@ -2,12 +2,9 @@ package plugin import ( "context" - "database/sql" - "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/host/plugin/store" - "github.com/hashicorp/boundary/internal/oplog" wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/hashicorp/go-kms-wrapping/structwrapping" "google.golang.org/protobuf/proto" @@ -89,42 +86,3 @@ func (c *HostCatalogSecret) decrypt(ctx context.Context, cipher wrapping.Wrapper c.CtSecret = nil return nil } - -// TODO(ICU-2835): this is necessary as update logic requires a static -// public_id or private_id field. Once post-lookup updates can -// support alternate primary key fields, we can port this to native -// Update and upserts w/OnConflict. -func (c *HostCatalogSecret) upsertQuery() (query string, queryValues []interface{}) { - query = upsertHostCatalogSecretQuery - queryValues = []interface{}{ - sql.Named("catalog_id", c.CatalogId), - sql.Named("secret", c.CtSecret), - sql.Named("key_id", c.KeyId), - } - return -} - -func (c *HostCatalogSecret) deleteQuery() (query string, queryValues []interface{}) { - query = deleteHostCatalogSecretQuery - queryValues = []interface{}{ - sql.Named("catalog_id", c.CatalogId), - } - return -} - -func (c *HostCatalogSecret) oplogMessage(opType db.OpType) *oplog.Message { - msg := oplog.Message{ - Message: c.clone(), - TypeName: c.TableName(), - } - switch opType { - case db.CreateOp: - msg.OpType = oplog.OpType_OP_TYPE_CREATE - case db.UpdateOp: - msg.OpType = oplog.OpType_OP_TYPE_UPDATE - msg.FieldMaskPaths = []string{"secret", "key_id"} - case db.DeleteOp: - msg.OpType = oplog.OpType_OP_TYPE_DELETE - } - return &msg -} diff --git a/internal/host/plugin/host_catalog_secret_test.go b/internal/host/plugin/host_catalog_secret_test.go index 09bb7af02..146af909b 100644 --- a/internal/host/plugin/host_catalog_secret_test.go +++ b/internal/host/plugin/host_catalog_secret_test.go @@ -118,57 +118,82 @@ func TestHostCatalogSecret_New(t *testing.T) { } } -func TestHostCatalogSecret_Custom_Queries(t *testing.T) { - ctx := context.Background() +func TestHostCatalogSecret_Create_Upsert_Update_Delete(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") - rw := db.New(conn) wrapper := db.TestWrapper(t) kkms := kms.TestKms(t, conn, wrapper) _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) plg := host.TestPlugin(t, conn, "test") cat := TestCatalog(t, conn, prj.GetPublicId(), plg.GetPublicId()) + ctx := context.Background() + + secret, err := newHostCatalogSecret(ctx, cat.GetPublicId(), mustStruct(map[string]interface{}{ + "foo": "bar", + })) + require.NoError(t, err) + require.NotNil(t, secret) + require.Empty(t, secret.CtSecret) + databaseWrapper, err := kkms.GetWrapper(ctx, prj.PublicId, kms.KeyPurposeDatabase) require.NoError(t, err) + require.NotNil(t, databaseWrapper) + require.NoError(t, secret.encrypt(ctx, databaseWrapper)) - hcs, err := newHostCatalogSecret(ctx, cat.GetPublicId(), - &structpb.Struct{Fields: map[string]*structpb.Value{"foo": structpb.NewStringValue("bar")}}) - require.NoError(t, err) - assert.NoError(t, hcs.encrypt(ctx, databaseWrapper)) - q, v := hcs.upsertQuery() - _, err = rw.Exec(ctx, q, v) - assert.NoError(t, err) + // Create + w := db.New(conn) + require.NoError(t, w.Create(ctx, secret)) - found, err := newHostCatalogSecret(ctx, cat.GetPublicId(), nil) - require.NoError(t, err) - assert.NoError(t, rw.LookupWhere(ctx, found, "catalog_id=?", found.GetCatalogId())) + // Upsert + newStructUpsert := mustMarshal(map[string]interface{}{ + "baz": "qux", + }) + newSecretUpsert := secret.clone() + newSecretUpsert.Secret = newStructUpsert + require.NoError(t, newSecretUpsert.encrypt(ctx, databaseWrapper)) + require.NoError(t, w.Create(ctx, newSecretUpsert, db.WithOnConflict(&db.OnConflict{ + Target: db.Columns{"catalog_id"}, + Action: db.SetColumns([]string{"secret", "key_id"}), + }))) + found := &HostCatalogSecret{ + HostCatalogSecret: &store.HostCatalogSecret{ + CatalogId: cat.GetPublicId(), + }, + } + require.NoError(t, w.LookupById(ctx, found)) + assert.Empty(t, cmp.Diff(newSecretUpsert.HostCatalogSecret, found.HostCatalogSecret, protocmp.Transform())) require.NoError(t, found.decrypt(ctx, databaseWrapper)) - require.NoError(t, hcs.decrypt(ctx, databaseWrapper)) - // update the created/updated time from the original - hcs.CreateTime, hcs.UpdateTime = found.CreateTime, found.UpdateTime - assert.Empty(t, cmp.Diff(hcs, found, protocmp.Transform())) + assert.Empty(t, cmp.Diff(newStructUpsert, found.Secret, protocmp.Transform())) - // Update the secret and see the value updated. - updated, err := newHostCatalogSecret(ctx, cat.GetPublicId(), - &structpb.Struct{Fields: map[string]*structpb.Value{"updated": structpb.NewStringValue("value")}}) + // Update + newStructUpdate := mustMarshal(map[string]interface{}{ + "one": "two", + }) + newSecretUpdate := newSecretUpsert.clone() + newSecretUpdate.Secret = newStructUpdate + require.NoError(t, newSecretUpdate.encrypt(ctx, databaseWrapper)) + rowsUpdated, err := w.Update(ctx, newSecretUpdate, []string{"CtSecret"}, []string{}) require.NoError(t, err) - assert.NoError(t, updated.encrypt(ctx, databaseWrapper)) - q, v = updated.upsertQuery() - _, err = rw.Exec(ctx, q, v) - assert.NoError(t, err) - - assert.NoError(t, rw.LookupWhere(ctx, found, "catalog_id=?", found.GetCatalogId())) + require.Equal(t, 1, rowsUpdated) + found = &HostCatalogSecret{ + HostCatalogSecret: &store.HostCatalogSecret{ + CatalogId: cat.GetPublicId(), + }, + } + require.NoError(t, w.LookupById(ctx, found)) + assert.Empty(t, cmp.Diff(newSecretUpdate.HostCatalogSecret, found.HostCatalogSecret, protocmp.Transform())) require.NoError(t, found.decrypt(ctx, databaseWrapper)) - require.NoError(t, updated.decrypt(ctx, databaseWrapper)) - // set the created time to the first and update time from the newly found - updated.CreateTime, updated.UpdateTime = hcs.CreateTime, found.UpdateTime - assert.Empty(t, cmp.Diff(updated, found, protocmp.Transform())) + assert.Empty(t, cmp.Diff(newStructUpdate, found.Secret, protocmp.Transform())) - // Try to delete this secret. - q, v = updated.deleteQuery() - _, err = rw.Exec(ctx, q, v) - assert.NoError(t, err) - err = rw.LookupWhere(ctx, found, "catalog_id=?", found.GetCatalogId()) - assert.Error(t, err) - assert.True(t, errors.IsNotFoundError(err)) + // Delete + rowsDeleted, err := w.Delete(ctx, found) + require.NoError(t, err) + require.Equal(t, 1, rowsDeleted) + err = w.LookupById(ctx, &HostCatalogSecret{ + HostCatalogSecret: &store.HostCatalogSecret{ + CatalogId: cat.GetPublicId(), + }, + }) + require.Error(t, err) + require.True(t, errors.IsNotFoundError(err)) } diff --git a/internal/host/plugin/repository_host_catalog.go b/internal/host/plugin/repository_host_catalog.go index 2bf89a2c3..3b6f6469b 100644 --- a/internal/host/plugin/repository_host_catalog.go +++ b/internal/host/plugin/repository_host_catalog.go @@ -142,15 +142,11 @@ func (r *Repository) CreateCatalog(ctx context.Context, c *HostCatalog, _ ...Opt } if hcSecret != nil { newSecret := hcSecret.clone() - q, v := newSecret.upsertQuery() - rows, err := w.Exec(ctx, q, v) - if err != nil { + var sOplogMsg oplog.Message + if err := w.Create(ctx, newSecret, db.NewOplogMsg(&sOplogMsg)); err != nil { return errors.Wrap(ctx, err, op) } - if rows > 1 { - return errors.New(ctx, errors.MultipleRecords, op, "more than 1 catalog secret would have been created") - } - msgs = append(msgs, newSecret.oplogMessage(db.CreateOp)) + msgs = append(msgs, &sOplogMsg) } } @@ -400,16 +396,16 @@ func (r *Repository) UpdateCatalog(ctx context.Context, c *HostCatalog, version // We didn't set/encrypt the persisted data because there was // none returned. Just delete the entry. deletedSecret := hcSecret.clone() - q, v := deletedSecret.deleteQuery() - secretsUpdated, err := w.Exec(ctx, q, v) + var sOplogMsg oplog.Message + secretsDeleted, err := w.Delete(ctx, deletedSecret, db.NewOplogMsg(&sOplogMsg)) if err != nil { return errors.Wrap(ctx, err, op) } - if secretsUpdated != 1 { - return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("expected 1 catalog secret to be deleted, got %d", secretsUpdated)) + if secretsDeleted != 1 { + return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("expected 1 catalog secret to be deleted, got %d", secretsDeleted)) } updatedPersisted = true - msgs = append(msgs, deletedSecret.oplogMessage(db.DeleteOp)) + msgs = append(msgs, &sOplogMsg) } else { hcSecret, err := newHostCatalogSecret(ctx, currentCatalog.GetPublicId(), plgResp.GetPersisted().GetSecrets()) if err != nil { @@ -421,16 +417,20 @@ func (r *Repository) UpdateCatalog(ctx context.Context, c *HostCatalog, version // Update the secrets. updatedSecret := hcSecret.clone() - q, v := updatedSecret.upsertQuery() - secretsUpdated, err := w.Exec(ctx, q, v) - if err != nil { + var sOplogMsg oplog.Message + if err := w.Create( + ctx, + updatedSecret, + db.WithOnConflict(&db.OnConflict{ + Target: db.Columns{"catalog_id"}, + Action: db.SetColumns([]string{"secret", "key_id"}), + }), + db.NewOplogMsg(&sOplogMsg), + ); err != nil { return errors.Wrap(ctx, err, op) } - if secretsUpdated != 1 { - return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("expected 1 catalog secret to be updated, got %d", secretsUpdated)) - } updatedPersisted = true - msgs = append(msgs, updatedSecret.oplogMessage(db.UpdateOp)) + msgs = append(msgs, &sOplogMsg) } } diff --git a/internal/host/plugin/repository_host_catalog_test.go b/internal/host/plugin/repository_host_catalog_test.go index 3125a81ac..32e371639 100644 --- a/internal/host/plugin/repository_host_catalog_test.go +++ b/internal/host/plugin/repository_host_catalog_test.go @@ -1119,10 +1119,8 @@ func TestRepository_UpdateCatalog(t *testing.T) { cSecret, err := newHostCatalogSecret(ctx, cat.GetPublicId(), cat.Secrets) require.NoError(err) require.NoError(cSecret.encrypt(ctx, scopeWrapper)) - cSecretQ, cSecretV := cSecret.upsertQuery() - secretsUpdated, err := dbRW.Exec(ctx, cSecretQ, cSecretV) + err = dbRW.Create(ctx, cSecret) require.NoError(err) - require.Equal(1, secretsUpdated) // Set some (default) attributes on our test catalog and update SecretsHmac at the same time cat.Attributes = mustMarshal(map[string]interface{}{ diff --git a/internal/host/plugin/repository_host_set_test.go b/internal/host/plugin/repository_host_set_test.go index 581fb0757..31fa3c55e 100644 --- a/internal/host/plugin/repository_host_set_test.go +++ b/internal/host/plugin/repository_host_set_test.go @@ -424,10 +424,8 @@ func TestRepository_UpdateSet(t *testing.T) { scopeWrapper, err := dbKmsCache.GetWrapper(ctx, testCatalog.GetScopeId(), kms.KeyPurposeDatabase) require.NoError(t, err) require.NoError(t, testCatalogSecret.encrypt(ctx, scopeWrapper)) - testCatalogSecretQ, testCatalogSecretV := testCatalogSecret.upsertQuery() - secretsUpdated, err := dbRW.Exec(ctx, testCatalogSecretQ, testCatalogSecretV) + err = dbRW.Create(ctx, testCatalogSecret) require.NoError(t, err) - require.Equal(t, 1, secretsUpdated) // Create a test duplicate set. We don't use this set, it just // exists to ensure that we can test for conflicts when setting diff --git a/website/content/docs/concepts/filtering/worker-tags.mdx b/website/content/docs/concepts/filtering/worker-tags.mdx index dcc325508..d0d626726 100644 --- a/website/content/docs/concepts/filtering/worker-tags.mdx +++ b/website/content/docs/concepts/filtering/worker-tags.mdx @@ -53,6 +53,53 @@ worker { In this format, it is not possible to have an equal sign be a part of the key. +It is also possible to set the entire `tags` block or the keys' values within +to point to an environment variable or filepath in the system, through the +`env://` and `file://` URLs: + +```hcl +worker { + name = "web-prod-us-east-1" + tags = "env://BOUNDARY_ALL_WORKER_TAGS" +} +``` + +```hcl +worker { + name = "web-prod-us-east-1" + tags { + type = "env://BOUNDARY_WORKER_TYPE_TAGS" + region = "file://config/worker/region_tags" + usage = ["admin"] + } +} +``` + +Note that the syntax within the environment variable / file changes +slightly depending on how the configuration file is set: + +For setting the entire `tags` block, both the keys and values need +to be specified, in JSON or HCL format: + +```json +{ + "region": ["us-east-1"], + "type": ["prod", "webservers"] +} +``` + +```hcl +region = ["us-east-1"] +type = ["prod", "webservers"] +``` + +For setting the keys' values within the `tags` block, only a JSON +array with the tags intended for the particular key is required: + +```json +["prod", "webservers"] +``` + # Target Worker Filtering Once workers have tags, it is possible to use these tags to control which diff --git a/website/content/docs/configuration/worker.mdx b/website/content/docs/configuration/worker.mdx index 7a371faf0..93e1e6d17 100644 --- a/website/content/docs/configuration/worker.mdx +++ b/website/content/docs/configuration/worker.mdx @@ -39,7 +39,8 @@ worker { - `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 proxy via [worker tags](/docs/concepts/filtering/worker-tags). On `SIGHUP`, the - tags set here will be re-parsed and new values used.. + tags set here will be re-parsed and new values used. It can also be a string + referring to a file on disk (file://) or an env var (env://). ## KMS Configuration diff --git a/website/package-lock.json b/website/package-lock.json index 7d7008842..af33d1e34 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -21,7 +21,7 @@ "@hashicorp/react-content": "^8.0.2", "@hashicorp/react-docs-page": "^14.4.2", "@hashicorp/react-enterprise-alert": "^6.0.1", - "@hashicorp/react-hashi-stack-menu": "^2.0.7", + "@hashicorp/react-hashi-stack-menu": "^2.1.2", "@hashicorp/react-head": "^3.1.2", "@hashicorp/react-hero": "^8.0.2", "@hashicorp/react-image": "^4.0.3", @@ -1342,12 +1342,13 @@ "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" }, "node_modules/@hashicorp/react-hashi-stack-menu": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@hashicorp/react-hashi-stack-menu/-/react-hashi-stack-menu-2.0.7.tgz", - "integrity": "sha512-3b9VxAzSPbb5YeiKbMiygrointTb56VsMBp27mXPXS2dqrZ4pj1o5AJCYwescv7g7KbwwL+YC9hlLlhRxjErUw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@hashicorp/react-hashi-stack-menu/-/react-hashi-stack-menu-2.1.2.tgz", + "integrity": "sha512-3K6Y2FNSLq6TsMceX3a1pdmxaVx8lRvxCgjxcKL1P6DpmOk0AU5/eL0CvKP8GzXSYstL36xXxIhWFfiaAaF/yQ==", "dependencies": { - "@hashicorp/react-inline-svg": "^1.0.2", - "slugify": "1.3.4" + "@hashicorp/react-inline-svg": "^6.0.1", + "@hashicorp/react-link-wrap": "^3.0.3", + "slugify": "1.6.0" }, "peerDependencies": { "@hashicorp/mktg-global-styles": ">=3.x", @@ -1355,20 +1356,12 @@ "react": ">=16.x" } }, - "node_modules/@hashicorp/react-hashi-stack-menu/node_modules/@hashicorp/react-inline-svg": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@hashicorp/react-inline-svg/-/react-inline-svg-1.0.2.tgz", - "integrity": "sha512-AAFnBslSTgnEr++dTbMn3sybAqvn7myIj88ijGigF6u11eSRiV64zqEcyYLQKWTV6dF4AvYoxiYC6GSOgiM0Yw==", - "peerDependencies": { - "react": "^16.9.0" - } - }, "node_modules/@hashicorp/react-hashi-stack-menu/node_modules/slugify": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.4.tgz", - "integrity": "sha512-KP0ZYk5hJNBS8/eIjGkFDCzGQIoZ1mnfQRYS5WM3273z+fxGWXeN0fkwf2ebEweydv9tioZIHGZKoF21U07/nw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.0.tgz", + "integrity": "sha512-FkMq+MQc5hzYgM86nLuHI98Acwi3p4wX+a5BO9Hhw4JdK4L7WueIiZ4tXEobImPqBz2sVcV0+Mu3GRB30IGang==", "engines": { - "node": ">=4.0.0" + "node": ">=8.0.0" } }, "node_modules/@hashicorp/react-head": { @@ -2347,13 +2340,9 @@ "version": "11.1.2", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-11.1.2.tgz", "integrity": "sha512-hZuwOlGOwBZADA8EyDYyjx3+4JGIGjSHDHWrmpI7g5rFmQNltjlbaefAbiU5Kk7j3BUSDwt30quJRFv3nyJQ0w==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": ">= 10" } @@ -2362,13 +2351,9 @@ "version": "11.1.2", "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-11.1.2.tgz", "integrity": "sha512-PGOp0E1GisU+EJJlsmJVGE+aPYD0Uh7zqgsrpD3F/Y3766Ptfbe1lEPPWnRDl+OzSSrSrX1lkyM/Jlmh5OwNvA==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": ">= 10" } @@ -2377,13 +2362,9 @@ "version": "11.1.2", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-11.1.2.tgz", "integrity": "sha512-YcDHTJjn/8RqvyJVB6pvEKXihDcdrOwga3GfMv/QtVeLphTouY4BIcEUfrG5+26Nf37MP1ywN3RRl1TxpurAsQ==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">= 10" } @@ -2392,13 +2373,9 @@ "version": "11.1.2", "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-11.1.2.tgz", "integrity": "sha512-e/pIKVdB+tGQYa1cW3sAeHm8gzEri/HYLZHT4WZojrUxgWXqx8pk7S7Xs47uBcFTqBDRvK3EcQpPLf3XdVsDdg==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "optional": true, - "os": [ - "win32" - ], + "os": ["win32"], "engines": { "node": ">= 10" } @@ -5711,9 +5688,7 @@ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "devOptional": true, - "engines": [ - "node >= 0.8" - ], + "engines": ["node >= 0.8"], "peer": true, "dependencies": { "buffer-from": "^1.0.0", @@ -8821,9 +8796,7 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "hasInstallScript": true, "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -19529,9 +19502,7 @@ "dev": true, "hasInstallScript": true, "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "peer": true, "dependencies": { "bindings": "^1.5.0", @@ -21413,24 +21384,19 @@ } }, "@hashicorp/react-hashi-stack-menu": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@hashicorp/react-hashi-stack-menu/-/react-hashi-stack-menu-2.0.7.tgz", - "integrity": "sha512-3b9VxAzSPbb5YeiKbMiygrointTb56VsMBp27mXPXS2dqrZ4pj1o5AJCYwescv7g7KbwwL+YC9hlLlhRxjErUw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@hashicorp/react-hashi-stack-menu/-/react-hashi-stack-menu-2.1.2.tgz", + "integrity": "sha512-3K6Y2FNSLq6TsMceX3a1pdmxaVx8lRvxCgjxcKL1P6DpmOk0AU5/eL0CvKP8GzXSYstL36xXxIhWFfiaAaF/yQ==", "requires": { - "@hashicorp/react-inline-svg": "^1.0.2", - "slugify": "1.3.4" + "@hashicorp/react-inline-svg": "^6.0.1", + "@hashicorp/react-link-wrap": "^3.0.3", + "slugify": "1.6.0" }, "dependencies": { - "@hashicorp/react-inline-svg": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@hashicorp/react-inline-svg/-/react-inline-svg-1.0.2.tgz", - "integrity": "sha512-AAFnBslSTgnEr++dTbMn3sybAqvn7myIj88ijGigF6u11eSRiV64zqEcyYLQKWTV6dF4AvYoxiYC6GSOgiM0Yw==", - "requires": {} - }, "slugify": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.4.tgz", - "integrity": "sha512-KP0ZYk5hJNBS8/eIjGkFDCzGQIoZ1mnfQRYS5WM3273z+fxGWXeN0fkwf2ebEweydv9tioZIHGZKoF21U07/nw==" + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.0.tgz", + "integrity": "sha512-FkMq+MQc5hzYgM86nLuHI98Acwi3p4wX+a5BO9Hhw4JdK4L7WueIiZ4tXEobImPqBz2sVcV0+Mu3GRB30IGang==" } } }, diff --git a/website/package.json b/website/package.json index ee9fcb45b..c8e9e3324 100644 --- a/website/package.json +++ b/website/package.json @@ -17,7 +17,7 @@ "@hashicorp/react-content": "^8.0.2", "@hashicorp/react-docs-page": "^14.4.2", "@hashicorp/react-enterprise-alert": "^6.0.1", - "@hashicorp/react-hashi-stack-menu": "^2.0.7", + "@hashicorp/react-hashi-stack-menu": "^2.1.2", "@hashicorp/react-head": "^3.1.2", "@hashicorp/react-hero": "^8.0.2", "@hashicorp/react-image": "^4.0.3",