From 14cc97b645a15703069645fffba7b1e91c046a00 Mon Sep 17 00:00:00 2001 From: Todd Date: Sun, 26 Sep 2021 08:17:21 -0700 Subject: [PATCH] Add authorize session for plugin based host sets. (#1559) Added a new Endpoint struct at the top level host package level to capture host, set, and address chosen. Temporarily add a host_plugin_host table to capture the hosts returned by the plugins. This ensures the datawarehouse is able to query the table to decide what host was chosen. The view used to update the datawarehouse dimension table provides rows with host set ids and hosts which are not in that host set. This is ok since the function which updates the dimension table provides the host set and host id chosen so that relationship doesn't need to be captured in the view itself. Co-authored-by: Jeff Mitchell --- .../postgres/17/04_plugin_host.up.sql | 60 ++++ .../17/06_wh_session_dimensions.up.sql | 85 +++++ internal/db/schema/postgres_migration.gen.go | 145 ++++++++- internal/host/endpoint.go | 9 + internal/host/host.go | 23 ++ internal/host/plugin/host.go | 76 +++++ internal/host/plugin/host_test.go | 202 ++++++++++++ internal/host/plugin/ids.go | 17 +- .../host/plugin/repository_host_catalog.go | 30 +- internal/host/plugin/repository_host_set.go | 293 ++++++++++++++--- .../host/plugin/repository_host_set_test.go | 134 ++++++++ internal/host/plugin/store/host.pb.go | 298 +++++++++++++----- internal/host/static/repository_host_set.go | 55 ++-- internal/host/subtype.go | 20 -- .../storage/host/plugin/store/v1/host.proto | 36 +++ internal/servers/controller/handler.go | 2 + .../handlers/targets/target_service.go | 121 +++---- .../handlers/targets/target_service_test.go | 262 +++++++++------ 18 files changed, 1536 insertions(+), 332 deletions(-) create mode 100644 internal/db/schema/migrations/postgres/17/06_wh_session_dimensions.up.sql create mode 100644 internal/host/endpoint.go create mode 100644 internal/host/host.go create mode 100644 internal/host/plugin/host.go create mode 100644 internal/host/plugin/host_test.go 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,