Add repository and functions around plugin host set membership (#1629)

Co-authored-by: Todd <T.Alan.Knight@gmail.com>
This commit is contained in:
Jeff MitchellandTodd authored and GitHub committed 2021-10-28 10:09:43 -04:00
1 parent b1a72bd445
commit b64761f38f
23 files changed
+1392 -286

No files matched your search

+2 -1
View File
@@ -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
+2 -1
View File
@@ -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=
+11
View File
@@ -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
+10 -1
View File
@@ -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{
+9 -6
View File
@@ -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())
@@ -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;
+4
View File
@@ -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
+5
View File
@@ -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) {
+4
View File
@@ -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,
},
}
+34 -5
View File
@@ -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 {
+60
View File
@@ -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
}
@@ -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())
})
}
}
+4 -5
View File
@@ -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
}
}
+9
View File
@@ -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
}
}
+6
View File
@@ -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)
})
}
+273 -160
View File
@@ -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
}
+1 -1
View File
@@ -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)
}
+29 -4
View File
@@ -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)
}
})
}
}
@@ -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
}
@@ -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)
}
})
}
}
+26 -4
View File
@@ -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,
@@ -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
+79 -57
View File
@@ -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 (