diff --git a/internal/db/schema/postgres_migration.gen.go b/internal/db/schema/postgres_migration.gen.go index d6373fb49..1c8e4724d 100644 --- a/internal/db/schema/postgres_migration.gen.go +++ b/internal/db/schema/postgres_migration.gen.go @@ -7059,7 +7059,7 @@ alter table wh_host_dimension check(length(trim(plugin_name)) > 0) constraint plugin_name_must_be_lowercase check(lower(trim(plugin_name)) = plugin_name) - constraint plugin_host_name_uq + constraint plugin_host_plugin_name_uq unique, id_prefix text not null constraint plugin_id_prefix_must_be_not_empty @@ -7217,7 +7217,7 @@ alter table wh_host_dimension update_time wt_timestamp, version wt_version, attributes bytea not null - constraint attributes_must_not_empty + constraint attributes_must_not_be_empty check(length(attributes) > 0), constraint host_catalog_fkey foreign key (scope_id, public_id) @@ -7289,7 +7289,7 @@ alter table wh_host_dimension update_time wt_timestamp, version wt_version, attributes bytea not null - constraint attributes_must_not_empty + constraint attributes_must_not_be_empty check(length(attributes) > 0), constraint host_plugin_set_catalog_id_name_uq unique(catalog_id, name), diff --git a/internal/plugin/host/plugin.go b/internal/plugin/host/plugin.go index ddabc58ec..b6a205ca7 100644 --- a/internal/plugin/host/plugin.go +++ b/internal/plugin/host/plugin.go @@ -1,8 +1,10 @@ package host import ( + "github.com/hashicorp/boundary/internal/oplog" "github.com/hashicorp/boundary/internal/plugin/host/store" "github.com/hashicorp/boundary/internal/types/scope" + "google.golang.org/protobuf/proto" ) // A Plugin enables additional logic to be used by boundary. @@ -41,3 +43,28 @@ func (c *Plugin) TableName() string { func (c *Plugin) SetTableName(n string) { c.tableName = n } + +func allocPlugin() *Plugin { + return &Plugin{ + Plugin: &store.Plugin{}, + } +} + +func (c *Plugin) clone() *Plugin { + cp := proto.Clone(c.Plugin) + return &Plugin{ + Plugin: cp.(*store.Plugin), + } +} + +func newPluginMetadata(p *Plugin, op oplog.OpType) oplog.Metadata { + metadata := oplog.Metadata{ + "resource-public-id": []string{p.GetPublicId()}, + "resource-type": []string{"host plugin"}, + "op-type": []string{op.String()}, + } + if p.ScopeId != "" { + metadata["scope-id"] = []string{p.ScopeId} + } + return metadata +} diff --git a/internal/plugin/host/plugin_test.go b/internal/plugin/host/plugin_test.go index 01bccd8ae..a70476494 100644 --- a/internal/plugin/host/plugin_test.go +++ b/internal/plugin/host/plugin_test.go @@ -18,7 +18,6 @@ func TestPlugin_Create(t *testing.T) { type args struct { pluginName string idPrefix string - semVer string opts []Option } diff --git a/internal/plugin/host/repository.go b/internal/plugin/host/repository.go new file mode 100644 index 000000000..a8604342a --- /dev/null +++ b/internal/plugin/host/repository.go @@ -0,0 +1,47 @@ +package host + +import ( + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" +) + +// A Repository stores and retrieves the persistent types in the host +// package. It is not safe to use a repository concurrently. +type Repository struct { + reader db.Reader + writer db.Writer + kms *kms.Kms + // defaultLimit provides a default for limiting the number of results + // returned from the repo + defaultLimit int +} + +// NewRepository creates a new Repository. The returned repository should +// only be used for one transaction and it is not safe for concurrent go +// routines to access it. WithLimit option is used as a repo wide default +// limit applied to all ListX methods. +func NewRepository(r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*Repository, error) { + const op = "static.NewRepository" + switch { + case r == nil: + return nil, errors.NewDeprecated(errors.InvalidParameter, op, "db.Reader") + case w == nil: + return nil, errors.NewDeprecated(errors.InvalidParameter, op, "db.Writer") + case kms == nil: + return nil, errors.NewDeprecated(errors.InvalidParameter, op, "kms") + } + + opts := getOpts(opt...) + if opts.withLimit == 0 { + // zero signals the boundary defaults should be used. + opts.withLimit = db.DefaultLimit + } + + return &Repository{ + reader: r, + writer: w, + kms: kms, + defaultLimit: opts.withLimit, + }, nil +} diff --git a/internal/plugin/host/repository_plugin.go b/internal/plugin/host/repository_plugin.go new file mode 100644 index 000000000..574ade970 --- /dev/null +++ b/internal/plugin/host/repository_plugin.go @@ -0,0 +1,151 @@ +package host + +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/internal/types/scope" +) + +// CreatePlugin inserts p into the repository and returns a new +// Plugin containing the plugin's PublicId. p is not changed. p must +// contain a valid ScopeID. p must not contain a PublicId. The PublicId is +// generated and assigned by this method. opt is ignored. +// +// Both p.IdPrefix and p.PluginName are required and must match. The +// p.IdPrefix must contain only lower case alphanumeric characters. +// +// Both p.Name and p.Description are optional. If p.Name is set, it must be +// unique within p.ScopeID. +// +// Both p.CreateTime and c.UpdateTime are ignored. +func (r *Repository) CreatePlugin(ctx context.Context, p *Plugin, _ ...Option) (*Plugin, error) { + const op = "host.(Repository).CreatePlugin" + if p == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "nil Plugin") + } + if p.Plugin == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "nil embedded Plugin") + } + if p.ScopeId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no scope id") + } + if p.ScopeId != scope.Global.String() { + return nil, errors.New(ctx, errors.InvalidParameter, op, "scope id is not 'global'") + } + if p.PublicId != "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "public id not empty") + } + if p.PluginName == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no plugin name") + } + if p.IdPrefix == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no id prefix") + } + // TODO: remove the restriction on prefix having to be the same as plugin name + if p.PluginName != p.IdPrefix { + return nil, errors.New(ctx, errors.InvalidParameter, op, "id prefix and plugin name don't match") + } + + p = p.clone() + + id, err := newPluginId() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + p.PublicId = id + + oplogWrapper, err := r.kms.GetWrapper(ctx, p.ScopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + metadata := newPluginMetadata(p, oplog.OpType_OP_TYPE_CREATE) + + var newPlugin *Plugin + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + newPlugin = p.clone() + err := w.Create( + ctx, + newPlugin, + db.WithOplog(oplogWrapper, metadata), + ) + if err != nil { + return errors.Wrap(ctx, err, op) + } + return nil + }, + ) + + if err != nil { + if errors.IsUniqueError(err) { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in scope: %s: name %s already exists", p.ScopeId, p.Name))) + } + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in scope: %s", p.ScopeId))) + } + return newPlugin, nil +} + +// LookupPlugin returns the Plugin for id. Returns nil, nil if no +// Plugin is found for id. +func (r *Repository) LookupPlugin(ctx context.Context, id string, _ ...Option) (*Plugin, error) { + const op = "host.(Repository).LookupPlugin" + if id == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no public id") + } + c := allocPlugin() + c.PublicId = id + if err := r.reader.LookupByPublicId(ctx, c); err != nil { + if errors.IsNotFoundError(err) { + return nil, nil + } + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for: %s", id))) + } + return c, nil +} + +// LookupPluginByPluginName returns the Plugin for a given name. Returns nil, nil if no +// Plugin is found with that plugin name. +func (r *Repository) LookupPluginByPluginName(ctx context.Context, name string, _ ...Option) (*Plugin, error) { + const op = "host.(Repository).LookupPluginByPluginName" + if name == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no plugin name") + } + p := allocPlugin() + + if err := r.reader.LookupWhere(ctx, p, "plugin_name=?", name); err != nil { + if errors.IsNotFoundError(err) { + return nil, nil + } + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for: %s", name))) + } + return p, nil +} + +// ListPlugins returns a slice of Plugins for the scope IDs. WithLimit is the only option supported. +func (r *Repository) ListPlugins(ctx context.Context, scopeIds []string, opt ...Option) ([]*Plugin, error) { + const op = "host.(Repository).ListPlugins" + if len(scopeIds) == 0 { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no scope id") + } + opts := getOpts(opt...) + limit := r.defaultLimit + if opts.withLimit != 0 { + // non-zero signals an override of the default limit for the repo. + limit = opts.withLimit + } + var plugins []*Plugin + err := r.reader.SearchWhere(ctx, &plugins, "scope_id in (?)", []interface{}{scopeIds}, db.WithLimit(limit)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return plugins, nil +} diff --git a/internal/plugin/host/repository_plugin_test.go b/internal/plugin/host/repository_plugin_test.go new file mode 100644 index 000000000..262aaf99c --- /dev/null +++ b/internal/plugin/host/repository_plugin_test.go @@ -0,0 +1,326 @@ +package host + +import ( + "context" + "strings" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/plugin/host/store" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepository_CreatePlugin(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + iam.TestRepo(t, conn, wrapper) + kmsCache := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kmsCache) + assert.NoError(t, err) + assert.NotNil(t, repo) + + tests := []struct { + name string + in *Plugin + opts []Option + want *Plugin + wantIsErr errors.Code + }{ + { + name: "nil-plugin", + wantIsErr: errors.InvalidParameter, + }, + { + name: "nil-embedded-plugin", + in: &Plugin{}, + wantIsErr: errors.InvalidParameter, + }, + { + name: "valid-no-options", + in: &Plugin{ + Plugin: &store.Plugin{ + ScopeId: scope.Global.String(), + IdPrefix: "validnooptions", + PluginName: "validnooptions", + }, + }, + want: &Plugin{ + Plugin: &store.Plugin{ + ScopeId: scope.Global.String(), + IdPrefix: "validnooptions", + PluginName: "validnooptions", + }, + }, + }, + { + name: "valid-with-name", + in: &Plugin{ + Plugin: &store.Plugin{ + Name: "test-name-repo", + ScopeId: scope.Global.String(), + IdPrefix: "validwithname", + PluginName: "validwithname", + }, + }, + want: &Plugin{ + Plugin: &store.Plugin{ + Name: "test-name-repo", + ScopeId: scope.Global.String(), + IdPrefix: "validwithname", + PluginName: "validwithname", + }, + }, + }, + { + name: "valid-with-description", + in: &Plugin{ + Plugin: &store.Plugin{ + Description: "test-description-repo", + ScopeId: scope.Global.String(), + IdPrefix: "validwithdescription", + PluginName: "validwithdescription", + }, + }, + want: &Plugin{ + Plugin: &store.Plugin{ + Description: "test-description-repo", + ScopeId: scope.Global.String(), + IdPrefix: "validwithdescription", + PluginName: "validwithdescription", + }, + }, + }, + { + name: "mismatching-prefix-plugin-name", + in: &Plugin{ + Plugin: &store.Plugin{ + ScopeId: scope.Global.String(), + IdPrefix: "foo", + PluginName: "bar", + }, + }, + wantIsErr: errors.InvalidParameter, + }, + { + name: "non-global-scope", + in: &Plugin{ + Plugin: &store.Plugin{ + ScopeId: "o_1234567890", + IdPrefix: "foo", + PluginName: "foo", + }, + }, + wantIsErr: errors.InvalidParameter, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + got, err := repo.CreatePlugin(context.Background(), tt.in, tt.opts...) + if tt.wantIsErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "want err: %q got: %q", tt.wantIsErr, err) + assert.Nil(got) + return + } + require.NoError(t, err) + assert.NoError(err) + assert.Empty(tt.in.PublicId) + assert.NotNil(got) + assertPublicId(t, PluginPrefix, got.PublicId) + assert.NotSame(tt.in, got) + assert.Equal(tt.want.Name, got.Name) + assert.Equal(tt.want.Description, got.Description) + assert.Equal(got.CreateTime, got.UpdateTime) + }) + } + + t.Run("invalid-duplicate-names", func(t *testing.T) { + assert := assert.New(t) + kms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms) + assert.NoError(err) + assert.NotNil(repo) + in := &Plugin{ + Plugin: &store.Plugin{ + ScopeId: scope.Global.String(), + IdPrefix: "invalidduplicatenames", + PluginName: "invalidduplicatenames", + Name: "invalid-duplicate-names", + }, + } + + got, err := repo.CreatePlugin(context.Background(), in) + assert.NoError(err) + assert.NotNil(got) + assertPublicId(t, PluginPrefix, got.PublicId) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + assert.Equal(got.CreateTime, got.UpdateTime) + + got2, err := repo.CreatePlugin(context.Background(), in) + assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "want err code: %v got err: %v", errors.NotUnique, err) + assert.Nil(got2) + }) + + t.Run("invalid-duplicate-plugin-names-prefixes", func(t *testing.T) { + assert := assert.New(t) + kms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms) + assert.NoError(err) + assert.NotNil(repo) + in := &Plugin{ + Plugin: &store.Plugin{ + ScopeId: scope.Global.String(), + IdPrefix: "invalidduplicatepluginnames", + PluginName: "invalidduplicatepluginnames", + }, + } + + got, err := repo.CreatePlugin(context.Background(), in) + assert.NoError(err) + assert.NotNil(got) + assertPublicId(t, PluginPrefix, got.PublicId) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + assert.Equal(got.CreateTime, got.UpdateTime) + + got2, err := repo.CreatePlugin(context.Background(), in) + assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "want err code: %v got err: %v", errors.NotUnique, err) + assert.Nil(got2) + }) +} + +func assertPublicId(t *testing.T, prefix, actual string) { + t.Helper() + assert.NotEmpty(t, actual) + parts := strings.Split(actual, "_") + assert.Equalf(t, 2, len(parts), "want one '_' in PublicId, got multiple in %q", actual) + assert.Equalf(t, prefix, parts[0], "PublicId want prefix: %q, got: %q in %q", prefix, parts[0], actual) +} + +func TestRepository_LookupPlugin(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + plg := TestPlugin(t, conn, "test", "test") + badId, err := newPluginId() + assert.NoError(t, err) + assert.NotNil(t, badId) + + tests := []struct { + name string + id string + want *Plugin + wantErr errors.Code + }{ + { + name: "found", + id: plg.GetPublicId(), + want: plg, + }, + { + name: "not-found", + id: badId, + want: nil, + }, + { + name: "bad-public-id", + id: "", + want: nil, + wantErr: errors.InvalidParameter, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + kms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms) + assert.NoError(err) + assert.NotNil(repo) + + got, err := repo.LookupPlugin(context.Background(), tt.id) + if tt.wantErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + return + } + assert.NoError(err) + + switch { + case tt.want == nil: + assert.Nil(got) + case tt.want != nil: + assert.NotNil(got) + assert.Equal(got, tt.want) + } + }) + } +} + +func TestRepository_LookupPluginByPluginName(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + plg := TestPlugin(t, conn, "name123", "name123") + + tests := []struct { + name string + pluginName string + want *Plugin + wantErr errors.Code + }{ + { + name: "found", + pluginName: plg.GetPluginName(), + want: plg, + }, + { + name: "not-found", + pluginName: "randomname", + want: nil, + }, + { + name: "emptyname", + pluginName: "", + want: nil, + wantErr: errors.InvalidParameter, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + kms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(rw, rw, kms) + assert.NoError(err) + assert.NotNil(repo) + + got, err := repo.LookupPluginByPluginName(context.Background(), tt.pluginName) + if tt.wantErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + return + } + assert.NoError(err) + + switch { + case tt.want == nil: + assert.Nil(got) + case tt.want != nil: + assert.NotNil(got) + assert.Equal(got, tt.want) + } + }) + } +} diff --git a/internal/plugin/host/repository_test.go b/internal/plugin/host/repository_test.go new file mode 100644 index 000000000..ba4b57797 --- /dev/null +++ b/internal/plugin/host/repository_test.go @@ -0,0 +1,118 @@ +package host + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" +) + +func TestRepository_New(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + + type args struct { + r db.Reader + w db.Writer + kms *kms.Kms + opts []Option + } + + tests := []struct { + name string + args args + want *Repository + wantIsErr errors.Code + }{ + { + name: "valid", + args: args{ + r: rw, + w: rw, + kms: kmsCache, + }, + want: &Repository{ + reader: rw, + writer: rw, + kms: kmsCache, + defaultLimit: db.DefaultLimit, + }, + }, + { + name: "valid-with-limit", + args: args{ + r: rw, + w: rw, + kms: kmsCache, + opts: []Option{WithLimit(5)}, + }, + want: &Repository{ + reader: rw, + writer: rw, + kms: kmsCache, + defaultLimit: 5, + }, + }, + { + name: "nil-reader", + args: args{ + r: nil, + w: rw, + kms: kmsCache, + }, + want: nil, + wantIsErr: errors.InvalidParameter, + }, + { + name: "nil-writer", + args: args{ + r: rw, + w: nil, + kms: kmsCache, + }, + want: nil, + wantIsErr: errors.InvalidParameter, + }, + { + name: "nil-kms", + args: args{ + r: rw, + w: rw, + kms: nil, + }, + want: nil, + wantIsErr: errors.InvalidParameter, + }, + { + name: "all-nils", + args: args{ + r: nil, + w: nil, + kms: nil, + }, + want: nil, + wantIsErr: errors.InvalidParameter, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewRepository(tt.args.r, tt.args.w, tt.args.kms, tt.args.opts...) + if tt.wantIsErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "want err: %q got: %q", tt.wantIsErr, err) + assert.Nil(got) + return + } + assert.NoError(err) + require.NotNil(got) + assert.Equal(tt.want, got) + }) + } +} diff --git a/internal/proto/local/controller/storage/plugin/store/v1/plugin.proto b/internal/proto/local/controller/storage/plugin/store/v1/plugin.proto index 8a349da46..b58884b97 100644 --- a/internal/proto/local/controller/storage/plugin/store/v1/plugin.proto +++ b/internal/proto/local/controller/storage/plugin/store/v1/plugin.proto @@ -20,4 +20,4 @@ message Plugin { // name is optional. If set, it must be unique within scope_id. // @inject_tag: `gorm:"default:null"` string name = 30; -} \ No newline at end of file +} diff --git a/sdk/pbs/plugin/host_plugin_service.pb.go b/sdk/pbs/plugin/host_plugin_service.pb.go index 40c4aed5c..e90bad60e 100644 --- a/sdk/pbs/plugin/host_plugin_service.pb.go +++ b/sdk/pbs/plugin/host_plugin_service.pb.go @@ -82,7 +82,7 @@ type OnCreateCatalogResponse struct { // Secret data to persist encrypted within Boundary. This should be used to // store authentication data and other necessary configuration to be used in // later hooks and calls. Returning an error from the call will cause this - // data to not be persisted. If this is nil, no changes are written. + // data to not be persisted. If this is nil, nothing is written. Persisted *HostCatalogPersisted `protobuf:"bytes,10,opt,name=persisted,proto3" json:"persisted,omitempty"` } @@ -132,9 +132,9 @@ type OnUpdateCatalogRequest struct { // The existing state of the catalog. CurrentCatalog *hostcatalogs.HostCatalog `protobuf:"bytes,10,opt,name=current_catalog,json=currentCatalog,proto3" json:"current_catalog,omitempty"` - // The requested new state of the catalog. This field may contain - // optional secret data that may have been updated from old - // authentication data contained within the persisted state. + // The requested new state of the catalog. This field may contain optional + // secret data that may have been updated from old authentication data + // contained within the persisted state. NewCatalog *hostcatalogs.HostCatalog `protobuf:"bytes,20,opt,name=new_catalog,json=newCatalog,proto3" json:"new_catalog,omitempty"` // The existing persisted secret data. Persisted *HostCatalogPersisted `protobuf:"bytes,30,opt,name=persisted,proto3" json:"persisted,omitempty"` @@ -198,11 +198,10 @@ type OnUpdateCatalogResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // The updated secret data to persist encrypted within Boundary. - // It's important that this be returned if it existed previously, - // as the returned data overwrites the previously existing copy. If - // an error is returned, the update of the persisted data is - // aborted. If this is nil, no changes are written. + // The updated secret data to persist encrypted within Boundary. If an error + // is returned, the update of the persisted data is aborted. If this is nil, + // no changes are written. To remove all values, simply return an allocated + // but empty map. Persisted *HostCatalogPersisted `protobuf:"bytes,10,opt,name=persisted,proto3" json:"persisted,omitempty"` }