diff --git a/internal/db/schema/migrations/postgres/17/04_plugin_host.up.sql b/internal/db/schema/migrations/postgres/17/04_plugin_host.up.sql index d3b80e48c..1ca7386f2 100644 --- a/internal/db/schema/migrations/postgres/17/04_plugin_host.up.sql +++ b/internal/db/schema/migrations/postgres/17/04_plugin_host.up.sql @@ -154,6 +154,66 @@ begin; create trigger delete_host_set_subtype after delete on host_plugin_set for each row execute procedure delete_host_set_subtype(); + + -- TODO: Rebuild this table to appropriately cache or sync data from the + -- plugin'ed service. + -- host_plugin_host captures plugin based host data. This is only written to + -- from the controller and is not mutable directly by actions from the end + -- user. + create table host_plugin_host ( + public_id wt_public_id primary key, + catalog_id wt_public_id not null + constraint host_plugin_catalog_fkey + references host_plugin_catalog (public_id) + on delete cascade + on update cascade, + name wt_name, + description text, + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + + -- TODO: Break out the address to match the domain model where a host + -- can have multiple addresses and relies on the set to select the + -- prefered one. + address text not null + constraint address_must_be_more_than_2_characters + check(length(trim(address)) > 2) + constraint address_must_be_less_than_256_characters + check(length(trim(address)) < 256), + + constraint host_fkey + foreign key (catalog_id, public_id) + 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_public_id_uq + unique(catalog_id, public_id) + ); + + create trigger update_version_column after update on host_plugin_host + for each row execute procedure update_version_column(); + + create trigger update_time_column before update on host_plugin_host + for each row execute procedure update_time_column(); + + create trigger default_create_time_column before insert on host_plugin_host + for each row execute procedure default_create_time(); + + create trigger immutable_columns before update on host_plugin_host + for each row execute procedure immutable_columns('public_id', 'catalog_id','create_time'); + + create trigger insert_host_subtype before insert on host_plugin_host + for each row execute procedure insert_host_subtype(); + + create trigger delete_host_subtype after delete on host_plugin_host + for each row execute procedure delete_host_subtype(); + + insert into oplog_ticket (name, version) values ('host_plugin_catalog', 1), diff --git a/internal/db/schema/migrations/postgres/17/06_wh_session_dimensions.up.sql b/internal/db/schema/migrations/postgres/17/06_wh_session_dimensions.up.sql new file mode 100644 index 000000000..6c8b9daac --- /dev/null +++ b/internal/db/schema/migrations/postgres/17/06_wh_session_dimensions.up.sql @@ -0,0 +1,85 @@ +begin; + +-- replaces view from 15/02_wh_rename_host_dimension_org.up.sql + +-- Updates whx_host_dimension_source to support plugin based host and related +-- resources. While this will return rows where there are host_sets where hosts +-- are not in the host sets, this view is only used when looking up a row by +-- host, set, and target so the records where hosts and host sets aren't +-- related beyond the shared catalog will be ignored. +-- TODO: Support a host having multiple addresses. +drop view whx_host_dimension_source; +create view whx_host_dimension_source as +select -- id is the first column in the target view + h.public_id as host_id, + case when sh.public_id is not null then 'static host' + when ph.public_id is not null then 'plugin host' + else 'Unknown' end as host_type, + case when sh.public_id is not null then coalesce(sh.name, 'None') + when ph.public_id is not null then coalesce(ph.name, 'None') + else 'Unknown' end as host_name, + case when sh.public_id is not null then coalesce(sh.description, 'None') + when ph.public_id is not null then coalesce(ph.description, 'None') + else 'Unknown' end as host_description, + + -- TODO: update this to be endpoint address... + coalesce(sh.address, 'Unknown') as host_address, + + hs.public_id as host_set_id, + case when shs.public_id is not null then 'static host set' + when phs.public_id is not null then 'plugin host set' + else 'Unknown' end as host_set_type, + case + when shs.public_id is not null then coalesce(shs.name, 'None') + when phs.public_id is not null then coalesce(phs.name, 'None') + else 'None' + end as host_set_name, + case + when shs.public_id is not null then coalesce(shs.description, 'None') + when phs.public_id is not null then coalesce(phs.description, 'None') + else 'None' + end as host_set_description, + hc.public_id as host_catalog_id, + case when shc.public_id is not null then 'static host catalog' + when phc.public_id is not null then 'plugin host catalog' + else 'Unknown' end as host_catalog_type, + case + when shc.public_id is not null then coalesce(shc.name, 'None') + when phc.public_id is not null then coalesce(phc.name, 'None') + else 'None' + end as host_catalog_name, + case + when shc.public_id is not null then coalesce(shc.description, 'None') + when phc.public_id is not null then coalesce(phc.description, 'None') + else 'None' + end as host_catalog_description, + t.public_id as target_id, + 'tcp target' as target_type, + coalesce(t.name, 'None') as target_name, + coalesce(t.description, 'None') as target_description, + coalesce(t.default_port, 0) as target_default_port_number, + t.session_max_seconds as target_session_max_seconds, + t.session_connection_limit as target_session_connection_limit, + p.public_id as project_id, + coalesce(p.name, 'None') as project_name, + coalesce(p.description, 'None') as project_description, + o.public_id as organization_id, + coalesce(o.name, 'None') as organization_name, + coalesce(o.description, 'None') as organization_description + from host as h + join host_catalog as hc on h.catalog_id = hc.public_id + join host_set as hs on h.catalog_id = hs.catalog_id + join target_host_set as ts on hs.public_id = ts.host_set_id + join target_tcp as t on ts.target_id = t.public_id + join iam_scope as p on t.scope_id = p.public_id and p.type = 'project' + join iam_scope as o on p.parent_id = o.public_id and o.type = 'org' + + left join static_host as sh on sh.public_id = h.public_id + left join host_plugin_host as ph on ph.public_id = h.public_id + left join static_host_catalog as shc on shc.public_id = hc.public_id + left join host_plugin_catalog as phc on phc.public_id = hc.public_id + left join static_host_set as shs on shs.public_id = hs.public_id + left join host_plugin_set as phs on phs.public_id = hs.public_id +; + +commit; diff --git a/internal/db/schema/postgres_migration.gen.go b/internal/db/schema/postgres_migration.gen.go index 5504c5ef4..f8f42984c 100644 --- a/internal/db/schema/postgres_migration.gen.go +++ b/internal/db/schema/postgres_migration.gen.go @@ -4,7 +4,7 @@ package schema func init() { migrationStates["postgres"] = migrationState{ - binarySchemaVersion: 17005, + binarySchemaVersion: 17006, upMigrations: map[int][]byte{ 1: []byte(` create domain wt_public_id as text @@ -7828,6 +7828,66 @@ alter table wh_session_connection_accumulating_fact create trigger delete_host_set_subtype after delete on host_plugin_set for each row execute procedure delete_host_set_subtype(); + + -- TODO: Rebuild this table to appropriately cache or sync data from the + -- plugin'ed service. + -- host_plugin_host captures plugin based host data. This is only written to + -- from the controller and is not mutable directly by actions from the end + -- user. + create table host_plugin_host ( + public_id wt_public_id primary key, + catalog_id wt_public_id not null + constraint host_plugin_catalog_fkey + references host_plugin_catalog (public_id) + on delete cascade + on update cascade, + name wt_name, + description text, + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + + -- TODO: Break out the address to match the domain model where a host + -- can have multiple addresses and relies on the set to select the + -- prefered one. + address text not null + constraint address_must_be_more_than_2_characters + check(length(trim(address)) > 2) + constraint address_must_be_less_than_256_characters + check(length(trim(address)) < 256), + + constraint host_fkey + foreign key (catalog_id, public_id) + 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_public_id_uq + unique(catalog_id, public_id) + ); + + create trigger update_version_column after update on host_plugin_host + for each row execute procedure update_version_column(); + + create trigger update_time_column before update on host_plugin_host + for each row execute procedure update_time_column(); + + create trigger default_create_time_column before insert on host_plugin_host + for each row execute procedure default_create_time(); + + create trigger immutable_columns before update on host_plugin_host + for each row execute procedure immutable_columns('public_id', 'catalog_id','create_time'); + + create trigger insert_host_subtype before insert on host_plugin_host + for each row execute procedure insert_host_subtype(); + + create trigger delete_host_subtype after delete on host_plugin_host + for each row execute procedure delete_host_subtype(); + + insert into oplog_ticket (name, version) values ('host_plugin_catalog', 1), @@ -7906,6 +7966,89 @@ from group by hs.public_id, hc.plugin_id; comment on view host_plugin_host_set_with_value_obj is 'host plugin host set with its associated value objects'; +`), + 17006: []byte(` +-- replaces view from 15/02_wh_rename_host_dimension_org.up.sql + +-- Updates whx_host_dimension_source to support plugin based host and related +-- resources. While this will return rows where there are host_sets where hosts +-- are not in the host sets, this view is only used when looking up a row by +-- host, set, and target so the records where hosts and host sets aren't +-- related beyond the shared catalog will be ignored. +-- TODO: Support a host having multiple addresses. +drop view whx_host_dimension_source; +create view whx_host_dimension_source as +select -- id is the first column in the target view + h.public_id as host_id, + case when sh.public_id is not null then 'static host' + when ph.public_id is not null then 'plugin host' + else 'Unknown' end as host_type, + case when sh.public_id is not null then coalesce(sh.name, 'None') + when ph.public_id is not null then coalesce(ph.name, 'None') + else 'Unknown' end as host_name, + case when sh.public_id is not null then coalesce(sh.description, 'None') + when ph.public_id is not null then coalesce(ph.description, 'None') + else 'Unknown' end as host_description, + + -- TODO: update this to be endpoint address... + coalesce(sh.address, 'Unknown') as host_address, + + hs.public_id as host_set_id, + case when shs.public_id is not null then 'static host set' + when phs.public_id is not null then 'plugin host set' + else 'Unknown' end as host_set_type, + case + when shs.public_id is not null then coalesce(shs.name, 'None') + when phs.public_id is not null then coalesce(phs.name, 'None') + else 'None' + end as host_set_name, + case + when shs.public_id is not null then coalesce(shs.description, 'None') + when phs.public_id is not null then coalesce(phs.description, 'None') + else 'None' + end as host_set_description, + hc.public_id as host_catalog_id, + case when shc.public_id is not null then 'static host catalog' + when phc.public_id is not null then 'plugin host catalog' + else 'Unknown' end as host_catalog_type, + case + when shc.public_id is not null then coalesce(shc.name, 'None') + when phc.public_id is not null then coalesce(phc.name, 'None') + else 'None' + end as host_catalog_name, + case + when shc.public_id is not null then coalesce(shc.description, 'None') + when phc.public_id is not null then coalesce(phc.description, 'None') + else 'None' + end as host_catalog_description, + t.public_id as target_id, + 'tcp target' as target_type, + coalesce(t.name, 'None') as target_name, + coalesce(t.description, 'None') as target_description, + coalesce(t.default_port, 0) as target_default_port_number, + t.session_max_seconds as target_session_max_seconds, + t.session_connection_limit as target_session_connection_limit, + p.public_id as project_id, + coalesce(p.name, 'None') as project_name, + coalesce(p.description, 'None') as project_description, + o.public_id as organization_id, + coalesce(o.name, 'None') as organization_name, + coalesce(o.description, 'None') as organization_description + from host as h + join host_catalog as hc on h.catalog_id = hc.public_id + join host_set as hs on h.catalog_id = hs.catalog_id + join target_host_set as ts on hs.public_id = ts.host_set_id + join target_tcp as t on ts.target_id = t.public_id + join iam_scope as p on t.scope_id = p.public_id and p.type = 'project' + join iam_scope as o on p.parent_id = o.public_id and o.type = 'org' + + left join static_host as sh on sh.public_id = h.public_id + left join host_plugin_host as ph on ph.public_id = h.public_id + left join static_host_catalog as shc on shc.public_id = hc.public_id + left join host_plugin_catalog as phc on phc.public_id = hc.public_id + left join static_host_set as shs on shs.public_id = hs.public_id + left join host_plugin_set as phs on phs.public_id = hs.public_id +; `), 2001: []byte(` -- log_migration entries represent logs generated during migrations diff --git a/internal/host/endpoint.go b/internal/host/endpoint.go new file mode 100644 index 000000000..50f5792fe --- /dev/null +++ b/internal/host/endpoint.go @@ -0,0 +1,9 @@ +package host + +// Endpoint is a struct which identifies an address provided by a host and +// selected as the priority address by the specified host set. +type Endpoint struct { + HostId string + SetId string + Address string +} diff --git a/internal/host/host.go b/internal/host/host.go new file mode 100644 index 000000000..02bc57963 --- /dev/null +++ b/internal/host/host.go @@ -0,0 +1,23 @@ +package host + +import "github.com/hashicorp/boundary/internal/boundary" + +// Catalog contains the common methods across all the different types of host +// catalogs. +type Catalog interface { + boundary.Resource + GetScopeId() string +} + +// Set contains the common methods across all the different types of host sets. +type Set interface { + boundary.Resource + GetCatalogId() string +} + +// Host contains the common methods across all the different types of hosts. +type Host interface { + boundary.Resource + GetCatalogId() string + GetAddress() string +} diff --git a/internal/host/plugin/host.go b/internal/host/plugin/host.go new file mode 100644 index 000000000..96b431dfb --- /dev/null +++ b/internal/host/plugin/host.go @@ -0,0 +1,76 @@ +package plugin + +import ( + "context" + + "github.com/hashicorp/boundary/internal/host/plugin/store" + "github.com/hashicorp/boundary/internal/oplog" + "google.golang.org/protobuf/proto" +) + +// A Host is a temporarily cached plugin based resource. The source of truth +// of the data contained here is provided by the service backing the plugin for +// this host. The plugin for this host can be found by looking at the plugin +// field for this host's host catalog. +type Host struct { + *store.Host + tableName string `gorm:"-"` +} + +// newHost creates a new in memory Host assigned to catalogId with an address. +// Name and description are the only valid options. All other options are +// ignored. +func newHost(ctx context.Context, catalogId, address string, opt ...Option) *Host { + opts := getOpts(opt...) + + h := &Host{ + Host: &store.Host{ + CatalogId: catalogId, + Name: opts.withName, + Description: opts.withDescription, + Address: address, + }, + } + + return h +} + +// TableName returns the table name for the host set. +func (s *Host) TableName() string { + if s.tableName != "" { + return s.tableName + } + return "host_plugin_host" +} + +// SetTableName sets the table name. If the caller attempts to +// set the name to "" the name will be reset to the default name. +func (s *Host) SetTableName(n string) { + s.tableName = n +} + +func allocHost() *Host { + return &Host{ + Host: &store.Host{}, + } +} + +func (s *Host) clone() *Host { + cp := proto.Clone(s.Host) + hs := &Host{ + Host: cp.(*store.Host), + } + return hs +} + +func (s *Host) oplog(op oplog.OpType) oplog.Metadata { + metadata := oplog.Metadata{ + "resource-public-id": []string{s.PublicId}, + "resource-type": []string{"plugin-host"}, + "op-type": []string{op.String()}, + } + if s.CatalogId != "" { + metadata["catalog-id"] = []string{s.CatalogId} + } + return metadata +} diff --git a/internal/host/plugin/host_test.go b/internal/host/plugin/host_test.go new file mode 100644 index 000000000..ee7bd501c --- /dev/null +++ b/internal/host/plugin/host_test.go @@ -0,0 +1,202 @@ +package plugin + +import ( + "context" + "testing" + + "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/plugin/host" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHost_Create(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + plg := host.TestPlugin(t, conn, "test") + cat := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) + cat2 := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) + + type args struct { + catalogId string + address string + opts []Option + } + + tests := []struct { + name string + args args + want *Host + wantErr bool + }{ + { + name: "blank-catalogId", + args: args{ + catalogId: "", + address: "foo.bar.com", + }, + want: &Host{Host: &store.Host{ + Address: "foo.bar.com", + }}, + wantErr: true, + }, + { + name: "blank-address", + args: args{ + catalogId: cat.GetPublicId(), + }, + want: &Host{Host: &store.Host{ + CatalogId: cat.GetPublicId(), + }}, + wantErr: true, + }, + { + name: "valid-no-options", + args: args{ + catalogId: cat.GetPublicId(), + address: "foo.bar.com", + }, + want: &Host{ + Host: &store.Host{ + CatalogId: cat.GetPublicId(), + Address: "foo.bar.com", + }, + }, + }, + { + name: "valid-with-name", + args: args{ + catalogId: cat.GetPublicId(), + address: "foo.bar.com", + opts: []Option{ + WithName("test-name"), + }, + }, + want: &Host{ + Host: &store.Host{ + CatalogId: cat.GetPublicId(), + Address: "foo.bar.com", + Name: "test-name", + }, + }, + }, + { + name: "duplicate-name", + args: args{ + catalogId: cat.GetPublicId(), + address: "foo.bar.com", + opts: []Option{ + WithName("test-name"), + }, + }, + want: &Host{ + Host: &store.Host{ + CatalogId: cat.GetPublicId(), + Address: "foo.bar.com", + Name: "test-name", + }, + }, + wantErr: true, + }, + { + name: "valid-duplicate-name-different-catalog", + args: args{ + catalogId: cat2.GetPublicId(), + address: "foo.bar.com", + opts: []Option{ + WithName("test-name"), + }, + }, + want: &Host{ + Host: &store.Host{ + CatalogId: cat2.GetPublicId(), + Address: "foo.bar.com", + Name: "test-name", + }, + }, + }, + { + name: "valid-with-description", + args: args{ + catalogId: cat.GetPublicId(), + address: "foo.bar.com", + opts: []Option{ + WithDescription("test-description"), + }, + }, + want: &Host{ + Host: &store.Host{ + CatalogId: cat.GetPublicId(), + Address: "foo.bar.com", + Description: "test-description", + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + got := newHost(ctx, tt.args.catalogId, tt.args.address, tt.args.opts...) + require.NotNil(t, got) + assert.Emptyf(t, got.PublicId, "PublicId set") + assert.Equal(t, tt.want, got) + + id, err := newHostId(ctx, tt.name, tt.name) + assert.NoError(t, err) + + tt.want.PublicId = id + got.PublicId = id + + w := db.New(conn) + err = w.Create(context.Background(), got) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestHost_SetTableName(t *testing.T) { + defaultTableName := "host_plugin_host" + 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 := &Host{ + Host: &store.Host{}, + } + require.Equal(defaultTableName, def.TableName()) + s := &Host{ + Host: &store.Host{}, + tableName: tt.initialName, + } + s.SetTableName(tt.setNameTo) + assert.Equal(tt.want, s.TableName()) + }) + } +} diff --git a/internal/host/plugin/ids.go b/internal/host/plugin/ids.go index fa384bded..0516abead 100644 --- a/internal/host/plugin/ids.go +++ b/internal/host/plugin/ids.go @@ -17,9 +17,9 @@ func init() { // PublicId prefixes for the resources in the plugin package. const ( - // TODO: Pull these out of being constants and have them derivable at run time. HostCatalogPrefix = "hc" HostSetPrefix = "hs" + HostPrefix = "h" Subtype = subtypes.Subtype("plugin") ) @@ -39,3 +39,18 @@ func newHostSetId(ctx context.Context) (string, error) { } return id, nil } + +func newHostId(ctx context.Context, catalogId, externalId string) (string, error) { + const op = "plugin.newHostId" + if catalogId == "" { + return "", errors.New(ctx, errors.InvalidParameter, op, "missing catalog id") + } + if externalId == "" { + return "", errors.New(ctx, errors.InvalidParameter, op, "missing external id") + } + id, err := db.NewPublicId(HostPrefix, db.WithPrngValues([]string{catalogId, externalId})) + if err != nil { + return "", errors.Wrap(ctx, err, op) + } + return id, nil +} diff --git a/internal/host/plugin/repository_host_catalog.go b/internal/host/plugin/repository_host_catalog.go index 89cd3dbaf..416815206 100644 --- a/internal/host/plugin/repository_host_catalog.go +++ b/internal/host/plugin/repository_host_catalog.go @@ -151,10 +151,11 @@ func (r *Repository) LookupCatalog(ctx context.Context, id string, _ ...Option) return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "no public id") } c, err := r.getCatalog(ctx, id) + if errors.IsNotFoundError(err) { + return nil, nil, nil + } + if err != nil { - if errors.IsNotFoundError(err) { - return nil, nil, nil - } return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for: %s", id))) } plg, err := r.getPlugin(ctx, c.GetPluginId()) @@ -206,16 +207,6 @@ func (r *Repository) getCatalog(ctx context.Context, id string) (*HostCatalog, e return c, nil } -func (r *Repository) getPlugin(ctx context.Context, id string) (*hostplugin.Plugin, error) { - const op = "plugin.(Repository).getPlugin" - c := hostplugin.NewPlugin() - c.PublicId = id - if err := r.reader.LookupByPublicId(ctx, c); err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for: %s", id))) - } - return c, nil -} - // getPersistedDataForCatalog returns the persisted data for a catalog if // present. c must have a valid Public Id and Scope Id set. // TODO: consider merging the functions for getting catalog and persisted data into a view. @@ -252,6 +243,19 @@ func (r *Repository) getPersistedDataForCatalog(ctx context.Context, c *HostCata return &plgpb.HostCatalogPersisted{Secrets: secrets}, nil } +func (r *Repository) getPlugin(ctx context.Context, plgId string) (*hostplugin.Plugin, error) { + const op = "plugin.(Repository).getPlugin" + if plgId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no plugin id") + } + plg := hostplugin.NewPlugin() + plg.PublicId = plgId + if err := r.reader.LookupByPublicId(ctx, plg); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get host plugin with id %q", plgId))) + } + return plg, nil +} + // toPluginCatalog returns a host catalog, with it's secret if available, in the format expected // by the host plugin system. func toPluginCatalog(ctx context.Context, in *HostCatalog) (*pb.HostCatalog, error) { diff --git a/internal/host/plugin/repository_host_set.go b/internal/host/plugin/repository_host_set.go index 72e58ecd4..74c798a72 100644 --- a/internal/host/plugin/repository_host_set.go +++ b/internal/host/plugin/repository_host_set.go @@ -12,8 +12,10 @@ import ( "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/host" "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/libs/endpoint" "github.com/hashicorp/boundary/internal/oplog" hostplugin "github.com/hashicorp/boundary/internal/plugin/host" + hcpb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets" plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" "google.golang.org/grpc/codes" @@ -192,9 +194,6 @@ func (r *Repository) ListSets(ctx context.Context, catalogId string, opt ...host func (r *Repository) getSets(ctx context.Context, publicId string, catalogId string, opt ...host.Option) ([]*HostSet, *hostplugin.Plugin, error) { const op = "plugin.(Repository).getSets" - const aggregateDelimiter = "|" - const priorityDelimiter = "=" - if publicId == "" && catalogId == "" { return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing search criteria: both host set id and catalog id are empty") } @@ -245,55 +244,9 @@ func (r *Repository) getSets(ctx context.Context, publicId string, catalogId str sets := make([]*HostSet, 0, len(aggHostSets)) for _, agg := range aggHostSets { - hs := allocHostSet() - hs.PublicId = agg.PublicId - hs.CatalogId = agg.CatalogId - hs.Name = agg.Name - hs.Description = agg.Description - hs.CreateTime = agg.CreateTime - hs.UpdateTime = agg.UpdateTime - hs.Version = agg.Version - hs.Attributes = agg.Attributes - if agg.PreferredEndpoints != "" { - eps := strings.Split(agg.PreferredEndpoints, aggregateDelimiter) - if len(eps) > 0 { - // We want to protect against someone messing with the DB - // and not panic, so we do a bit of a dance here - var sortErr error - sort.Slice(eps, func(i, j int) bool { - epi := strings.Split(eps[i], priorityDelimiter) - if len(epi) != 2 { - sortErr = errors.New(ctx, errors.NotSpecificIntegrity, op, fmt.Sprintf("preferred endpoint %s had unexpected fields", eps[i])) - return false - } - epj := strings.Split(eps[j], priorityDelimiter) - if len(epj) != 2 { - sortErr = errors.New(ctx, errors.NotSpecificIntegrity, op, fmt.Sprintf("preferred endpoint %s had unexpected fields", eps[j])) - return false - } - indexi, err := strconv.Atoi(epi[0]) - if err != nil { - sortErr = errors.Wrap(ctx, err, op) - return false - } - indexj, err := strconv.Atoi(epj[0]) - if err != nil { - sortErr = errors.Wrap(ctx, err, op) - return false - } - return indexi < indexj - }) - if sortErr != nil { - return nil, nil, sortErr - } - for i, ep := range eps { - // At this point they're in the correct order, but we still - // have to strip off the priority - eps[i] = strings.Split(ep, priorityDelimiter)[1] - } - - hs.PreferredEndpoints = eps - } + hs, err := agg.toHostSet(ctx) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op) } sets = append(sets, hs) } @@ -323,6 +276,62 @@ type hostSetAgg struct { PreferredEndpoints string } +func (agg *hostSetAgg) toHostSet(ctx context.Context) (*HostSet, error) { + const op = "plugin.(hostSetAgg).toHostSet" + const aggregateDelimiter = "|" + const priorityDelimiter = "=" + hs := allocHostSet() + hs.PublicId = agg.PublicId + hs.CatalogId = agg.CatalogId + hs.Name = agg.Name + hs.Description = agg.Description + hs.CreateTime = agg.CreateTime + hs.UpdateTime = agg.UpdateTime + hs.Version = agg.Version + hs.Attributes = agg.Attributes + if agg.PreferredEndpoints != "" { + eps := strings.Split(agg.PreferredEndpoints, aggregateDelimiter) + if len(eps) > 0 { + // We want to protect against someone messing with the DB + // and not panic, so we do a bit of a dance here + var sortErr error + sort.Slice(eps, func(i, j int) bool { + epi := strings.Split(eps[i], priorityDelimiter) + if len(epi) != 2 { + sortErr = errors.New(ctx, errors.NotSpecificIntegrity, op, fmt.Sprintf("preferred endpoint %s had unexpected fields", eps[i])) + return false + } + epj := strings.Split(eps[j], priorityDelimiter) + if len(epj) != 2 { + sortErr = errors.New(ctx, errors.NotSpecificIntegrity, op, fmt.Sprintf("preferred endpoint %s had unexpected fields", eps[j])) + return false + } + indexi, err := strconv.Atoi(epi[0]) + if err != nil { + sortErr = errors.Wrap(ctx, err, op) + return false + } + indexj, err := strconv.Atoi(epj[0]) + if err != nil { + sortErr = errors.Wrap(ctx, err, op) + return false + } + return indexi < indexj + }) + if sortErr != nil { + return nil, sortErr + } + for i, ep := range eps { + // At this point they're in the correct order, but we still + // have to strip off the priority + eps[i] = strings.Split(ep, priorityDelimiter)[1] + } + hs.PreferredEndpoints = eps + } + } + return hs, nil +} + // TableName returns the table name for gorm func (agg *hostSetAgg) TableName() string { return "host_plugin_host_set_with_value_obj" } @@ -344,3 +353,183 @@ func toPluginSet(ctx context.Context, in *HostSet) (*pb.HostSet, error) { } return hs, nil } + +// Endpoints provides all the endpoints available for a given set id. +// An error is returned if the set, related catalog, or related plugin are +// unable to be retrieved. If a host does not contain an addressible endpoint +// it is not included in the resulting slice of endpoints. +func (r *Repository) Endpoints(ctx context.Context, setIds []string) ([]*host.Endpoint, error) { + const op = "plugin.(Repository).Endpoints" + if len(setIds) == 0 { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no set ids") + } + var setAggs []*hostSetAgg + if err := r.reader.SearchWhere(ctx, &setAggs, "public_id in (?)", []interface{}{setIds}); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("can't retrieve sets %v", setIds))) + } + if len(setAggs) == 0 { + return nil, nil + } + + type setInfo struct { + preferredEndpoint endpoint.Option + plgSet *pb.HostSet + } + + type catalogInfo struct{ + publicId string + plg plgpb.HostPluginServiceServer + setInfos map[string]*setInfo + plgCat *hcpb.HostCatalog + persisted *plgpb.HostCatalogPersisted + } + + catalogInfos := make(map[string]*catalogInfo) + for _, ag := range setAggs { + ci, ok := catalogInfos[ag.CatalogId] + if !ok { + ci = &catalogInfo{ + publicId: ag.CatalogId, + setInfos: make(map[string]*setInfo), + } + } + ci.plg, ok = r.plugins[ag.PluginId] + if !ok { + return nil, errors.New(ctx, errors.Internal, op, fmt.Sprintf("expected plugin %q not available", ag.PluginId)) + } + + s, err := ag.toHostSet(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + si, ok := ci.setInfos[s.GetPublicId()] + if !ok { + si = &setInfo{} + } + si.preferredEndpoint = endpoint.WithPreferenceOrder(s.GetPreferredEndpoints()) + si.plgSet, err = toPluginSet(ctx, s) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("converting set %q to plugin set", s.GetPublicId()))) + } + + ci.setInfos[s.GetPublicId()] = si + catalogInfos[ag.CatalogId] = ci + } + + catIds := make([]string, 0, len(catalogInfos)) + for k := range catalogInfos { + catIds = append(catIds, k) + } + var cats []*HostCatalog + if err := r.reader.SearchWhere(ctx, &cats, "public_id in (?)", []interface{}{catIds}); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("can't retrieve catalogs %v", catIds))) + } + if len(cats) == 0 { + return nil, errors.New(ctx, errors.NotSpecificIntegrity, op, "no catalogs returned for retrieved sets") + } + for _, c := range cats { + ci, ok := catalogInfos[c.GetPublicId()] + if !ok { + return nil, errors.New(ctx, errors.NotSpecificIntegrity, op, "catalog returned when no set requested it") + } + plgCat, err := toPluginCatalog(ctx, c) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("storage to plugin catalog conversion")) + } + ci.plgCat = plgCat + + // TODO: Do these looksups from the DB in bulk instead of individually. + per, err := r.getPersistedDataForCatalog(ctx, c) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("persisted catalog lookup failed")) + } + ci.persisted = per + catalogInfos[c.GetPublicId()] = ci + } + + // For writing the hosts to the db so data warehouse doesn't complain + var hosts []interface{} + hostIds := map[string]bool{} + var es []*host.Endpoint + for _, ci := range catalogInfos { + + var sets []*pb.HostSet + for _, si := range ci.setInfos { + sets = append(sets, si.plgSet) + } + + resp, err := ci.plg.ListHosts(ctx, &plgpb.ListHostsRequest{ + Catalog: ci.plgCat, + Sets: sets, + //Persisted: ci.persisted, + }) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + for _, h := range resp.GetHosts() { + hostId, err := newHostId(ctx, ci.publicId, h.GetExternalId()) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + for _, sId := range h.GetSetIds() { + var opts []endpoint.Option + if len(h.GetIpAddresses()) > 0 { + opts = append(opts, endpoint.WithIpAddrs(h.GetIpAddresses())) + } + if len(h.GetDnsNames()) > 0 { + opts = append(opts, endpoint.WithIpAddrs(h.GetDnsNames())) + } + + si, ok := ci.setInfos[sId] + if !ok { + return nil, errors.New(ctx, errors.NotSpecificIntegrity, op, "host is reporting it's part of a set we didn't query for") + } + pref, err := endpoint.NewPreferencer(ctx, si.preferredEndpoint) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("getting preferencer for set %q", sId))) + } + addr, err := pref.Choose(ctx, opts...) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + if addr == "" { + continue + } + es = append(es, &host.Endpoint{ + HostId: hostId, + SetId: sId, + Address: addr, + }) + + if _, ok := hostIds[hostId]; !ok { + hostIds[hostId] = true + host := newHost(ctx, ci.publicId, addr) + host.PublicId = hostId + hosts = append(hosts, host) + } + } + } + } + + if len(hosts) > 0 { + _, err := r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + if _, err := r.writer.DeleteItems(ctx, hosts); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("couldn't delete existing hosts")) + } + if err := r.writer.CreateItems(ctx, hosts); err != nil { + return errors.Wrap(ctx, err, op, errors.WithMsg("can't persist hosts")) + } + return nil + }) + if err != nil { + return nil, err + } + } + return es, nil +} diff --git a/internal/host/plugin/repository_host_set_test.go b/internal/host/plugin/repository_host_set_test.go index 3ffb12142..d875ca999 100644 --- a/internal/host/plugin/repository_host_set_test.go +++ b/internal/host/plugin/repository_host_set_test.go @@ -2,6 +2,7 @@ package plugin import ( "context" + "sort" "strings" "testing" "time" @@ -365,6 +366,139 @@ func TestRepository_LookupSet(t *testing.T) { } } +func TestRepository_Endpoints(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) + iamRepo := iam.TestRepo(t, conn, wrapper) + _, prj := iam.TestScopes(t, iamRepo) + plg := hostplg.TestPlugin(t, conn, "endpoints") + + hostlessCatalog := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) + plgm := map[string]plgpb.HostPluginServiceServer{ + plg.GetPublicId(): &TestPluginServer{ + ListHostsFn: func(_ context.Context, req *plgpb.ListHostsRequest) (*plgpb.ListHostsResponse, error) { + if req.Catalog.GetId() == hostlessCatalog.GetPublicId() { + return &plgpb.ListHostsResponse{}, nil + } + var setIds []string + for _, set := range req.GetSets() { + setIds = append(setIds, set.GetId()) + } + return &plgpb.ListHostsResponse{Hosts: []*plgpb.ListHostsResponseHost{ + { + SetIds: setIds, + ExternalId: "test", + IpAddresses: []string{"10.0.0.5", "192.168.0.5"}, + DnsNames: nil, + }, + }}, nil + }, + }, + } + + catalog := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId()) + hostSet10 := TestSet(t, conn, kms, catalog, plgm, WithPreferredEndpoints([]string{"cidr:10.0.0.1/24"})) + hostSet192 := TestSet(t, conn, kms, catalog, plgm, WithPreferredEndpoints([]string{"cidr:192.168.0.1/24"})) + hostSet100 := TestSet(t, conn, kms, catalog, plgm, WithPreferredEndpoints([]string{"cidr:100.100.100.100/24"})) + hostlessSet := TestSet(t, conn, kms, hostlessCatalog, plgm) + + tests := []struct { + name string + setIds []string + want []*host.Endpoint + wantIsErr errors.Code + }{ + { + name: "with-no-set-id", + wantIsErr: errors.InvalidParameter, + }, + { + name: "with-set10", + setIds: []string{hostSet10.GetPublicId()}, + want: []*host.Endpoint{ + { + HostId: func() string { + s, err := newHostId(ctx, catalog.GetPublicId(), "test") + require.NoError(t, err) + return s + }(), + SetId: hostSet10.GetPublicId(), + Address: "10.0.0.5", + }, + }, + }, + { + name: "with-different-set", + setIds: []string{hostSet192.GetPublicId()}, + want: []*host.Endpoint{ + { + HostId: func() string { + s, err := newHostId(ctx, catalog.GetPublicId(), "test") + require.NoError(t, err) + return s + }(), + SetId: hostSet192.GetPublicId(), + Address: "192.168.0.5", + }, + }, + }, + { + name: "with-all-addresses-filtered-set", + setIds: []string{hostSet100.GetPublicId()}, + want: nil, + }, + { + name: "with-no-hosts-from-plugin", + setIds: []string{hostlessSet.GetPublicId()}, + want: nil, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kms, plgm) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.Endpoints(ctx, tt.setIds) + if tt.wantIsErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "want err: %q got: %q", tt.wantIsErr, err) + assert.Nil(got) + return + } + require.NoError(err) + if tt.want == nil { + return + } + + sort.Slice(tt.want, func(i, j int) bool { + return tt.want[i].HostId < tt.want[j].HostId + }) + sort.Slice(got, func(i, j int) bool { + return got[i].HostId < got[j].HostId + }) + assert.Empty(cmp.Diff(got, tt.want, protocmp.Transform())) + + // TODO: Remove this once we no longer persist all host lookup calls + // when retrieving the endpoints. + for _, ep := range got { + h := allocHost() + h.PublicId = ep.HostId + require.NoError(rw.LookupByPublicId(ctx, h)) + + assert.Equal(uint32(1), h.Version) + assert.Equal(ep.HostId, h.PublicId) + assert.Equal(ep.Address, h.Address) + assert.Equal(catalog.GetPublicId(), h.GetCatalogId()) + } + }) + } +} + func TestRepository_ListSets(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) diff --git a/internal/host/plugin/store/host.pb.go b/internal/host/plugin/store/host.pb.go index 2b88558b7..1f2d7dcec 100644 --- a/internal/host/plugin/store/host.pb.go +++ b/internal/host/plugin/store/host.pb.go @@ -155,6 +155,127 @@ func (x *HostCatalog) GetAttributes() []byte { return nil } +type Host struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + PublicId string `protobuf:"bytes,1,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // The update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,3,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // name is optional. If set, it must be unique within + // catalog_id. + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description is optional. + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // catalog_id is the public_id of the owning + // plugin_host_catalog and must be set. + // @inject_tag: `gorm:"not_null"` + CatalogId string `protobuf:"bytes,6,opt,name=catalog_id,json=catalogId,proto3" json:"catalog_id,omitempty" gorm:"not_null"` + // version allows optimistic locking of the resource + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,7,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // address is the IP Address or DNS name of the host. + // @inject_tag: `gorm:"default:null"` + Address string `protobuf:"bytes,8,opt,name=address,proto3" json:"address,omitempty" gorm:"default:null"` +} + +func (x *Host) Reset() { + *x = Host{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Host) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Host) ProtoMessage() {} + +func (x *Host) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Host.ProtoReflect.Descriptor instead. +func (*Host) Descriptor() ([]byte, []int) { + return file_controller_storage_host_plugin_store_v1_host_proto_rawDescGZIP(), []int{1} +} + +func (x *Host) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *Host) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *Host) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *Host) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Host) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Host) GetCatalogId() string { + if x != nil { + return x.CatalogId + } + return "" +} + +func (x *Host) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *Host) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + type HostSet struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -194,7 +315,7 @@ type HostSet struct { func (x *HostSet) Reset() { *x = HostSet{} if protoimpl.UnsafeEnabled { - mi := &file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[1] + mi := &file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -207,7 +328,7 @@ func (x *HostSet) String() string { func (*HostSet) ProtoMessage() {} func (x *HostSet) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[1] + mi := &file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -220,7 +341,7 @@ func (x *HostSet) ProtoReflect() protoreflect.Message { // Deprecated: Use HostSet.ProtoReflect.Descriptor instead. func (*HostSet) Descriptor() ([]byte, []int) { - return file_controller_storage_host_plugin_store_v1_host_proto_rawDescGZIP(), []int{1} + return file_controller_storage_host_plugin_store_v1_host_proto_rawDescGZIP(), []int{2} } func (x *HostSet) GetPublicId() string { @@ -316,7 +437,7 @@ type HostCatalogSecret struct { func (x *HostCatalogSecret) Reset() { *x = HostCatalogSecret{} if protoimpl.UnsafeEnabled { - mi := &file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[2] + mi := &file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -329,7 +450,7 @@ func (x *HostCatalogSecret) String() string { func (*HostCatalogSecret) ProtoMessage() {} func (x *HostCatalogSecret) ProtoReflect() protoreflect.Message { - mi := &file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[2] + mi := &file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -342,7 +463,7 @@ func (x *HostCatalogSecret) ProtoReflect() protoreflect.Message { // Deprecated: Use HostCatalogSecret.ProtoReflect.Descriptor instead. func (*HostCatalogSecret) Descriptor() ([]byte, []int) { - return file_controller_storage_host_plugin_store_v1_host_proto_rawDescGZIP(), []int{2} + return file_controller_storage_host_plugin_store_v1_host_proto_rawDescGZIP(), []int{3} } func (x *HostCatalogSecret) GetCatalogId() string { @@ -429,57 +550,77 @@ var file_controller_storage_host_plugin_store_v1_host_proto_rawDesc = []byte{ 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x61, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x22, 0xb2, 0x03, 0x0a, 0x07, 0x48, 0x6f, - 0x73, 0x74, 0x53, 0x65, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, - 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, - 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x22, 0xc6, 0x02, 0x0a, 0x04, 0x48, 0x6f, + 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, + 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, - 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x64, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x5f, - 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, - 0x67, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, - 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x2f, 0x0a, - 0x13, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x70, 0x72, 0x65, 0x66, - 0x65, 0x72, 0x72, 0x65, 0x64, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x22, 0x98, - 0x02, 0x0a, 0x11, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x53, 0x65, - 0x63, 0x72, 0x65, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, - 0x67, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, - 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, - 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, - 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x16, 0x0a, - 0x06, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x73, - 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, - 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x63, 0x74, 0x53, 0x65, 0x63, 0x72, - 0x65, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x42, 0x40, 0x5a, 0x3e, 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, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, - 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x04, 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, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x1d, 0x0a, 0x0a, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x49, 0x64, 0x12, 0x18, + 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x22, 0xb2, 0x03, 0x0a, 0x07, 0x48, 0x6f, 0x73, 0x74, 0x53, 0x65, 0x74, 0x12, 0x1b, + 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, + 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, + 0x0a, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x2f, 0x0a, 0x13, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, + 0x72, 0x65, 0x64, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x09, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x12, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x45, 0x6e, + 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x22, 0x98, 0x02, 0x0a, 0x11, 0x48, 0x6f, 0x73, 0x74, + 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x1d, 0x0a, + 0x0a, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, + 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x1b, + 0x0a, 0x09, 0x63, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x08, 0x63, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6b, + 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, + 0x49, 0x64, 0x42, 0x40, 0x5a, 0x3e, 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, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x68, 0x6f, 0x73, + 0x74, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -494,25 +635,28 @@ func file_controller_storage_host_plugin_store_v1_host_proto_rawDescGZIP() []byt return file_controller_storage_host_plugin_store_v1_host_proto_rawDescData } -var file_controller_storage_host_plugin_store_v1_host_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_controller_storage_host_plugin_store_v1_host_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_controller_storage_host_plugin_store_v1_host_proto_goTypes = []interface{}{ (*HostCatalog)(nil), // 0: controller.storage.host.plugin.store.v1.HostCatalog - (*HostSet)(nil), // 1: controller.storage.host.plugin.store.v1.HostSet - (*HostCatalogSecret)(nil), // 2: controller.storage.host.plugin.store.v1.HostCatalogSecret - (*timestamp.Timestamp)(nil), // 3: controller.storage.timestamp.v1.Timestamp + (*Host)(nil), // 1: controller.storage.host.plugin.store.v1.Host + (*HostSet)(nil), // 2: controller.storage.host.plugin.store.v1.HostSet + (*HostCatalogSecret)(nil), // 3: controller.storage.host.plugin.store.v1.HostCatalogSecret + (*timestamp.Timestamp)(nil), // 4: controller.storage.timestamp.v1.Timestamp } var file_controller_storage_host_plugin_store_v1_host_proto_depIdxs = []int32{ - 3, // 0: controller.storage.host.plugin.store.v1.HostCatalog.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 3, // 1: controller.storage.host.plugin.store.v1.HostCatalog.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 3, // 2: controller.storage.host.plugin.store.v1.HostSet.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 3, // 3: controller.storage.host.plugin.store.v1.HostSet.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 3, // 4: controller.storage.host.plugin.store.v1.HostCatalogSecret.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 3, // 5: controller.storage.host.plugin.store.v1.HostCatalogSecret.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // [6:6] is the sub-list for method output_type - 6, // [6:6] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 4, // 0: controller.storage.host.plugin.store.v1.HostCatalog.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 1: controller.storage.host.plugin.store.v1.HostCatalog.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 2: controller.storage.host.plugin.store.v1.Host.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 3: controller.storage.host.plugin.store.v1.Host.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 4: controller.storage.host.plugin.store.v1.HostSet.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 5: controller.storage.host.plugin.store.v1.HostSet.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 6: controller.storage.host.plugin.store.v1.HostCatalogSecret.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 7: controller.storage.host.plugin.store.v1.HostCatalogSecret.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_controller_storage_host_plugin_store_v1_host_proto_init() } @@ -534,7 +678,7 @@ func file_controller_storage_host_plugin_store_v1_host_proto_init() { } } file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*HostSet); i { + switch v := v.(*Host); i { case 0: return &v.state case 1: @@ -546,6 +690,18 @@ func file_controller_storage_host_plugin_store_v1_host_proto_init() { } } file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*HostSet); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_host_plugin_store_v1_host_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*HostCatalogSecret); i { case 0: return &v.state @@ -564,7 +720,7 @@ func file_controller_storage_host_plugin_store_v1_host_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_storage_host_plugin_store_v1_host_proto_rawDesc, NumEnums: 0, - NumMessages: 3, + NumMessages: 4, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/host/static/repository_host_set.go b/internal/host/static/repository_host_set.go index a3d986924..b28138f4c 100644 --- a/internal/host/static/repository_host_set.go +++ b/internal/host/static/repository_host_set.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/boundary/internal/db" dbcommon "github.com/hashicorp/boundary/internal/db/common" "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/host" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" ) @@ -182,6 +183,26 @@ func (r *Repository) UpdateSet(ctx context.Context, scopeId string, s *HostSet, return returnedHostSet, hosts, rowsUpdated, nil } +// Endpoints returns a slice of host.Endpoint for the provided set id. +// If there are no hosts in the provided set id the slice is empty. +// If the set does not exist an error is returned. +func (r *Repository) Endpoints(ctx context.Context, setId string) ([]*host.Endpoint, error) { + const op = "static.(Repository).Endpoints" + _, hs, err := r.lookupSet(ctx, setId) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + var es []*host.Endpoint + for _, h := range hs { + es = append(es, &host.Endpoint{ + HostId: h.GetPublicId(), + SetId: setId, + Address: h.GetAddress(), + }) + } + return es, err +} + // LookupSet will look up a host set in the repository and return the host // set and the hosts assigned to the host set. If the host set is not // found, it will return nil, nil, nil. The WithLimit option can be used to @@ -191,6 +212,11 @@ func (r *Repository) LookupSet(ctx context.Context, publicId string, opt ...Opti if publicId == "" { return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "no public id") } + return r.lookupSet(ctx, publicId, opt...) +} + +func (r *Repository) lookupSet(ctx context.Context, publicId string, opt ...Option) (*HostSet, []*Host, error) { + const op = "static.(Repository).lookupSet" opts := getOpts(opt...) limit := r.defaultLimit if opts.withLimit != 0 { @@ -201,29 +227,20 @@ func (r *Repository) LookupSet(ctx context.Context, publicId string, opt ...Opti s := allocHostSet() s.PublicId = publicId + if err := r.reader.LookupByPublicId(ctx, s); err != nil { + if errors.IsNotFoundError(err) { + return nil, nil, nil + } + return nil, nil, errors.Wrap(ctx, err, op) + } var hosts []*Host - _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, _ db.Writer) error { - if err := reader.LookupByPublicId(ctx, s); err != nil { - if errors.IsNotFoundError(err) { - s = nil - return nil - } - if err != nil { - return errors.Wrap(ctx, err, op) - } - return nil - } - var err error - hosts, err = getHosts(ctx, reader, s.PublicId, limit) - if err != nil { - return errors.Wrap(ctx, err, op) - } - return nil - }) + var err error + if hosts, err = getHosts(ctx, r.reader, s.PublicId, limit); err != nil { + return nil, nil, errors.Wrap(ctx, err, op) + } if err != nil { return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in %s", s.PublicId))) } - return s, hosts, nil } diff --git a/internal/host/subtype.go b/internal/host/subtype.go index 2316d0785..b9da2e5c6 100644 --- a/internal/host/subtype.go +++ b/internal/host/subtype.go @@ -1,7 +1,6 @@ package host import ( - "github.com/hashicorp/boundary/internal/boundary" "github.com/hashicorp/boundary/internal/types/subtypes" ) @@ -25,22 +24,3 @@ func SubtypeFromId(id string) subtypes.Subtype { func Register(subtype subtypes.Subtype, prefixes ...string) error { return registry.Register(subtype, prefixes...) } - -// Catalog contains the common methods across all the different types of host catalogs. -type Catalog interface { - boundary.Resource - GetScopeId() string -} - -// Set contains the common methods across all the different types of host sets. -type Set interface { - boundary.Resource - GetCatalogId() string -} - -// Host contains the common methods across all the different types of hosts. -type Host interface { - boundary.Resource - GetCatalogId() string - GetAddress() string -} diff --git a/internal/proto/local/controller/storage/host/plugin/store/v1/host.proto b/internal/proto/local/controller/storage/host/plugin/store/v1/host.proto index 37f1c2141..5d9722bea 100644 --- a/internal/proto/local/controller/storage/host/plugin/store/v1/host.proto +++ b/internal/proto/local/controller/storage/host/plugin/store/v1/host.proto @@ -47,6 +47,42 @@ message HostCatalog { bytes attributes = 9; } +message Host { + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + string public_id = 1; + + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 2; + + // The update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 3; + + // name is optional. If set, it must be unique within + // catalog_id. + // @inject_tag: `gorm:"default:null"` + string name = 4; + + // description is optional. + // @inject_tag: `gorm:"default:null"` + string description = 5; + + // catalog_id is the public_id of the owning + // plugin_host_catalog and must be set. + // @inject_tag: `gorm:"not_null"` + string catalog_id = 6; + + // version allows optimistic locking of the resource + // @inject_tag: `gorm:"default:null"` + uint32 version = 7; + + // address is the IP Address or DNS name of the host. + // @inject_tag: `gorm:"default:null"` + string address = 8; +} + message HostSet { // public_id is a surrogate key suitable for use in a public API. // @inject_tag: `gorm:"primary_key"` diff --git a/internal/servers/controller/handler.go b/internal/servers/controller/handler.go index 94e259d6c..d59a39c82 100644 --- a/internal/servers/controller/handler.go +++ b/internal/servers/controller/handler.go @@ -142,11 +142,13 @@ func handleGrpcGateway(c *Controller, props HandlerProperties) (http.Handler, er return nil, fmt.Errorf("failed to register user service handler: %w", err) } ts, err := targets.NewService( + ctx, c.kms, c.TargetRepoFn, c.IamRepoFn, c.ServersRepoFn, c.SessionRepoFn, + c.PluginHostRepoFn, c.StaticHostRepoFn, c.VaultCredentialRepoFn) if err != nil { diff --git a/internal/servers/controller/handlers/targets/target_service.go b/internal/servers/controller/handlers/targets/target_service.go index d28371373..f1ab5844f 100644 --- a/internal/servers/controller/handlers/targets/target_service.go +++ b/internal/servers/controller/handlers/targets/target_service.go @@ -91,6 +91,7 @@ type Service struct { iamRepoFn common.IamRepoFactory serversRepoFn common.ServersRepoFactory sessionRepoFn common.SessionRepoFactory + pluginHostRepoFn common.PluginHostRepoFactory staticHostRepoFn common.StaticRepoFactory vaultCredRepoFn common.VaultCredentialRepoFactory kmsCache *kms.Kms @@ -98,37 +99,43 @@ type Service struct { // NewService returns a target service which handles target related requests to boundary. func NewService( + ctx context.Context, kmsCache *kms.Kms, repoFn common.TargetRepoFactory, iamRepoFn common.IamRepoFactory, serversRepoFn common.ServersRepoFactory, sessionRepoFn common.SessionRepoFactory, + pluginHostRepoFn common.PluginHostRepoFactory, staticHostRepoFn common.StaticRepoFactory, vaultCredRepoFn common.VaultCredentialRepoFactory) (Service, error) { const op = "targets.NewService" if repoFn == nil { - return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing target repository") + return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing target repository") } if iamRepoFn == nil { - return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing iam repository") + return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing iam repository") } if serversRepoFn == nil { - return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing servers repository") + return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing servers repository") } if sessionRepoFn == nil { - return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing session repository") + return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing session repository") + } + if pluginHostRepoFn == nil { + return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing plugin host repository") } if staticHostRepoFn == nil { - return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing static host repository") + return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing static host repository") } if vaultCredRepoFn == nil { - return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing vault credential repository") + return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing vault credential repository") } return Service{ repoFn: repoFn, iamRepoFn: iamRepoFn, serversRepoFn: serversRepoFn, sessionRepoFn: sessionRepoFn, + pluginHostRepoFn: pluginHostRepoFn, staticHostRepoFn: staticHostRepoFn, vaultCredRepoFn: vaultCredRepoFn, kmsCache: kmsCache, @@ -940,55 +947,65 @@ func (s Service) AuthorizeSession(ctx context.Context, req *pbs.AuthorizeSession "No workers are available to handle this session, or all have been filtered.") } - // First, fetch all available hosts. Unless one was chosen in the request, - // we will pick one at random. - type compoundHost struct { - hostSetId string - hostId string - } - - var chosenId *compoundHost requestedId := req.GetHostId() staticHostRepo, err := s.staticHostRepoFn() if err != nil { return nil, err } + pluginHostRepo, err := s.pluginHostRepoFn() + if err != nil { + return nil, err + } - hostIds := make([]compoundHost, 0, len(hostSources)*10) - -HostSourceIterationLoop: + var pluginHostSetIds []string + var endpoints []*host.Endpoint for _, hSource := range hostSources { hsId := hSource.Id() + // FIXME: read in type from DB rather than rely on prefix switch host.SubtypeFromId(hsId) { case static.Subtype: - _, hosts, err := staticHostRepo.LookupSet(ctx, hsId) + eps, err := staticHostRepo.Endpoints(ctx, hsId) if err != nil { return nil, err } - for _, host := range hosts { - compoundId := compoundHost{hostSetId: hsId, hostId: host.PublicId} - hostIds = append(hostIds, compoundId) - if host.PublicId == requestedId { - chosenId = &compoundId - break HostSourceIterationLoop - } + endpoints = append(endpoints, eps...) + default: + // Batch the plugin host set ids since each round trip to the plugin + // has the potential to be expensive. + pluginHostSetIds = append(pluginHostSetIds, hsId) + } + } + if len(pluginHostSetIds) > 0 { + eps, err := pluginHostRepo.Endpoints(ctx, pluginHostSetIds) + if err != nil { + return nil, err + } + endpoints = append(endpoints, eps...) + } + + var chosenEndpoint *host.Endpoint + if requestedId != "" { + for _, ep := range endpoints { + if ep.HostId == requestedId { + chosenEndpoint = ep } } - } - if requestedId != "" && chosenId == nil { - // We didn't find it - return nil, handlers.InvalidArgumentErrorf( - "Errors in provided fields.", - map[string]string{ - "host_id": "The requested host id is not available.", - }) - } - if chosenId == nil { - if len(hostIds) == 0 { - // No hosts were found, error - return nil, handlers.NotFoundErrorf("No hosts found from available target host sources.") + if chosenEndpoint == nil { + // We didn't find it + return nil, handlers.InvalidArgumentErrorf( + "Errors in provided fields.", + map[string]string{ + "host_id": "The requested host id is not available.", + }) } - chosenId = &hostIds[rand.Intn(len(hostIds))] + } + + if chosenEndpoint == nil { + if len(endpoints) == 0 { + // No hosts were found, error + return nil, handlers.NotFoundErrorf("No endpoint found from available target host sources.") + } + chosenEndpoint = endpoints[rand.Intn(len(endpoints))] } // Generate the endpoint URL @@ -996,22 +1013,10 @@ HostSourceIterationLoop: Scheme: t.GetType(), } defaultPort := t.GetDefaultPort() - var endpointHost string - switch host.SubtypeFromId(chosenId.hostId) { - case static.Subtype: - h, err := staticHostRepo.LookupHost(ctx, chosenId.hostId) - if err != nil { - return nil, errors.New(ctx, errors.InvalidParameter, op, "errors looking up host") - } - endpointHost = h.Address - if endpointHost == "" { - return nil, stderrors.New("host had empty address") - } - } if defaultPort != 0 { - endpointUrl.Host = fmt.Sprintf("%s:%d", endpointHost, defaultPort) + endpointUrl.Host = fmt.Sprintf("%s:%d", chosenEndpoint.Address, defaultPort) } else { - endpointUrl.Host = endpointHost + endpointUrl.Host = chosenEndpoint.Address } var reqs []credential.Request @@ -1028,9 +1033,9 @@ HostSourceIterationLoop: expTime.Seconds += int64(t.GetSessionMaxSeconds()) sessionComposition := session.ComposedOf{ UserId: authResults.UserId, - HostId: chosenId.hostId, + HostId: chosenEndpoint.HostId, TargetId: t.GetPublicId(), - HostSetId: chosenId.hostSetId, + HostSetId: chosenEndpoint.SetId, AuthTokenId: authResults.AuthTokenId, ScopeId: authResults.Scope.Id, Endpoint: endpointUrl.String(), @@ -1122,7 +1127,7 @@ HostSourceIterationLoop: Type: t.GetType(), Certificate: sess.Certificate, PrivateKey: privKey, - HostId: chosenId.hostId, + HostId: chosenEndpoint.HostId, Endpoint: endpointUrl.String(), WorkerInfo: workers, ConnectionLimit: t.GetSessionConnectionLimit(), @@ -1141,8 +1146,8 @@ HostSourceIterationLoop: Type: t.GetType(), AuthorizationToken: string(encodedMarshaledSad), UserId: authResults.UserId, - HostId: chosenId.hostId, - HostSetId: chosenId.hostSetId, + HostId: chosenEndpoint.HostId, + HostSetId: chosenEndpoint.SetId, Endpoint: endpointUrl.String(), Credentials: creds, } diff --git a/internal/servers/controller/handlers/targets/target_service_test.go b/internal/servers/controller/handlers/targets/target_service_test.go index cedd2b038..7b6e19936 100644 --- a/internal/servers/controller/handlers/targets/target_service_test.go +++ b/internal/servers/controller/handlers/targets/target_service_test.go @@ -88,10 +88,13 @@ func testService(t *testing.T, conn *gorm.DB, kms *kms.Kms, wrapper wrapping.Wra staticHostRepoFn := func() (*static.Repository, error) { return static.NewRepository(rw, rw, kms) } + pluginHostRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms, map[string]plgpb.HostPluginServiceServer{}) + } credentialRepoFn := func() (*vault.Repository, error) { return vault.NewRepository(rw, rw, kms, sche) } - return targets.NewService(kms, repoFn, iamRepoFn, serversRepoFn, sessionRepoFn, staticHostRepoFn, credentialRepoFn) + return targets.NewService(context.Background(), kms, repoFn, iamRepoFn, serversRepoFn, sessionRepoFn, pluginHostRepoFn, staticHostRepoFn, credentialRepoFn) } func TestGet(t *testing.T) { @@ -2586,6 +2589,33 @@ func TestAuthorizeSession(t *testing.T) { return authtoken.NewRepository(rw, rw, kms) } + plg := host.TestPlugin(t, conn, "test") + plgm := map[string]plgpb.HostPluginServiceServer{ + plg.GetPublicId(): plugin.TestPluginServer{ + ListHostsFn: func(_ context.Context, req *plgpb.ListHostsRequest) (*plgpb.ListHostsResponse, error) { + var setIds []string + for _, set := range req.GetSets() { + setIds = append(setIds, set.GetId()) + } + return &plgpb.ListHostsResponse{Hosts: []*plgpb.ListHostsResponseHost{ + { + SetIds: setIds, + ExternalId: "test", + IpAddresses: []string{"10.0.0.1", "192.168.0.1"}, + }, + { + SetIds: setIds, + ExternalId: "test2", + IpAddresses: []string{"10.1.1.1", "192.168.1.1"}, + }, + }}, nil + }, + }, + } + pluginHostRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms, plgm) + } + org, proj := iam.TestScopes(t, iamRepo) at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId()) ctx := auth.NewVerifierContext(requests.NewRequestContext(context.Background()), @@ -2603,21 +2633,16 @@ func TestAuthorizeSession(t *testing.T) { _ = iam.TestUserRole(t, conn, r.GetPublicId(), at.GetIamUserId()) _ = iam.TestRoleGrant(t, conn, r.GetPublicId(), "id=*;type=*;actions=*") - s, err := targets.NewService(kms, repoFn, iamRepoFn, serversRepoFn, sessionRepoFn, staticHostRepoFn, credentialRepoFn) + s, err := targets.NewService(ctx, kms, repoFn, iamRepoFn, serversRepoFn, sessionRepoFn, pluginHostRepoFn, staticHostRepoFn, credentialRepoFn) require.NoError(t, err) - tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), "test") hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] - hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] - _ = static.TestSetMembers(t, conn, hs.GetPublicId(), []*static.Host{h}) + shs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0] + _ = static.TestSetMembers(t, conn, shs.GetPublicId(), []*static.Host{h}) - apiTar, err := s.AddTargetHostSets(ctx, &pbs.AddTargetHostSetsRequest{ - Id: tar.GetPublicId(), - Version: tar.GetVersion(), - HostSetIds: []string{hs.GetPublicId()}, - }) - require.NoError(t, err) + phc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId()) + phs := plugin.TestSet(t, conn, kms, phc, plgm, plugin.WithPreferredEndpoints([]string{"cidr:10.0.0.0/24"})) v := vault.NewTestVaultServer(t) v.MountPKI(t) @@ -2638,95 +2663,135 @@ func TestAuthorizeSession(t *testing.T) { }}) require.NoError(t, err) - _, err = s.AddTargetCredentialSources(ctx, - &pbs.AddTargetCredentialSourcesRequest{ - Id: tar.GetPublicId(), - ApplicationCredentialSourceIds: []string{clsResp.GetItem().GetId()}, - Version: apiTar.GetItem().GetVersion(), + cases := []struct { + name string + hostSourceId string + credSourceId string + wantedHostId string + wantedEndpoint string + }{ + { + name: "static host", + hostSourceId: shs.GetPublicId(), + credSourceId: clsResp.GetItem().GetId(), + wantedHostId: h.GetPublicId(), + wantedEndpoint: h.GetAddress(), + }, + { + name: "plugin host", + hostSourceId: phs.GetPublicId(), + credSourceId: clsResp.GetItem().GetId(), + wantedHostId: "?", + wantedEndpoint: "10.0.0.1", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tar := target.TestTcpTarget(t, conn, proj.GetPublicId(), tc.name) + apiTar, err := s.AddTargetHostSets(ctx, &pbs.AddTargetHostSetsRequest{ + Id: tar.GetPublicId(), + Version: tar.GetVersion(), + HostSetIds: []string{tc.hostSourceId}, + }) + require.NoError(t, err) + _, err = s.AddTargetCredentialSources(ctx, + &pbs.AddTargetCredentialSourcesRequest{ + Id: tar.GetPublicId(), + ApplicationCredentialSourceIds: []string{clsResp.GetItem().GetId()}, + Version: apiTar.GetItem().GetVersion(), + }) + require.NoError(t, err) + + // Tell our DB that there is a worker ready to serve the data + workerService := workers.NewWorkerServiceServer(serversRepoFn, sessionRepoFn, &sync.Map{}, kms) + _, err = workerService.Status(ctx, &spbs.StatusRequest{ + Worker: &spb.Server{ + PrivateId: "testworker", + Address: "localhost:8457", + }, + }) + require.NoError(t, err) + + asRes1, err := s.AuthorizeSession(ctx, &pbs.AuthorizeSessionRequest{ + Id: tar.GetPublicId(), + }) + require.NoError(t, err) + asRes2, err := s.AuthorizeSession(ctx, &pbs.AuthorizeSessionRequest{ + Id: tar.GetPublicId(), + }) + require.NoError(t, err) + assert.NotEmpty(t, cmp.Diff(asRes1.GetItem().GetCredentials(), asRes2.GetItem().GetCredentials(), protocmp.Transform()), + "the credentials aren't unique per request authorized session") + + wantedHostId := tc.wantedHostId + if tc.wantedHostId == "?" { + wantedHostId = asRes2.GetItem().GetHostId() + } + + want := &pb.SessionAuthorization{ + Scope: &scopes.ScopeInfo{ + Id: proj.GetPublicId(), + Type: proj.GetType(), + Name: proj.GetName(), + Description: proj.GetDescription(), + ParentScopeId: proj.GetParentId(), + }, + TargetId: tar.GetPublicId(), + UserId: at.GetIamUserId(), + HostSetId: tc.hostSourceId, + HostId: wantedHostId, + Type: "tcp", + Endpoint: fmt.Sprintf("tcp://%s", tc.wantedEndpoint), + Credentials: []*pb.SessionCredential{{ + CredentialLibrary: &pb.CredentialLibrary{ + Id: clsResp.GetItem().GetId(), + Name: clsResp.GetItem().GetName().GetValue(), + Description: clsResp.GetItem().GetDescription().GetValue(), + CredentialStoreId: store.GetPublicId(), + Type: vault.Subtype.String(), + }, + CredentialSource: &pb.CredentialSource{ + Id: clsResp.GetItem().GetId(), + Name: clsResp.GetItem().GetName().GetValue(), + Description: clsResp.GetItem().GetDescription().GetValue(), + CredentialStoreId: store.GetPublicId(), + Type: vault.Subtype.String(), + }, + }}, + // TODO: validate the contents of the authorization token is what is expected + } + wantSecret := map[string]interface{}{ + "certificate": "-----BEGIN CERTIFICATE-----\n", + "issuing_ca": "-----BEGIN CERTIFICATE-----\n", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\n", + "private_key_type": "rsa", + } + got := asRes1.GetItem() + + require.Len(t, got.GetCredentials(), 1) + + gotCred := got.Credentials[0] + require.NotNil(t, gotCred.Secret) + assert.NotEmpty(t, gotCred.Secret.Raw) + dSec := decodeJsonSecret(t, gotCred.Secret.Raw) + require.NoError(t, err) + require.Equal(t, dSec, gotCred.Secret.Decoded.AsMap()) + for k, v := range wantSecret { + gotV, ok := dSec[k] + require.True(t, ok) + assert.Truef(t, strings.HasPrefix(gotV.(string), v.(string)), "%q:%q doesn't have prefix %q", k, gotV, v) + } + gotCred.Secret = nil + + got.AuthorizationToken, got.SessionId, got.CreatedTime = "", "", nil + assert.Empty(t, cmp.Diff(got, want, protocmp.Transform())) }) - require.NoError(t, err) - - // Tell our DB that there is a worker ready to serve the data - workerService := workers.NewWorkerServiceServer(serversRepoFn, sessionRepoFn, &sync.Map{}, kms) - _, err = workerService.Status(ctx, &spbs.StatusRequest{ - Worker: &spb.Server{ - PrivateId: "testworker", - Address: "localhost:8457", - }, - }) - require.NoError(t, err) - - asRes1, err := s.AuthorizeSession(ctx, &pbs.AuthorizeSessionRequest{ - Id: tar.GetPublicId(), - }) - require.NoError(t, err) - asRes2, err := s.AuthorizeSession(ctx, &pbs.AuthorizeSessionRequest{ - Id: tar.GetPublicId(), - }) - require.NoError(t, err) - assert.NotEmpty(t, cmp.Diff(asRes1.GetItem().GetCredentials(), asRes2.GetItem().GetCredentials(), protocmp.Transform()), - "the credentials aren't unique per request authorized session") - - want := &pb.SessionAuthorization{ - Scope: &scopes.ScopeInfo{ - Id: proj.GetPublicId(), - Type: proj.GetType(), - Name: proj.GetName(), - Description: proj.GetDescription(), - ParentScopeId: proj.GetParentId(), - }, - TargetId: tar.GetPublicId(), - UserId: at.GetIamUserId(), - HostSetId: hs.GetPublicId(), - HostId: h.GetPublicId(), - Type: "tcp", - Endpoint: fmt.Sprintf("tcp://%s", h.GetAddress()), - Credentials: []*pb.SessionCredential{{ - CredentialLibrary: &pb.CredentialLibrary{ - Id: clsResp.GetItem().GetId(), - Name: clsResp.GetItem().GetName().GetValue(), - Description: clsResp.GetItem().GetDescription().GetValue(), - CredentialStoreId: store.GetPublicId(), - Type: vault.Subtype.String(), - }, - CredentialSource: &pb.CredentialSource{ - Id: clsResp.GetItem().GetId(), - Name: clsResp.GetItem().GetName().GetValue(), - Description: clsResp.GetItem().GetDescription().GetValue(), - CredentialStoreId: store.GetPublicId(), - Type: vault.Subtype.String(), - }, - }}, - // TODO: validate the contents of the authorization token is what is expected } - wantSecret := map[string]interface{}{ - "certificate": "-----BEGIN CERTIFICATE-----\n", - "issuing_ca": "-----BEGIN CERTIFICATE-----\n", - "private_key": "-----BEGIN RSA PRIVATE KEY-----\n", - "private_key_type": "rsa", - } - got := asRes1.GetItem() - - require.Len(t, got.GetCredentials(), 1) - - gotCred := got.Credentials[0] - require.NotNil(t, gotCred.Secret) - assert.NotEmpty(t, gotCred.Secret.Raw) - dSec := decodeJsonSecret(t, gotCred.Secret.Raw) - require.NoError(t, err) - require.Equal(t, dSec, gotCred.Secret.Decoded.AsMap()) - for k, v := range wantSecret { - gotV, ok := dSec[k] - require.True(t, ok) - assert.Truef(t, strings.HasPrefix(gotV.(string), v.(string)), "%q:%q doesn't have prefix %q", k, gotV, v) - } - gotCred.Secret = nil - - got.AuthorizationToken, got.SessionId, got.CreatedTime = "", "", nil - assert.Empty(t, cmp.Diff(got, want, protocmp.Transform())) } func TestAuthorizeSession_Errors(t *testing.T) { + ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) @@ -2749,6 +2814,9 @@ func TestAuthorizeSession_Errors(t *testing.T) { staticHostRepoFn := func() (*static.Repository, error) { return static.NewRepository(rw, rw, kms) } + pluginHostRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms, map[string]plgpb.HostPluginServiceServer{}) + } credentialRepoFn := func() (*vault.Repository, error) { return vault.NewRepository(rw, rw, kms, sche) } @@ -2757,12 +2825,12 @@ func TestAuthorizeSession_Errors(t *testing.T) { } org, proj := iam.TestScopes(t, iamRepo) - s, err := targets.NewService(kms, repoFn, iamRepoFn, serversRepoFn, sessionRepoFn, staticHostRepoFn, credentialRepoFn) + s, err := targets.NewService(ctx, kms, repoFn, iamRepoFn, serversRepoFn, sessionRepoFn, pluginHostRepoFn, staticHostRepoFn, credentialRepoFn) require.NoError(t, err) // Authorized user gets full permissions at := authtoken.TestAuthToken(t, conn, kms, org.GetPublicId()) - ctx := auth.NewVerifierContext(requests.NewRequestContext(context.Background()), + ctx = auth.NewVerifierContext(requests.NewRequestContext(context.Background()), iamRepoFn, atRepoFn, serversRepoFn,