From b64761f38fad5843c21d7fd24ef107df5ae99e28 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 28 Oct 2021 10:09:43 -0400 Subject: [PATCH] Add repository and functions around plugin host set membership (#1629) Co-authored-by: Todd --- go.mod | 3 +- go.sum | 3 +- internal/db/option.go | 11 + internal/db/option_test.go | 11 +- internal/db/read_writer.go | 15 +- .../oss/postgres/19/05_plugin_host.up.sql | 75 ++- internal/errors/code.go | 4 + internal/errors/code_test.go | 5 + internal/errors/info.go | 4 + internal/host/plugin/host.go | 39 +- internal/host/plugin/host_set_member.go | 60 +++ internal/host/plugin/host_set_member_test.go | 204 +++++++++ internal/host/plugin/host_test.go | 9 +- internal/host/plugin/options.go | 9 + internal/host/plugin/options_test.go | 6 + internal/host/plugin/repository_host.go | 433 +++++++++++------- internal/host/plugin/repository_host_set.go | 2 +- internal/host/plugin/repository_host_test.go | 33 +- internal/host/plugin/repository_host_util.go | 262 +++++++++++ .../host/plugin/repository_host_util_test.go | 312 +++++++++++++ internal/host/plugin/testing.go | 30 +- .../local/plugin/v1/host_plugin_service.proto | 12 +- sdk/pbs/plugin/host_plugin_service.pb.go | 136 +++--- 23 files changed, 1392 insertions(+), 286 deletions(-) create mode 100644 internal/host/plugin/host_set_member.go create mode 100644 internal/host/plugin/host_set_member_test.go create mode 100644 internal/host/plugin/repository_host_util.go create mode 100644 internal/host/plugin/repository_host_util_test.go diff --git a/go.mod b/go.mod index 5a0d7310f..2370513b1 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,8 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-retryablehttp v0.7.0 github.com/hashicorp/go-rootcerts v1.0.2 - github.com/hashicorp/go-secure-stdlib/base62 v0.1.1 + github.com/hashicorp/go-secure-stdlib/awsutil v0.1.4 // indirect + github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 github.com/hashicorp/go-secure-stdlib/configutil v0.1.2 github.com/hashicorp/go-secure-stdlib/gatedwriter v0.1.1 github.com/hashicorp/go-secure-stdlib/kv-builder v0.1.1 diff --git a/go.sum b/go.sum index 9ddae006c..ad5afde4e 100644 --- a/go.sum +++ b/go.sum @@ -523,8 +523,9 @@ github.com/hashicorp/go-secure-stdlib/awsutil v0.1.1/go.mod h1:QRJZ7siKie+SZJB9j github.com/hashicorp/go-secure-stdlib/awsutil v0.1.2/go.mod h1:QRJZ7siKie+SZJB9jLbfKrs0Gd0yPWMtbneg0iU1PrY= github.com/hashicorp/go-secure-stdlib/awsutil v0.1.4 h1:aCPwC2WPyomaw5gk5ZZ7DPevN8d9DS3igkmq6DxGMjU= github.com/hashicorp/go-secure-stdlib/awsutil v0.1.4/go.mod h1:MpCPSPGLDILGb4JMm94/mMi3YysIqsXzGCzkEZjcjXg= -github.com/hashicorp/go-secure-stdlib/base62 v0.1.1 h1:6KMBnfEv0/kLAz0O76sliN5mXbCDcLfs2kP7ssP7+DQ= github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= +github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 h1:ET4pqyjiGmY09R5y+rSd70J2w45CtbWDNvGqWp/R3Ng= +github.com/hashicorp/go-secure-stdlib/base62 v0.1.2/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= github.com/hashicorp/go-secure-stdlib/configutil v0.1.2 h1:ai0P91rxlyGWkYUH/zFr7mduW2Q+2FMrXawkLM8e7NU= github.com/hashicorp/go-secure-stdlib/configutil v0.1.2/go.mod h1:EN1DJMjv9y5e/uRAuP2WPibAox5KHEiq2BjZl00aNPQ= github.com/hashicorp/go-secure-stdlib/gatedwriter v0.1.1 h1:9um9R8i0+HbRHS9d64kdvWR0/LJvo12sIonvR9zr1+U= diff --git a/internal/db/option.go b/internal/db/option.go index ba963875d..981a01a79 100644 --- a/internal/db/option.go +++ b/internal/db/option.go @@ -48,6 +48,10 @@ type Options struct { withGormFormatter hclog.Logger withMaxOpenConnections int + // withDebug indicates that the given operation should invoke Gorm's debug + // mode + withDebug bool + withOnConflict *OnConflict withRowsAffected *int64 } @@ -189,6 +193,13 @@ func WithMaxOpenConnections(max int) Option { } } +// WithDebug specifies the given operation should invoke debug mode in Gorm +func WithDebug(with bool) Option { + return func(o *Options) { + o.withDebug = with + } +} + // WithOnConflict specifies an optional on conflict criteria which specify // alternative actions to take when an insert results in a unique constraint or // exclusion constraint error diff --git a/internal/db/option_test.go b/internal/db/option_test.go index f2cf9588b..24e7159c1 100644 --- a/internal/db/option_test.go +++ b/internal/db/option_test.go @@ -192,13 +192,22 @@ func Test_getOpts(t *testing.T) { testOpts.withMaxOpenConnections = 22 assert.Equal(opts, testOpts) }) + t.Run("WithDebug", func(t *testing.T) { + assert := assert.New(t) + // test default of false + opts := GetOpts() + testOpts := getDefaultOptions() + assert.Equal(opts, testOpts) + // try setting to true + opts = GetOpts(WithDebug(true)) + testOpts.withDebug = true + }) t.Run("WithOnConflict", func(t *testing.T) { assert := assert.New(t) // test default of false opts := GetOpts() testOpts := getDefaultOptions() assert.Equal(opts, testOpts) - columns := SetColumns([]string{"name", "description"}) columnValues := SetColumnValues(map[string]interface{}{"expiration": "NULL"}) testOnConflict := OnConflict{ diff --git a/internal/db/read_writer.go b/internal/db/read_writer.go index e942147ae..749d5eb85 100644 --- a/internal/db/read_writer.go +++ b/internal/db/read_writer.go @@ -86,14 +86,14 @@ type Writer interface { // rows updated or an error. Supported options: WithOplog. Update(ctx context.Context, i interface{}, fieldMaskPaths []string, setToNullPaths []string, opt ...Option) (int, error) - // Create an object in the db with options: WithOplog - // the caller is responsible for the transaction life cycle of the writer + // Create an object in the db with options: WithOplog, WithOnConflict. + // The caller is responsible for the transaction life cycle of the writer // and if an error is returned the caller must decide what to do with // the transaction, which almost always should be to rollback. Create(ctx context.Context, i interface{}, opt ...Option) error // CreateItems will create multiple items of the same type. - // Supported options: WithOplog and WithOplogMsgs. WithOplog and + // Supported options: WithOplog and WithOplogMsgs. WithOplog and // WithOplogMsgs may not be used together. WithLookup is not a // supported option. The caller is responsible for the transaction life // cycle of the writer and if an error is returned the caller must decide @@ -101,15 +101,15 @@ type Writer interface { // rollback. CreateItems(ctx context.Context, createItems []interface{}, opt ...Option) error - // Delete an object in the db with options: WithOplog - // the caller is responsible for the transaction life cycle of the writer + // Delete an object in the db with options: WithOplog, WithDebug. + // The caller is responsible for the transaction life cycle of the writer // and if an error is returned the caller must decide what to do with // the transaction, which almost always should be to rollback. Delete // returns the number of rows deleted or an error. Delete(ctx context.Context, i interface{}, opt ...Option) (int, error) // DeleteItems will delete multiple items of the same type. - // Supported options: WithOplog and WithOplogMsgs. WithOplog and + // Supported options: WithOplog and WithOplogMsgs. WithOplog and // WithOplogMsgs may not be used together. The caller is responsible for the // transaction life cycle of the writer and if an error is returned the // caller must decide what to do with the transaction, which almost always @@ -664,6 +664,9 @@ func (rw *Db) Delete(ctx context.Context, i interface{}, opt ...Option) (int, er if opts.withWhereClause != "" { db = db.Where(opts.withWhereClause, opts.withWhereClauseArgs...) } + if opts.withDebug { + db = db.Debug() + } db = db.Delete(i) if db.Error != nil { return NoRowsAffected, errors.Wrap(ctx, db.Error, op, errors.WithoutEvent()) diff --git a/internal/db/schema/migrations/oss/postgres/19/05_plugin_host.up.sql b/internal/db/schema/migrations/oss/postgres/19/05_plugin_host.up.sql index cc68eb046..fa17b888b 100644 --- a/internal/db/schema/migrations/oss/postgres/19/05_plugin_host.up.sql +++ b/internal/db/schema/migrations/oss/postgres/19/05_plugin_host.up.sql @@ -178,8 +178,6 @@ begin; references host (catalog_id, public_id) on delete cascade on update cascade, - constraint host_plugin_host_catalog_id_name_uq - unique(catalog_id, name), constraint host_plugin_host_catalog_id_external_id_uq unique(catalog_id, external_id), constraint host_plugin_host_catalog_id_public_id_uq @@ -195,8 +193,29 @@ begin; create trigger immutable_columns before update on host_plugin_host for each row execute procedure immutable_columns('public_id', 'catalog_id', 'external_id', 'create_time'); - create trigger insert_host_subtype before insert on host_plugin_host - for each row execute procedure insert_host_subtype(); + -- insert_host_plugin_host_subtype is intended as a before insert trigger on + -- host_plugin_host. Its purpose is to insert a base host for new plugin-based + -- hosts. It's a bit different than the standard trigger for this, because it + -- will have conflicting PKs and we just want to "do nothing" on those + -- conflicts, deferring the raising on an error to insert into the + -- host_plugin_host table. This allows the upsert-style workflow. + create or replace function + insert_host_plugin_host_subtype() + returns trigger + as $$ + begin + insert into host + (public_id, catalog_id) + values + (new.public_id, new.catalog_id) + on conflict do nothing; + + return new; + end; + $$ language plpgsql; + + create trigger insert_host_plugin_host_subtype before insert on host_plugin_host + for each row execute procedure insert_host_plugin_host_subtype(); create trigger delete_host_subtype after delete on host_plugin_host for each row execute procedure delete_host_subtype(); @@ -309,40 +328,19 @@ begin; create trigger insert_host_plugin_set_member before insert on host_plugin_set_member for each row execute procedure insert_host_plugin_set_member(); - -- delete_orphaned_host_plugin_host is an after delete trigger - -- function to delete plugin hosts when no more set members - -- reference it. - create function delete_orphaned_host_plugin_host() - returns trigger - as $$ - begin - delete from host_plugin_host - where not exists (select host_id - from host_plugin_set_member - where host_id = old.host_id) - and - public_id = old.host_id; - return null; - end; - $$ language plpgsql; - comment on function delete_orphaned_host_plugin_host is - 'delete_orphaned_host_plugin_host deletes a host when all set members with that host are deleted.'; - - create trigger delete_orphaned_host_plugin_host after delete on host_plugin_set_member - for each row execute procedure delete_orphaned_host_plugin_host(); - insert into oplog_ticket (name, version) values ('host_plugin_catalog', 1), ('host_plugin_catalog_secret', 1), - ('host_plugin_set', 1); + ('host_plugin_set', 1), + ('host_plugin_host', 1); - -- host_plugin_host_with_value_obj is useful for reading a plugin host with - -- its associated value objects (ip addresses, dns names, set membership) as - -- columns with delimited values. The delimiter depends on the value objects - -- (e.g. if they need ordering). - create view host_plugin_host_with_value_obj as + -- host_plugin_host_with_value_obj_and_set_memberships is useful for reading a + -- plugin host with its associated value objects (ip addresses, dns names) and + -- set membership as columns with delimited values. The delimiter depends on + -- the value objects (e.g. if they need ordering). + create view host_plugin_host_with_value_obj_and_set_memberships as select h.public_id, h.catalog_id, @@ -354,15 +352,16 @@ begin; h.update_time, -- the string_agg(..) column will be null if there are no associated value objects string_agg(distinct host(hip.address), '|') as ip_addresses, - string_agg(distinct hdns.name, '|') as dns_names + string_agg(distinct hdns.name, '|') as dns_names, + string_agg(distinct hpsm.set_id, '|') as set_ids from host_plugin_host h - join host_plugin_catalog hc on h.catalog_id = hc.public_id - left outer join host_ip_address hip on h.public_id = hip.host_id - left outer join host_dns_name hdns on h.public_id = hdns.host_id - -- FIXME: add set membership once that's shaken out + join host_plugin_catalog hc on h.catalog_id = hc.public_id + left outer join host_ip_address hip on h.public_id = hip.host_id + left outer join host_dns_name hdns on h.public_id = hdns.host_id + left outer join host_plugin_set_member hpsm on h.public_id = hpsm.host_id group by h.public_id, hc.plugin_id; - comment on view host_plugin_host_with_value_obj is + comment on view host_plugin_host_with_value_obj_and_set_memberships is 'host plugin host with its associated value objects'; commit; diff --git a/internal/errors/code.go b/internal/errors/code.go index 02ea814ec..1c4a75846 100644 --- a/internal/errors/code.go +++ b/internal/errors/code.go @@ -89,6 +89,10 @@ const ( // reasons related to the state of the DDL and/or inputs (such as we're // already in the right state and don't want to end up writing oplogs). GracefullyAborted Code = 1106 + // UnexpectedRowsAffected indicates that an action expected to operate on a + // specific number of records returned a different count, e.g. if you + // expected to delete three items and only one was deleted. + UnexpectedRowsAffected Code = 1107 // Migration setup errors are codes 2000-2999 MigrationIntegrity Code = 2000 // MigrationIntegrity represents an error with the generated migration related code diff --git a/internal/errors/code_test.go b/internal/errors/code_test.go index 0b491f66c..78326f4de 100644 --- a/internal/errors/code_test.go +++ b/internal/errors/code_test.go @@ -292,6 +292,11 @@ func TestCode_Both_String_Info(t *testing.T) { c: GracefullyAborted, want: GracefullyAborted, }, + { + name: "UnexpectedRowsAffected", + c: UnexpectedRowsAffected, + want: UnexpectedRowsAffected, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/errors/info.go b/internal/errors/info.go index c9ec7a77c..40004249d 100644 --- a/internal/errors/info.go +++ b/internal/errors/info.go @@ -236,4 +236,8 @@ var errorCodeInfo = map[Code]Info{ Message: "purposefully aborted without error", Kind: Other, }, + UnexpectedRowsAffected: { + Message: "unexpected number of rows affected", + Kind: Integrity, + }, } diff --git a/internal/host/plugin/host.go b/internal/host/plugin/host.go index 9cccc4839..2da402973 100644 --- a/internal/host/plugin/host.go +++ b/internal/host/plugin/host.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/host/plugin/store" + "github.com/hashicorp/boundary/internal/oplog" "google.golang.org/protobuf/proto" ) @@ -16,14 +17,15 @@ import ( // field for this host's host catalog. type Host struct { *store.Host - PluginId string `gorm:"-"` - tableName string `gorm:"-"` + PluginId string `gorm:"-"` + SetIds []string `gorm:"-"` + tableName string `gorm:"-"` } // newHost creates a new in memory Host assigned to catalogId with an address. // Supported options: WithName, WithDescription, WithIpAddresses, WithDnsNames, // WithPluginId. Others ignored. -func newHost(ctx context.Context, catalogId, externalId string, opt ...Option) *Host { +func NewHost(ctx context.Context, catalogId, externalId string, opt ...Option) *Host { opts := getOpts(opt...) h := &Host{ @@ -74,10 +76,31 @@ func allocHost() *Host { func (h *Host) clone() *Host { cp := proto.Clone(h.Host) - return &Host{ + nh := &Host{ PluginId: h.PluginId, Host: cp.(*store.Host), } + switch { + case h.SetIds == nil: + case len(h.SetIds) == 0: + nh.SetIds = make([]string, 0) + default: + nh.SetIds = make([]string, len(h.SetIds)) + copy(nh.SetIds, h.SetIds) + } + return nh +} + +func (h *Host) oplog(op oplog.OpType) oplog.Metadata { + metadata := oplog.Metadata{ + "resource-public-id": []string{h.PublicId}, + "resource-type": []string{"plugin-host"}, + "op-type": []string{op.String()}, + } + if h.CatalogId != "" { + metadata["catalog-id"] = []string{h.CatalogId} + } + return metadata } // hostAgg is a view that aggregates the host's value objects in to @@ -93,6 +116,7 @@ type hostAgg struct { UpdateTime *timestamp.Timestamp IpAddresses string DnsNames string + SetIds string } func (agg *hostAgg) toHost(ctx context.Context) (*Host, error) { @@ -118,12 +142,17 @@ func (agg *hostAgg) toHost(ctx context.Context) (*Host, error) { sort.Strings(h.DnsNames) } + if agg.SetIds != "" { + h.SetIds = strings.Split(agg.SetIds, aggregateDelimiter) + sort.Strings(h.SetIds) + } + return h, nil } // TableName returns the table name for gorm func (agg *hostAgg) TableName() string { - return "host_plugin_host_with_value_obj" + return "host_plugin_host_with_value_obj_and_set_memberships" } func (agg *hostAgg) GetPublicId() string { diff --git a/internal/host/plugin/host_set_member.go b/internal/host/plugin/host_set_member.go new file mode 100644 index 000000000..912f148df --- /dev/null +++ b/internal/host/plugin/host_set_member.go @@ -0,0 +1,60 @@ +package plugin + +import ( + "context" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/host/plugin/store" +) + +// A HostSetMember represents the membership of a host in a host set. +type HostSetMember struct { + *store.HostSetMember + tableName string `gorm:"-"` +} + +// NewHostSetMember creates a new in memory HostSetMember representing the +// membership of hostId in hostSetId. +func NewHostSetMember(ctx context.Context, setId, hostId string, opt ...Option) (*HostSetMember, error) { + const op = "plugin.NewHostSetMember" + if setId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no set id") + } + if hostId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no host id") + } + member := &HostSetMember{ + HostSetMember: &store.HostSetMember{ + SetId: setId, + HostId: hostId, + }, + } + return member, nil +} + +// VetForWrite implements db.VetForWrite() interface for host set members. +func (m *HostSetMember) VetForWrite(ctx context.Context, _ db.Reader, _ db.OpType, _ ...db.Option) error { + const op = "plugin.(HostSetMember).VetForWrite" + if m.SetId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing set id") + } + if m.HostId == "" { + return errors.New(ctx, errors.InvalidParameter, op, "missing host id") + } + return nil +} + +// TableName returns the table name for the host set. +func (m *HostSetMember) TableName() string { + if m.tableName != "" { + return m.tableName + } + return "host_plugin_set_member" +} + +// SetTableName sets the table name. If the caller attempts to +// set the name to "" the name will be reset to the default name. +func (m *HostSetMember) SetTableName(n string) { + m.tableName = n +} diff --git a/internal/host/plugin/host_set_member_test.go b/internal/host/plugin/host_set_member_test.go new file mode 100644 index 000000000..47c67844e --- /dev/null +++ b/internal/host/plugin/host_set_member_test.go @@ -0,0 +1,204 @@ +package plugin + +import ( + "context" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/host/plugin/store" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + hostplg "github.com/hashicorp/boundary/internal/plugin/host" + plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestHostSetMember_InsertDelete tests insertion and validates the lookup via set IDs +// function as well. After, we remove set memberships one at a time and validate +// the cleanup function removes the hosts. +func TestHostSetMember_InsertDelete(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + + plg := hostplg.TestPlugin(t, conn, "create") + plgm := map[string]plgpb.HostPluginServiceClient{ + plg.GetPublicId(): NewWrappingPluginClient(&plgpb.UnimplementedHostPluginServiceServer{}), + } + + cats := TestCatalogs(t, conn, prj.PublicId, plg.PublicId, 2) + + blueCat := cats[0] + blueSet1 := TestSet(t, conn, kms, blueCat, plgm) + blueSet2 := TestSet(t, conn, kms, blueCat, plgm) + + hostId, err := db.NewPublicId(HostPrefix) + require.NoError(t, err) + blueHost1 := NewHost(ctx, blueCat.PublicId, "abcd", withPluginId(plg.GetPublicId())) + blueHost1.PublicId = hostId + require.NoError(t, rw.Create(ctx, blueHost1)) + + hostId, err = db.NewPublicId(HostPrefix) + require.NoError(t, err) + blueHost2 := NewHost(ctx, blueCat.PublicId, "zyxw", withPluginId(plg.GetPublicId())) + blueHost2.PublicId = hostId + require.NoError(t, rw.Create(ctx, blueHost2)) + + greenCat := cats[1] + greenSet := TestSet(t, conn, kms, greenCat, plgm) + + tests := []struct { + name string + sets []string + host *Host + wantErr bool + direct bool + }{ + { + name: "valid-host-in-set", + sets: []string{blueSet1.PublicId}, + host: blueHost1, + }, + { + name: "valid-other-host-in-set", + sets: []string{blueSet2.PublicId}, + host: blueHost2, + }, + { + name: "invalid-diff-catalogs", + sets: []string{greenSet.PublicId}, + host: blueHost1, + wantErr: true, + }, + { + name: "test-vet-for-write-no-set", + host: blueHost1, + sets: []string{""}, + wantErr: true, + direct: true, + }, + { + name: "test-vet-for-write-no-host", + sets: []string{blueSet1.PublicId}, + wantErr: true, + direct: true, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + for _, set := range tt.sets { + var got *HostSetMember + var err error + if !tt.direct { + got, err = NewHostSetMember(ctx, set, tt.host.PublicId) + } else { + got = &HostSetMember{ + HostSetMember: &store.HostSetMember{ + SetId: set, + }, + } + if tt.host != nil { + got.HostId = tt.host.PublicId + } + } + require.NoError(err) + require.NotNil(got) + err2 := rw.Create(ctx, got) + if tt.wantErr { + assert.Error(err2) + return + } + assert.NoError(err2) + } + + // Run a test on the aggregate to validate looking up sets + agg := &hostAgg{PublicId: tt.host.PublicId} + require.NoError(rw.LookupByPublicId(ctx, agg)) + h, err := agg.toHost(ctx) + require.NoError(err) + assert.ElementsMatch(h.SetIds, tt.sets) + }) + } + repo, err := NewRepository(rw, rw, kms, plgm) + require.NoError(t, err) + hosts, err := repo.ListHostsBySetIds(ctx, []string{blueSet1.PublicId, blueSet2.PublicId}) + require.NoError(t, err) + require.Len(t, hosts, 2) + + // Base case the count by catalog ID + hosts, err = repo.ListHostsByCatalogId(ctx, blueCat.PublicId) + require.NoError(t, err) + assert.Len(t, hosts, 2) + + // Delete first membership, validate host is gone + got, err := NewHostSetMember(ctx, blueSet1.PublicId, blueHost1.PublicId) + require.NoError(t, err) + require.NotNil(t, got) + num, err := rw.Delete(ctx, got) + require.NoError(t, err) + assert.Equal(t, 1, num) + require.NoError(t, repo.DeleteOrphanedHosts(ctx, blueCat.ScopeId)) + assert.NoError(t, db.TestVerifyOplog(t, rw, blueHost1.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second))) + hosts, err = repo.ListHostsByCatalogId(ctx, blueCat.PublicId) + require.NoError(t, err) + require.Len(t, hosts, 1) + + // Delete second, validate second host is gone + got, err = NewHostSetMember(ctx, blueSet2.PublicId, blueHost2.PublicId) + require.NoError(t, err) + require.NotNil(t, got) + num, err = rw.Delete(ctx, got) + require.NoError(t, err) + assert.Equal(t, 1, num) + require.NoError(t, repo.DeleteOrphanedHosts(ctx, blueCat.ScopeId)) + assert.NoError(t, db.TestVerifyOplog(t, rw, blueHost2.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second))) + hosts, err = repo.ListHostsByCatalogId(ctx, blueCat.PublicId) + require.NoError(t, err) + require.Len(t, hosts, 0) +} + +func TestHostSetMember_SetTableName(t *testing.T) { + defaultTableName := "host_plugin_set_member" + tests := []struct { + name string + initialName string + setNameTo string + want string + }{ + { + name: "new-name", + initialName: "", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + initialName: "initial", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := &HostSetMember{ + HostSetMember: &store.HostSetMember{}, + } + require.Equal(defaultTableName, def.TableName()) + s := &HostSetMember{ + HostSetMember: &store.HostSetMember{}, + tableName: tt.initialName, + } + s.SetTableName(tt.setNameTo) + assert.Equal(tt.want, s.TableName()) + }) + } +} diff --git a/internal/host/plugin/host_test.go b/internal/host/plugin/host_test.go index b82a5c3f8..3dcbcb2c1 100644 --- a/internal/host/plugin/host_test.go +++ b/internal/host/plugin/host_test.go @@ -84,7 +84,7 @@ func TestHost_Create(t *testing.T) { }, }, { - name: "duplicate-name", + name: "valid-duplicate-name-same-catalog", args: args{ catalogId: cat.GetPublicId(), externalId: "duplicate-name", @@ -99,7 +99,6 @@ func TestHost_Create(t *testing.T) { Name: "test-name", }, }, - wantErr: true, }, { name: "valid-duplicate-name-different-catalog", @@ -141,7 +140,7 @@ func TestHost_Create(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - got := newHost(ctx, tt.args.catalogId, tt.args.externalId, tt.args.opts...) + got := NewHost(ctx, tt.args.catalogId, tt.args.externalId, tt.args.opts...) require.NotNil(t, got) assert.Emptyf(t, got.PublicId, "PublicId set") assert.Equal(t, tt.want, got) @@ -208,10 +207,10 @@ func testHost(t *testing.T, conn *db.DB, catId, externId string) *Host { t.Helper() w := db.New(conn) ctx := context.Background() - host1 := newHost(ctx, catId, externId) + host1 := NewHost(ctx, catId, externId) var err error host1.PublicId, err = newHostId(ctx, catId, externId) require.NoError(t, err) require.NoError(t, w.Create(ctx, host1)) return host1 -} \ No newline at end of file +} diff --git a/internal/host/plugin/options.go b/internal/host/plugin/options.go index 0467bfa82..9829982f6 100644 --- a/internal/host/plugin/options.go +++ b/internal/host/plugin/options.go @@ -25,6 +25,7 @@ type options struct { withIpAddresses []string withDnsNames []string withLimit int + withSetIds []string } func getDefaultOptions() options { @@ -97,3 +98,11 @@ func WithLimit(l int) Option { o.withLimit = l } } + +// WithSetIds provides the ability to restrict lookups to particular matching +// sets. +func WithSetIds(with []string) Option { + return func(o *options) { + o.withSetIds = with + } +} diff --git a/internal/host/plugin/options_test.go b/internal/host/plugin/options_test.go index 758e3d619..a8c4b2b3f 100644 --- a/internal/host/plugin/options_test.go +++ b/internal/host/plugin/options_test.go @@ -50,4 +50,10 @@ func Test_GetOpts(t *testing.T) { testOpts.withIpAddresses = []string{"foo"} assert.EqualValues(t, opts, testOpts) }) + t.Run("withSetIds", func(t *testing.T) { + opts := getOpts(WithSetIds([]string{"foo"})) + testOpts := getDefaultOptions() + testOpts.withSetIds = []string{"foo"} + assert.EqualValues(t, opts, testOpts) + }) } diff --git a/internal/host/plugin/repository_host.go b/internal/host/plugin/repository_host.go index be6cc4214..938d6c6a9 100644 --- a/internal/host/plugin/repository_host.go +++ b/internal/host/plugin/repository_host.go @@ -3,11 +3,9 @@ package plugin import ( "context" "fmt" - "sort" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" - "github.com/hashicorp/boundary/internal/host" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" @@ -25,7 +23,7 @@ import ( func (r *Repository) UpsertHosts( ctx context.Context, hc *HostCatalog, - sets []string, + setIds []string, phs []*plgpb.ListHostsResponseHost, _ ...Option) ([]*Host, error) { const op = "plugin.(Repository).UpsertHosts" @@ -46,182 +44,204 @@ func (r *Repository) UpsertHosts( if hc.GetScopeId() == "" { return nil, errors.New(ctx, errors.InvalidParameter, op, "no scope id") } - if sets == nil { + if setIds == nil { return nil, errors.New(ctx, errors.InvalidParameter, op, "nil sets") } - if len(sets) == 0 { // At least one must have been given to the plugin + if len(setIds) == 0 { // At least one must have been given to the plugin return nil, errors.New(ctx, errors.InvalidParameter, op, "empty sets") } - // hostInfo stores the info we need for the transaction below as well as - // which sets they matched - type hostInfo struct { - h *Host - ips []interface{} - dnsNames []interface{} - sets map[string]struct{} - } - hostMapping := make(map[string]hostInfo, len(phs)) - var err error - var totalMsgs uint32 - - for _, ph := range phs { - totalMsgs += 2 // delete and create - - h := newHost(ctx, - hc.GetPublicId(), - ph.GetExternalId(), - withIpAddresses(ph.GetIpAddresses()), - withDnsNames(ph.GetDnsNames()), - withPluginId(hc.GetPluginId())) - h.PublicId, err = newHostId(ctx, hc.GetPublicId(), ph.GetExternalId()) - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - - var ipAddresses []interface{} - if len(h.GetIpAddresses()) > 0 { - sort.Strings(h.IpAddresses) - ipAddresses = make([]interface{}, 0, len(h.GetIpAddresses())) - for _, a := range h.GetIpAddresses() { - obj, err := host.NewIpAddress(ctx, h.PublicId, a) - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - ipAddresses = append(ipAddresses, obj) - } - } - - var dnsNames []interface{} - if len(h.GetDnsNames()) > 0 { - sort.Strings(h.DnsNames) - dnsNames = make([]interface{}, 0, len(h.GetDnsNames())) - for _, n := range h.GetDnsNames() { - obj, err := host.NewDnsName(ctx, h.PublicId, n) - if err != nil { - return nil, errors.Wrap(ctx, err, op) - } - dnsNames = append(dnsNames, obj) - } - } - - hi := hostInfo{ - h: h, - ips: ipAddresses, - dnsNames: dnsNames, - } - - for _, id := range ph.GetSetIds() { - if hi.sets == nil { - hi.sets = make(map[string]struct{}) - } - hi.sets[id] = struct{}{} - } - - hostMapping[h.PublicId] = hi - - totalMsgs += uint32(len(ipAddresses) + len(dnsNames)) - } - oplogWrapper, err := r.kms.GetWrapper(ctx, hc.GetScopeId(), kms.KeyPurposeOplog) if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) } + // First, fetch existing hosts for the set IDs passed in, and organize them + // into a lookup map by host ID for later usage + var currentHosts []*Host + var currentHostMap map[string]*Host + { + var err error + currentHosts, err = r.ListHostsBySetIds(ctx, setIds) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("looking up current hosts for returned sets")) + } + + currentHostMap = make(map[string]*Host, len(currentHosts)) + for _, h := range currentHosts { + currentHostMap[h.PublicId] = h + } + } + + // Now, parse the externally defined hosts into hostInfo values, which + // stores info useful for later comparisons + newHostMap, err := createNewHostMap(ctx, hc, phs, currentHostMap) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create new host map")) + } + var returnedHosts []*Host + // Iterate over hosts and add or update them + for _, hi := range newHostMap { + ret := hi.h.clone() - // Here's what this function does: first it deletes a host, which causes - // cascading deletes on ip addresses and dns names (but all within the DB). - // Then the host is recreated, which uses the same public ID (since it's - // based on the host catalog/external ID), along with new/updated - // information. - // - // If we ever do start allowing hosts directly in targets this may cause a - // problem because the host deletion would probably cascade to the target - // and remove it from the target host sources, but at this point there are - // several issues with that scenario so it's not soon... - // - // The main reason I'm doing it this way is because I'm not sure how to do - // oplogs with custom queries. Otherwise a custom insert/on conflict query - // could replace the initial deletion. @jimlambrt @mgaffney help? :-D - _, err = r.writer.DoTx( - ctx, - db.StdRetryCnt, - db.ExpBackoff{}, - func(r db.Reader, w db.Writer) error { - // FIXME: We need to ensure hosts no longer found have their - // memberships removed. To do this, first list all set memberships. - // This is waiting on membership to be plumbed through to gorm, - // which in turn is waiting on figuring out whether or not we can - // share that between static and plugin. - // - // Next, for each set, check whether a given host ID is in the - // hostMapping and if so, if the set ID is in the host's sets. If - // not, queue that membership entry for deletion. - msgs := make([]*oplog.Message, 0, totalMsgs+3) - ticket, err := w.GetTicket(hc) - if err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) - } - - for _, hi := range hostMapping { - ret := hi.h.clone() - - var hOplogMsg oplog.Message - // We don't need to check whether something was deleted because - // it's okay if it fails - if _, err := w.Delete(ctx, ret, db.NewOplogMsg(&hOplogMsg)); err != nil { - return errors.Wrap(ctx, err, op) + if !hi.dirtyHost && + len(hi.ipsToAdd) == 0 && + len(hi.ipsToRemove) == 0 && + len(hi.dnsNamesToAdd) == 0 && + len(hi.dnsNamesToRemove) == 0 { + returnedHosts = append(returnedHosts, ret) + continue + } + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(r db.Reader, w db.Writer) error { + msgs := make([]*oplog.Message, 0) + ticket, err := w.GetTicket(hi.h) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) } - msgs = append(msgs, &hOplogMsg) - hOplogMsg = oplog.Message{} - if err := w.Create(ctx, ret, db.NewOplogMsg(&hOplogMsg)); err != nil { - return errors.Wrap(ctx, err, op) - } - msgs = append(msgs, &hOplogMsg) - - if len(hi.ips) > 0 { - ipOplogMsgs := make([]*oplog.Message, 0, len(hi.ips)) - if err := w.CreateItems(ctx, hi.ips, db.NewOplogMsgs(&ipOplogMsgs)); err != nil { - return err + if hi.dirtyHost { + var hOplogMsg oplog.Message + onConflict := &db.OnConflict{ + Target: db.Constraint("host_plugin_host_pkey"), + Action: db.SetColumns([]string{"name", "description"}), } - msgs = append(msgs, ipOplogMsgs...) - } - - if len(hi.dnsNames) > 0 { - dnsOplogMsgs := make([]*oplog.Message, 0, len(hi.dnsNames)) - if err := w.CreateItems(ctx, hi.dnsNames, db.NewOplogMsgs(&dnsOplogMsgs)); err != nil { - return err + if err := w.Create(ctx, ret, db.NewOplogMsg(&hOplogMsg), db.WithOnConflict(onConflict)); err != nil { + return errors.Wrap(ctx, err, op) } - msgs = append(msgs, dnsOplogMsgs...) + msgs = append(msgs, &hOplogMsg) } - // FIXME: Add set memberships here + // IP handling + { + if len(hi.ipsToRemove) > 0 { + oplogMsgs := make([]*oplog.Message, 0, len(hi.ipsToRemove)) + count, err := w.DeleteItems(ctx, hi.ipsToRemove.toArray(), db.NewOplogMsgs(&oplogMsgs)) + if err != nil { + return err + } + if count != len(hi.ipsToRemove) { + return errors.New(ctx, errors.UnexpectedRowsAffected, op, fmt.Sprintf("expected to remove %d ips from host %s, removed %d", len(hi.ipsToRemove), hi.h.PublicId, count)) + } + msgs = append(msgs, oplogMsgs...) + } + if len(hi.ipsToAdd) > 0 { + oplogMsgs := make([]*oplog.Message, 0, len(hi.ipsToAdd)) + if err := w.CreateItems(ctx, hi.ipsToAdd.toArray(), db.NewOplogMsgs(&oplogMsgs)); err != nil { + return err + } + msgs = append(msgs, oplogMsgs...) + } + } + + // DNS handling + { + if len(hi.dnsNamesToRemove) > 0 { + oplogMsgs := make([]*oplog.Message, 0, len(hi.dnsNamesToRemove)) + count, err := w.DeleteItems(ctx, hi.dnsNamesToRemove.toArray(), db.NewOplogMsgs(&oplogMsgs)) + if err != nil { + return err + } + if count != len(hi.dnsNamesToRemove) { + return errors.New(ctx, errors.UnexpectedRowsAffected, op, fmt.Sprintf("expected to remove %d dns names from host %s, removed %d", len(hi.dnsNamesToRemove), hi.h.PublicId, count)) + } + msgs = append(msgs, oplogMsgs...) + } + if len(hi.dnsNamesToAdd) > 0 { + oplogMsgs := make([]*oplog.Message, 0, len(hi.dnsNamesToAdd)) + if err := w.CreateItems(ctx, hi.dnsNamesToAdd.toArray(), db.NewOplogMsgs(&oplogMsgs)); err != nil { + return err + } + msgs = append(msgs, oplogMsgs...) + } + } + + metadata := hi.h.oplog(oplog.OpType_OP_TYPE_UPDATE) + if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog")) + } returnedHosts = append(returnedHosts, ret) - } - - metadata := hc.oplog(oplog.OpType_OP_TYPE_CREATE) - if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil { - return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog")) - } - - return nil - }, - ) - - if err != nil { - if errors.IsCheckConstraintError(err) || errors.IsNotNullError(err) { - return nil, errors.New(ctx, - errors.InvalidAddress, - op, - fmt.Sprintf("in catalog: %s", hc.GetPublicId()), - errors.WithWrap(err), - ) + return nil + }, + ) + if err != nil { + return nil, errors.Wrap(ctx, err, op) } - return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in catalog: %s", hc.PublicId))) } + + // Now, check set membership changes + setMembershipsToAdd, setMembershipsToRemove, allSetIds := getSetChanges(currentHostMap, newHostMap) + + // Iterate through the sets and update memberships, one transaction per set + for setId := range allSetIds { + hs, err := NewHostSet(ctx, hc.PublicId) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + hs.PublicId = setId + + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(r db.Reader, w db.Writer) error { + msgs := make([]*oplog.Message, 0) + + ticket, err := w.GetTicket(hs) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket")) + } + + // Perform additions + for _, hostId := range setMembershipsToAdd[hs.PublicId] { + membership, err := NewHostSetMember(ctx, hs.PublicId, hostId) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + var hOplogMsg oplog.Message + if err := w.Create(ctx, membership, db.NewOplogMsg(&hOplogMsg)); err != nil { + return errors.Wrap(ctx, err, op) + } + msgs = append(msgs, &hOplogMsg) + } + + // Perform removals + for _, hostId := range setMembershipsToRemove[hs.PublicId] { + membership, err := NewHostSetMember(ctx, hs.PublicId, hostId) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + var hOplogMsg oplog.Message + rows, err := w.Delete(ctx, membership, db.NewOplogMsg(&hOplogMsg)) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if rows != 1 { + return errors.New(ctx, errors.RecordNotFound, op, "record not found when deleting set membership") + } + msgs = append(msgs, &hOplogMsg) + } + + metadata := hc.oplog(oplog.OpType_OP_TYPE_UPDATE) + if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog")) + } + + return nil + }, + ) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + } + return returnedHosts, nil } @@ -248,10 +268,10 @@ func (r *Repository) LookupHost(ctx context.Context, publicId string, opt ...Opt return h, nil } -// ListHosts returns a slice of Hosts for the catalogId. +// ListHostsByCatalogId returns a slice of Hosts for the catalogId. // WithLimit is the only option supported. -func (r *Repository) ListHosts(ctx context.Context, catalogId string, opt ...Option) ([]*Host, error) { - const op = "plugin.(Repository).ListHosts" +func (r *Repository) ListHostsByCatalogId(ctx context.Context, catalogId string, opt ...Option) ([]*Host, error) { + const op = "plugin.(Repository).ListHostsByCatalogId" if catalogId == "" { return nil, errors.New(ctx, errors.InvalidParameter, op, "no catalog id") } @@ -282,3 +302,96 @@ func (r *Repository) ListHosts(ctx context.Context, catalogId string, opt ...Opt return hosts, nil } + +// ListHostsBySetId returns a slice of Hosts for the given set IDs. +// WithLimit is the only option supported. +func (r *Repository) ListHostsBySetIds(ctx context.Context, setIds []string, opt ...Option) ([]*Host, error) { + const op = "plugin.(Repository).ListHostsBySetIds" + if len(setIds) == 0 { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no set ids") + } + 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 + } + + query := ` +public_id in + (select distinct host_id + from host_plugin_set_member + where set_id in (?)) +` + + var hostAggs []*hostAgg + err := r.reader.SearchWhere(ctx, &hostAggs, query, []interface{}{setIds}, db.WithLimit(limit)) + + switch { + case err != nil: + return nil, errors.Wrap(ctx, err, op) + case hostAggs == nil: + return nil, nil + } + + hosts := make([]*Host, 0, len(hostAggs)) + for _, ha := range hostAggs { + host, err := ha.toHost(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + hosts = append(hosts, host) + } + + return hosts, nil +} + +// DeleteOphanedHosts deletes any hosts that no longer belong to any set. +// WithLimit is the only option supported. No options are currently supported. +func (r *Repository) DeleteOrphanedHosts(ctx context.Context, scopeId string, _ ...Option) error { + const op = "plugin.(Repository).DeleteOrphanedHosts" + + oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + query := ` +public_id in + (select public_id + from host_plugin_host + where public_id not in + (select host_id from host_plugin_set_member)) +` + + var hostAggs []*hostAgg + err = r.reader.SearchWhere(ctx, &hostAggs, query, nil) + switch { + case err != nil: + return errors.Wrap(ctx, err, op) + case len(hostAggs) == 0: + return nil + } + + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(r db.Reader, w db.Writer) error { + for _, ha := range hostAggs { + h := NewHost(ctx, ha.CatalogId, ha.ExternalId) + h.PublicId = ha.PublicId + metadata := h.oplog(oplog.OpType_OP_TYPE_DELETE) + dHost := h.clone() + if _, err := w.Delete(ctx, dHost, db.WithOplog(oplogWrapper, metadata)); err != nil { + return errors.Wrap(ctx, err, op) + } + } + return nil + }) + if err != nil { + return errors.Wrap(ctx, err, op) + } + + return nil +} diff --git a/internal/host/plugin/repository_host_set.go b/internal/host/plugin/repository_host_set.go index b89821dce..96b1acf40 100644 --- a/internal/host/plugin/repository_host_set.go +++ b/internal/host/plugin/repository_host_set.go @@ -648,7 +648,7 @@ func (r *Repository) Endpoints(ctx context.Context, setIds []string) ([]*host.En if _, ok := hostIds[hostId]; !ok { hostIds[hostId] = true - host := newHost(ctx, ci.publicId, addr) + host := NewHost(ctx, ci.publicId, addr) host.PublicId = hostId hosts = append(hosts, host) } diff --git a/internal/host/plugin/repository_host_test.go b/internal/host/plugin/repository_host_test.go index de59125a9..460192d0e 100644 --- a/internal/host/plugin/repository_host_test.go +++ b/internal/host/plugin/repository_host_test.go @@ -43,7 +43,10 @@ func TestRepository_UpsertHosts(t *testing.T) { set := TestSet(t, conn, kms, catalog, plgm) setIds = append(setIds, set.GetPublicId()) } + sort.Strings(setIds) phs, exp := TestExternalHosts(t, catalog, setIds, setCount) + phs[2].Name = phs[0].Name + exp[2].Name = exp[0].Name type input struct { catalog *HostCatalog @@ -139,7 +142,7 @@ func TestRepository_UpsertHosts(t *testing.T) { wantIsErr: errors.InvalidParameter, }, { - name: "valid", + name: "valid", // Note: this also tests duplicate names in: func() *input { return &input{ catalog: catalog, @@ -164,7 +167,11 @@ func TestRepository_UpsertHosts(t *testing.T) { sort.Strings(e.IpAddresses) sort.Strings(e.DnsNames) + ph.Name, ph.Description = ph.Description, ph.Name + e.Name, e.Description = e.Description, e.Name + ph.SetIds = ph.SetIds[0 : len(ph.SetIds)-1] + e.SetIds = e.SetIds[0 : len(e.SetIds)-1] return &input{ catalog: catalog, sets: setIds, @@ -194,7 +201,9 @@ func TestRepository_UpsertHosts(t *testing.T) { // Basic tests assert.Len(got, len(in.phs)) - assert.NoError(db.TestVerifyOplog(t, rw, in.catalog.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second))) + for _, h := range in.exp { + assert.NoError(db.TestVerifyOplog(t, rw, h.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + } // Make sure outputs match. Ignore timestamps. assert.Empty( @@ -210,7 +219,7 @@ func TestRepository_UpsertHosts(t *testing.T) { ) // Check again, but via performing an explicit list - got, err = repo.ListHosts(ctx, in.catalog.GetPublicId()) + got, err = repo.ListHostsByCatalogId(ctx, in.catalog.GetPublicId()) require.NoError(err) require.NotNil(got) assert.Len(got, len(in.phs)) @@ -226,11 +235,17 @@ func TestRepository_UpsertHosts(t *testing.T) { ), ) - // Now individually call read on each host + // Now individually call read on each host, cache the matching set + // IDs, and then check membership + setIdMap := make(map[string][]string) for _, exp := range in.exp { + for _, setId := range exp.SetIds { + setIdMap[setId] = append(setIdMap[setId], exp.GetPublicId()) + } got, err := repo.LookupHost(ctx, exp.GetPublicId()) require.NoError(err) require.NotNil(got) + assert.NotEmpty(got.SetIds) assert.Empty( cmp.Diff( exp, @@ -240,6 +255,16 @@ func TestRepository_UpsertHosts(t *testing.T) { ), ) } + for setId, expHostIds := range setIdMap { + got, err = repo.ListHostsBySetIds(ctx, []string{setId}) + require.NoError(err) + require.NotNil(got) + var gotHostIds []string + for _, h := range got { + gotHostIds = append(gotHostIds, h.GetPublicId()) + } + assert.ElementsMatch(expHostIds, gotHostIds) + } }) } } diff --git a/internal/host/plugin/repository_host_util.go b/internal/host/plugin/repository_host_util.go new file mode 100644 index 000000000..fb8cd97db --- /dev/null +++ b/internal/host/plugin/repository_host_util.go @@ -0,0 +1,262 @@ +package plugin + +import ( + "context" + "sort" + + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/host" + plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" + "github.com/hashicorp/go-secure-stdlib/strutil" +) + +// valueToInterfaceMap is a map that has a function to convert values into an +// array +type valueToInterfaceMap map[string]interface{} + +func (m valueToInterfaceMap) toArray() []interface{} { + switch { + case m == nil: + return nil + case len(m) == 0: + return make([]interface{}, 0) + default: + ret := make([]interface{}, 0, len(m)) + for _, v := range m { + ret = append(ret, v) + } + return ret + } +} + +// hostInfo stores the info we need for figuring out host, set membership, +// and value object differences. It also stores dirty flags to indicate +// whether we need to update value objects or the host itself. +type hostInfo struct { + h *Host + ipsToAdd valueToInterfaceMap + ipsToRemove valueToInterfaceMap + dnsNamesToAdd valueToInterfaceMap + dnsNamesToRemove valueToInterfaceMap + dirtyHost bool +} + +func createNewHostMap(ctx context.Context, + catalog *HostCatalog, + phs []*plgpb.ListHostsResponseHost, + currentHostMap map[string]*Host) (map[string]*hostInfo, error) { + + const op = "plugin.createNewHostMap" + newHostMap := make(map[string]*hostInfo, len(phs)) + + var err error + for _, ph := range phs { + newHost := NewHost(ctx, + catalog.GetPublicId(), + ph.GetExternalId(), + WithName(ph.GetName()), + WithDescription(ph.GetDescription()), + withIpAddresses(ph.GetIpAddresses()), + withDnsNames(ph.GetDnsNames()), + withPluginId(catalog.GetPluginId())) + newHost.PublicId, err = newHostId(ctx, catalog.GetPublicId(), ph.GetExternalId()) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + newHost.SetIds = ph.SetIds + hi := &hostInfo{ + h: newHost, + } + newHostMap[newHost.PublicId] = hi + + // Check if the host is dirty; that is, we need to perform an upsert + // operation. If the host isn't dirty, we have nothing to do. Note + // that we don't check every value exhaustively; for instance, we + // assume catalog ID and external ID don't change because if they do + // the public ID will be different as well. + currHost := currentHostMap[newHost.PublicId] + switch { + case currHost == nil, + currHost.Name != newHost.Name, + currHost.Description != newHost.Description: + hi.dirtyHost = true + } + + // Get the current set of host IPs/DNS names for comparison. These + // will be in sorted order since ordering is kept in the database + // and they will have been sorted before insertion. + var currHostIps []string + var currHostDnsNames []string + if currHost != nil { + currHostIps = currHost.IpAddresses + currHostDnsNames = currHost.DnsNames + } + + // Sort these here before comparison. We always use a priority order + // based on the behavior of sort.Strings so that we can check for + // equivalency. + sort.Strings(newHost.IpAddresses) + sort.Strings(newHost.DnsNames) + + // IPs + { + switch { + case strutil.EquivalentSlices(currHostIps, newHost.GetIpAddresses()): + // Nothing to do...don't remove or add anything + + default: + // No match, so we need to remove the old ones and add the new + + // First, build up removals + for _, ip := range currHostIps { + if hi.ipsToRemove == nil { + hi.ipsToRemove = make(valueToInterfaceMap, len(currHostIps)) + } + obj, err := host.NewIpAddress(ctx, newHost.PublicId, ip) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + hi.ipsToRemove[ip] = obj + } + + // Now build up additions + for _, ip := range newHost.GetIpAddresses() { + // If it's in ipsToRemove we found it on the current host; + // finding it again here means the host still has it, so + // simply do no change. + if hi.ipsToRemove != nil && hi.ipsToRemove[ip] != nil { + delete(hi.ipsToRemove, ip) + if len(hi.ipsToRemove) == 0 { + hi.ipsToRemove = nil + } + continue + } + if hi.ipsToAdd == nil { + hi.ipsToAdd = make(valueToInterfaceMap, len(newHost.GetIpAddresses())) + } + obj, err := host.NewIpAddress(ctx, newHost.PublicId, ip) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + hi.ipsToAdd[ip] = obj + } + } + } + + // DNS names + { + switch { + case strutil.EquivalentSlices(currHostDnsNames, newHost.GetDnsNames()): + // Nothing to do...don't remove or add anything + + default: + // No match, so we need to remove the old ones and add the new + + // First, build up removals + for _, name := range currHostDnsNames { + if hi.dnsNamesToRemove == nil { + hi.dnsNamesToRemove = make(valueToInterfaceMap, len(currHostDnsNames)) + } + obj, err := host.NewDnsName(ctx, newHost.PublicId, name) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + hi.dnsNamesToRemove[name] = obj + } + + // Now build up additions + for _, name := range newHost.GetDnsNames() { + // If it's in dnsNamesToRemove we found it on the current + // host; finding it again here means the host still has it, + // so simply do no change. + if hi.dnsNamesToRemove != nil && hi.dnsNamesToRemove[name] != nil { + delete(hi.dnsNamesToRemove, name) + if len(hi.dnsNamesToRemove) == 0 { + hi.dnsNamesToRemove = nil + } + continue + } + if hi.dnsNamesToAdd == nil { + hi.dnsNamesToAdd = make(valueToInterfaceMap, len(newHost.GetDnsNames())) + } + obj, err := host.NewDnsName(ctx, newHost.PublicId, name) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + hi.dnsNamesToAdd[name] = obj + } + } + } + } + + return newHostMap, nil +} + +func getSetChanges( + currentHostMap map[string]*Host, + newHostMap map[string]*hostInfo) ( + setMembershipsToAdd, setMembershipsToRemove map[string][]string, + allSetIds map[string]struct{}) { + + // First, find sets that hosts should be added to: hosts that are new or + // have new set IDs returned. + for newHostId, newHost := range newHostMap { + var setsToAdd []string + currentHost, ok := currentHostMap[newHostId] + if !ok { + // If the host was not known about before now, any sets the host + // matches will need to be added + setsToAdd = newHost.h.SetIds + } else { + // Otherwise, add to any it doesn't currently match + for _, setId := range newHost.h.SetIds { + if !strutil.StrListContains(currentHost.SetIds, setId) { + setsToAdd = append(setsToAdd, setId) + } + } + } + // Add to the total set + for _, setToAdd := range setsToAdd { + if setMembershipsToAdd == nil { + setMembershipsToAdd = make(map[string][]string) + } + if allSetIds == nil { + allSetIds = make(map[string]struct{}) + } + setMembershipsToAdd[setToAdd] = append(setMembershipsToAdd[setToAdd], newHostId) + allSetIds[setToAdd] = struct{}{} + } + } + + // Now, do the inverse: remove hosts from sets that appear there now but no + // longer have that set ID in their current list. + for currentHostId, currentHost := range currentHostMap { + var setsToRemove []string + newHost, ok := newHostMap[currentHostId] + if !ok { + // If the host doesn't even appear now, we obviously want to remove + // it from all existing set memberships + setsToRemove = currentHost.SetIds + } else { + // Otherwise, remove it from any it doesn't currently have + for _, setId := range currentHost.SetIds { + if !strutil.StrListContains(newHost.h.SetIds, setId) { + setsToRemove = append(setsToRemove, setId) + } + } + } + // Add to the total set + for _, setToRemove := range setsToRemove { + if setMembershipsToRemove == nil { + setMembershipsToRemove = make(map[string][]string) + } + if allSetIds == nil { + allSetIds = make(map[string]struct{}) + } + setMembershipsToRemove[setToRemove] = append(setMembershipsToRemove[setToRemove], currentHostId) + allSetIds[setToRemove] = struct{}{} + } + } + + return +} diff --git a/internal/host/plugin/repository_host_util_test.go b/internal/host/plugin/repository_host_util_test.go new file mode 100644 index 000000000..fe852492d --- /dev/null +++ b/internal/host/plugin/repository_host_util_test.go @@ -0,0 +1,312 @@ +package plugin + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/boundary/internal/host" + hoststore "github.com/hashicorp/boundary/internal/host/store" + plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestUtilFunctions(t *testing.T) { + ctx := context.Background() + const externalId = "ext_1234567890" + const pluginId = "plg_1234567890" + + catalog, err := NewHostCatalog(ctx, "p_1234567890", pluginId) + require.NoError(t, err) + require.NotNil(t, catalog) + catalog.PublicId = "hc_1234567890" + baseHostId, err := newHostId(ctx, catalog.GetPublicId(), externalId) + require.NoError(t, err) + + baseResponseHost := &plgpb.ListHostsResponseHost{ + ExternalId: externalId, + Name: "base-name", + Description: "base-description", + IpAddresses: []string{"1.2.3.4", "5.6.7.8"}, + DnsNames: []string{"a.b.c", "x.y.z"}, + SetIds: []string{"set1", "set2"}, + } + baseIpsIfaces := valueToInterfaceMap{} + for _, v := range baseResponseHost.IpAddresses { + ip, err := host.NewIpAddress(ctx, baseHostId, v) + require.NoError(t, err) + baseIpsIfaces[v] = ip + } + baseDnsNamesIfaces := valueToInterfaceMap{} + for _, v := range baseResponseHost.DnsNames { + name, err := host.NewDnsName(ctx, baseHostId, v) + require.NoError(t, err) + baseDnsNamesIfaces[v] = name + } + baseHost := NewHost(ctx, catalog.PublicId, externalId) + baseHost.Name = baseResponseHost.Name + baseHost.Description = baseResponseHost.Description + baseHost.IpAddresses = baseResponseHost.IpAddresses + baseHost.DnsNames = baseResponseHost.DnsNames + baseHost.PluginId = pluginId + baseHost.SetIds = baseResponseHost.SetIds + + defaultHostFunc := func(in *Host) *Host { + return in + } + defaultSetsFunc := func() []string { + return baseHost.SetIds + } + + tests := []struct { + name string + host func(*Host) *Host + // This function should take in a base host (which will be provided as a + // clone) and transform however needed for the particular test + in func(*plgpb.ListHostsResponseHost) (*plgpb.ListHostsResponseHost, *hostInfo) + sets func() []string + setsToAdd map[string][]string + setsToRemove map[string][]string + }{ + { + name: "base", + host: func(in *Host) *Host { + return nil + }, + sets: func() []string { + return nil + }, + in: func(in *plgpb.ListHostsResponseHost) (*plgpb.ListHostsResponseHost, *hostInfo) { + hi := &hostInfo{ + dirtyHost: true, + ipsToAdd: baseIpsIfaces, + dnsNamesToAdd: baseDnsNamesIfaces, + } + return in, hi + }, + }, + { + name: "no-change", + host: defaultHostFunc, + sets: defaultSetsFunc, + in: func(in *plgpb.ListHostsResponseHost) (*plgpb.ListHostsResponseHost, *hostInfo) { + hi := &hostInfo{} + return in, hi + }, + }, + { + name: "new-name", + host: defaultHostFunc, + sets: defaultSetsFunc, + in: func(in *plgpb.ListHostsResponseHost) (*plgpb.ListHostsResponseHost, *hostInfo) { + in.Name = "newname" + hi := &hostInfo{ + dirtyHost: true, + } + return in, hi + }, + }, + { + name: "new-description", + host: defaultHostFunc, + sets: defaultSetsFunc, + in: func(in *plgpb.ListHostsResponseHost) (*plgpb.ListHostsResponseHost, *hostInfo) { + in.Description = "newdescription" + hi := &hostInfo{ + dirtyHost: true, + } + return in, hi + }, + }, + { + name: "extra-ip", + host: defaultHostFunc, + sets: defaultSetsFunc, + in: func(in *plgpb.ListHostsResponseHost) (*plgpb.ListHostsResponseHost, *hostInfo) { + const newIp = "11.22.33.44" + in.IpAddresses = append(in.IpAddresses, newIp) + ip, err := host.NewIpAddress(ctx, baseHostId, newIp) + require.NoError(t, err) + hi := &hostInfo{ + ipsToAdd: valueToInterfaceMap{newIp: ip}, + } + return in, hi + }, + }, + { + name: "remove-ip", + host: defaultHostFunc, + sets: defaultSetsFunc, + in: func(in *plgpb.ListHostsResponseHost) (*plgpb.ListHostsResponseHost, *hostInfo) { + hi := &hostInfo{ + ipsToRemove: valueToInterfaceMap{}, + } + for _, ip := range in.IpAddresses[1:] { + hi.ipsToRemove[ip] = baseIpsIfaces[ip] + } + in.IpAddresses = in.IpAddresses[0:1] + return in, hi + }, + }, + { + name: "extra-dns-name", + host: defaultHostFunc, + sets: defaultSetsFunc, + in: func(in *plgpb.ListHostsResponseHost) (*plgpb.ListHostsResponseHost, *hostInfo) { + const newName = "this.is.a.test" + in.DnsNames = append(in.DnsNames, newName) + name, err := host.NewDnsName(ctx, baseHostId, newName) + require.NoError(t, err) + hi := &hostInfo{ + dnsNamesToAdd: valueToInterfaceMap{newName: name}, + } + return in, hi + }, + }, + { + name: "remove-dns-name", + host: defaultHostFunc, + sets: defaultSetsFunc, + in: func(in *plgpb.ListHostsResponseHost) (*plgpb.ListHostsResponseHost, *hostInfo) { + hi := &hostInfo{ + dnsNamesToRemove: valueToInterfaceMap{}, + } + for _, name := range in.DnsNames[1:] { + hi.dnsNamesToRemove[name] = baseDnsNamesIfaces[name] + } + in.DnsNames = in.DnsNames[0:1] + return in, hi + }, + }, + { + name: "add-sets", + host: defaultHostFunc, + sets: func() []string { + return append(defaultSetsFunc(), "extra-set", "extra-set-2") + }, + in: func(in *plgpb.ListHostsResponseHost) (*plgpb.ListHostsResponseHost, *hostInfo) { + hi := &hostInfo{} + return in, hi + }, + setsToAdd: map[string][]string{ + "extra-set": {baseHostId}, + "extra-set-2": {baseHostId}, + }, + }, + { + name: "remove-sets", + host: defaultHostFunc, + sets: func() []string { + return defaultSetsFunc()[0:1] + }, + in: func(in *plgpb.ListHostsResponseHost) (*plgpb.ListHostsResponseHost, *hostInfo) { + hi := &hostInfo{} + return in, hi + }, + setsToRemove: map[string][]string{ + "set2": {baseHostId}, + }, + }, + { + name: "add-and-remove-sets", + host: defaultHostFunc, + sets: func() []string { + return append(defaultSetsFunc()[0:1], "extra-set") + }, + in: func(in *plgpb.ListHostsResponseHost) (*plgpb.ListHostsResponseHost, *hostInfo) { + hi := &hostInfo{} + return in, hi + }, + setsToAdd: map[string][]string{ + "extra-set": {baseHostId}, + }, + setsToRemove: map[string][]string{ + "set2": {baseHostId}, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + require, assert := require.New(t), assert.New(t) + var hi *hostInfo + currHost := tt.host(baseHost.clone()) + currentHostMap := map[string]*Host{} + if currHost != nil { + currentHostMap[baseHostId] = currHost + } + h := proto.Clone(baseResponseHost).(*plgpb.ListHostsResponseHost) + h, hi = tt.in(h) + + newHostMap, err := createNewHostMap(ctx, catalog, []*plgpb.ListHostsResponseHost{h}, currentHostMap) + require.NoError(err) + require.NotNil(newHostMap) + require.NotNil(newHostMap[baseHostId]) + got := newHostMap[baseHostId] + + // Check the various host/hostinfo bits + { + assert.Equal(h.ExternalId, got.h.ExternalId) + assert.Equal(h.Name, got.h.Name) + assert.Equal(h.Description, got.h.Description) + assert.ElementsMatch(h.IpAddresses, got.h.IpAddresses) + assert.ElementsMatch(h.DnsNames, got.h.DnsNames) + assert.ElementsMatch(h.SetIds, got.h.SetIds) + + assert.Equal(hi.dirtyHost, got.dirtyHost) + assert.Empty( + cmp.Diff( + hi.ipsToAdd, + got.ipsToAdd, + cmpopts.IgnoreUnexported(host.IpAddress{}, hoststore.IpAddress{}), + cmpopts.SortSlices(func(x, y interface{}) bool { + return x.(*host.IpAddress).Address < y.(*host.IpAddress).Address + }), + ), + ) + assert.Empty( + cmp.Diff( + hi.ipsToRemove, + got.ipsToRemove, + cmpopts.IgnoreUnexported(host.IpAddress{}, hoststore.IpAddress{}), + cmpopts.SortSlices(func(x, y interface{}) bool { + return x.(*host.IpAddress).Address < y.(*host.IpAddress).Address + }), + ), + ) + assert.Empty( + cmp.Diff( + hi.dnsNamesToAdd, + got.dnsNamesToAdd, + cmpopts.IgnoreUnexported(host.DnsName{}, hoststore.DnsName{}), + cmpopts.SortSlices(func(x, y interface{}) bool { + return x.(*host.DnsName).Name < y.(*host.DnsName).Name + }), + ), + ) + assert.Empty( + cmp.Diff( + hi.dnsNamesToRemove, + got.dnsNamesToRemove, + cmpopts.IgnoreUnexported(host.DnsName{}, hoststore.DnsName{}), + cmpopts.SortSlices(func(x, y interface{}) bool { + return x.(*host.DnsName).Name < y.(*host.DnsName).Name + }), + ), + ) + } + + // Run through the sets function + { + newHostMap[baseHostId].h.SetIds = tt.sets() + toAdd, toRemove, _ := getSetChanges(currentHostMap, newHostMap) + assert.Equal(tt.setsToAdd, toAdd) + assert.Equal(tt.setsToRemove, toRemove) + } + }) + } +} diff --git a/internal/host/plugin/testing.go b/internal/host/plugin/testing.go index 45d38c42e..c3c7e20c5 100644 --- a/internal/host/plugin/testing.go +++ b/internal/host/plugin/testing.go @@ -18,7 +18,19 @@ import ( "github.com/stretchr/testify/require" ) -// TestCatalog creates count number of plugin host catalogs to the provided DB +// TestCatalogs creates count number of static host catalogs to the provided DB +// with the provided scope id. If any errors are encountered during the creation of +// the host catalog, the test will fail. +func TestCatalogs(t *testing.T, conn *db.DB, scopeId, pluginId string, count int) []*HostCatalog { + t.Helper() + var cats []*HostCatalog + for i := 0; i < count; i++ { + cats = append(cats, TestCatalog(t, conn, scopeId, pluginId)) + } + return cats +} + +// TestCatalog creates a plugin host catalogs to the provided DB // with the provided scope id. If any errors are encountered during the creation of // the host catalog, the test will fail. func TestCatalog(t *testing.T, conn *db.DB, scopeId, pluginId string, opt ...Option) *HostCatalog { @@ -78,6 +90,10 @@ func TestExternalHosts(t *testing.T, catalog *HostCatalog, setIds []string, coun require := require.New(t) retRH := make([]*plgpb.ListHostsResponseHost, 0, count) retH := make([]*Host, 0, count) + if setIds == nil { + // Prevent panics + setIds = make([]string, 0) + } for i := 0; i < count; i++ { externalId, err := base62.Random(10) @@ -86,19 +102,25 @@ func TestExternalHosts(t *testing.T, catalog *HostCatalog, setIds []string, coun ipStr := testGetIpAddress(t) dnsName := testGetDnsName(t) - retRH = append(retRH, &plgpb.ListHostsResponseHost{ + rh := &plgpb.ListHostsResponseHost{ ExternalId: externalId, - SetIds: setIds, + Name: base62.MustRandom(10), + Description: base62.MustRandom(10), + SetIds: setIds[0 : i+1], IpAddresses: []string{ipStr}, DnsNames: []string{dnsName}, - }) + } + retRH = append(retRH, rh) publicId, err := newHostId(context.Background(), catalog.PublicId, externalId) require.NoError(err) retH = append(retH, &Host{ PluginId: catalog.PluginId, + SetIds: setIds[0 : i+1], Host: &store.Host{ + Name: rh.Name, + Description: rh.Description, CatalogId: catalog.PublicId, PublicId: publicId, ExternalId: externalId, diff --git a/internal/proto/local/plugin/v1/host_plugin_service.proto b/internal/proto/local/plugin/v1/host_plugin_service.proto index 4de9aaf58..9b7b51c89 100644 --- a/internal/proto/local/plugin/v1/host_plugin_service.proto +++ b/internal/proto/local/plugin/v1/host_plugin_service.proto @@ -149,15 +149,21 @@ message ListHostsResponseHost { // useful, ie: a compute instance ID. string external_id = 10; + // If supplied, will be set as the managed name of the host + string name = 20; + + // If supplied, will be set as the managed description of the host + string description = 30; + // Required. Any and all known IP addresses for the host. - repeated string ip_addresses = 20; + repeated string ip_addresses = 40; // Required. Any and all known DNS names for the host. - repeated string dns_names = 30; + repeated string dns_names = 50; // Required. The host set IDs that match this host, out of the host sets // sent in the request. - repeated string set_ids = 40; + repeated string set_ids = 60; // Optional. Provider-specific metadata that is applicable to this // host. Example: host descriptions, tags, alternate network diff --git a/sdk/pbs/plugin/host_plugin_service.pb.go b/sdk/pbs/plugin/host_plugin_service.pb.go index 459450ee9..1ea2a7695 100644 --- a/sdk/pbs/plugin/host_plugin_service.pb.go +++ b/sdk/pbs/plugin/host_plugin_service.pb.go @@ -785,13 +785,17 @@ type ListHostsResponseHost struct { // included in audit logs. It should be set to something unique and // useful, ie: a compute instance ID. ExternalId string `protobuf:"bytes,10,opt,name=external_id,json=externalId,proto3" json:"external_id,omitempty"` + // If supplied, will be set as the managed name of the host + Name string `protobuf:"bytes,20,opt,name=name,proto3" json:"name,omitempty"` + // If supplied, will be set as the managed description of the host + Description string `protobuf:"bytes,30,opt,name=description,proto3" json:"description,omitempty"` // Required. Any and all known IP addresses for the host. - IpAddresses []string `protobuf:"bytes,20,rep,name=ip_addresses,json=ipAddresses,proto3" json:"ip_addresses,omitempty"` + IpAddresses []string `protobuf:"bytes,40,rep,name=ip_addresses,json=ipAddresses,proto3" json:"ip_addresses,omitempty"` // Required. Any and all known DNS names for the host. - DnsNames []string `protobuf:"bytes,30,rep,name=dns_names,json=dnsNames,proto3" json:"dns_names,omitempty"` + DnsNames []string `protobuf:"bytes,50,rep,name=dns_names,json=dnsNames,proto3" json:"dns_names,omitempty"` // Required. The host set IDs that match this host, out of the host sets // sent in the request. - SetIds []string `protobuf:"bytes,40,rep,name=set_ids,json=setIds,proto3" json:"set_ids,omitempty"` + SetIds []string `protobuf:"bytes,60,rep,name=set_ids,json=setIds,proto3" json:"set_ids,omitempty"` // Optional. Provider-specific metadata that is applicable to this // host. Example: host descriptions, tags, alternate network // addresses, etc. @@ -837,6 +841,20 @@ func (x *ListHostsResponseHost) GetExternalId() string { return "" } +func (x *ListHostsResponseHost) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ListHostsResponseHost) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + func (x *ListHostsResponseHost) GetIpAddresses() []string { if x != nil { return x.IpAddresses @@ -1058,66 +1076,70 @@ var file_plugin_v1_host_plugin_service_proto_rawDesc = []byte{ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x05, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x22, 0xca, - 0x01, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6e, 0x73, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x52, 0x05, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x22, 0x80, + 0x02, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x6f, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, - 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x70, 0x5f, - 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x14, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x0b, 0x69, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, - 0x64, 0x6e, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x1e, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x08, 0x64, 0x6e, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x65, 0x74, - 0x5f, 0x69, 0x64, 0x73, 0x18, 0x28, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x74, 0x49, - 0x64, 0x73, 0x12, 0x37, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, - 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x22, 0x49, 0x0a, 0x14, 0x48, - 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, - 0x74, 0x65, 0x64, 0x12, 0x31, 0x0a, 0x07, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, 0x64, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x07, 0x73, - 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x32, 0xd3, 0x04, 0x0a, 0x11, 0x48, 0x6f, 0x73, 0x74, 0x50, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x58, 0x0a, 0x0f, - 0x4f, 0x6e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, - 0x21, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, + 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x1e, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x21, 0x0a, 0x0c, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, + 0x28, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x69, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x6e, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, + 0x32, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x64, 0x6e, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, + 0x17, 0x0a, 0x07, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x3c, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x06, 0x73, 0x65, 0x74, 0x49, 0x64, 0x73, 0x12, 0x37, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, + 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x73, 0x22, 0x49, 0x0a, 0x14, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, + 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x64, 0x12, 0x31, 0x0a, 0x07, 0x73, 0x65, 0x63, + 0x72, 0x65, 0x74, 0x73, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, + 0x75, 0x63, 0x74, 0x52, 0x07, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x32, 0xd3, 0x04, 0x0a, + 0x11, 0x48, 0x6f, 0x73, 0x74, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x4f, 0x6e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x61, + 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x21, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, + 0x31, 0x2e, 0x4f, 0x6e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, + 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x61, 0x74, + 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0f, + 0x4f, 0x6e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, + 0x21, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x55, 0x70, + 0x64, 0x61, 0x74, 0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4f, - 0x6e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x4f, 0x6e, 0x55, 0x70, 0x64, 0x61, + 0x6e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x4f, 0x6e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x12, 0x21, 0x2e, 0x70, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x43, 0x61, + 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x58, 0x0a, 0x0f, 0x4f, 0x6e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x61, 0x74, 0x61, - 0x6c, 0x6f, 0x67, 0x12, 0x21, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, - 0x4f, 0x6e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, - 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, - 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x0b, 0x4f, 0x6e, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x74, 0x12, 0x1d, 0x2e, 0x70, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, - 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x74, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x0b, 0x4f, 0x6e, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x74, 0x12, 0x1d, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, - 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, - 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x0b, 0x4f, 0x6e, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x53, 0x65, 0x74, 0x12, 0x1d, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, - 0x31, 0x2e, 0x4f, 0x6e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, 0x31, - 0x2e, 0x4f, 0x6e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, - 0x73, 0x12, 0x1b, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, - 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x48, - 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x35, 0x5a, 0x33, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, - 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, - 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x3b, 0x70, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x12, 0x4c, 0x0a, 0x0b, 0x4f, 0x6e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x74, 0x12, + 0x1d, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, + 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, + 0x0a, 0x0b, 0x4f, 0x6e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x74, 0x12, 0x1d, 0x2e, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x55, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x0b, + 0x4f, 0x6e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x65, 0x74, 0x12, 0x1d, 0x2e, 0x70, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, + 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x09, 0x4c, 0x69, + 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x1b, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x76, 0x31, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x6f, 0x73, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, + 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x3b, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var (