diff --git a/api/hosts/host.gen.go b/api/hosts/host.gen.go index 24dcc2178..718ffd667 100644 --- a/api/hosts/host.gen.go +++ b/api/hosts/host.gen.go @@ -26,6 +26,8 @@ type Host struct { Type string `json:"type,omitempty"` HostSetIds []string `json:"host_set_ids,omitempty"` Attributes map[string]interface{} `json:"attributes,omitempty"` + IpAddresses []string `json:"ip_addresses,omitempty"` + DnsNames []string `json:"dns_names,omitempty"` AuthorizedActions []string `json:"authorized_actions,omitempty"` response *api.Response diff --git a/globals/fields.go b/globals/fields.go index fe6738718..3ee466d03 100644 --- a/globals/fields.go +++ b/globals/fields.go @@ -62,4 +62,6 @@ const ( PluginIdField = "plugin_id" PluginField = "plugin" PluginNameField = "plugin_name" + IpAddressesField = "ip_addresses" + DnsNamesField = "dns_names" ) diff --git a/internal/cmd/config/config.go b/internal/cmd/config/config.go index bd47130d7..824ce0523 100644 --- a/internal/cmd/config/config.go +++ b/internal/cmd/config/config.go @@ -131,6 +131,12 @@ type Controller struct { // // TODO: This field is currently internal. StatusGracePeriodDuration time.Duration `hcl:"-"` + + // SchedulerRunJobInterval is the time interval between waking up the + // scheduler to run pending jobs. + // + // TODO: This field is currently internal. + SchedulerRunJobInterval time.Duration `hcl:"-"` } func (c *Controller) InitNameIfEmpty() (string, error) { diff --git a/internal/gen/controller.swagger.json b/internal/gen/controller.swagger.json index 8b5f11ef0..81d63567f 100644 --- a/internal/gen/controller.swagger.json +++ b/internal/gen/controller.swagger.json @@ -4213,6 +4213,22 @@ "type": "object", "description": "The attributes that are applicable to the specific Host type." }, + "ip_addresses": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Output only. The list of ip addresses associated with this host.", + "readOnly": true + }, + "dns_names": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Output only. The list of dns addresses associated with this host.", + "readOnly": true + }, "authorized_actions": { "type": "array", "items": { diff --git a/internal/host/plugin/host_set_member_test.go b/internal/host/plugin/host_set_member_test.go index 2002d0360..056552db8 100644 --- a/internal/host/plugin/host_set_member_test.go +++ b/internal/host/plugin/host_set_member_test.go @@ -157,7 +157,7 @@ func TestHostSetMember_InsertDelete(t *testing.T) { require.Len(t, hosts, 2) // Base case the count by catalog ID - hosts, err = repo.ListHostsByCatalogId(ctx, blueCat.PublicId) + hosts, _, err = repo.ListHostsByCatalogId(ctx, blueCat.PublicId) require.NoError(t, err) assert.Len(t, hosts, 4) @@ -174,7 +174,7 @@ func TestHostSetMember_InsertDelete(t *testing.T) { require.NoError(t, err) assert.Equal(t, 1, count) assert.NoError(t, db.TestVerifyOplog(t, rw, blueHost1.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second))) - hosts, err = repo.ListHostsByCatalogId(ctx, blueCat.PublicId) + hosts, _, err = repo.ListHostsByCatalogId(ctx, blueCat.PublicId) require.NoError(t, err) require.Len(t, hosts, 3) @@ -189,7 +189,7 @@ func TestHostSetMember_InsertDelete(t *testing.T) { require.NoError(t, err) assert.Equal(t, 1, count) assert.NoError(t, db.TestVerifyOplog(t, rw, blueHost2.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second))) - hosts, err = repo.ListHostsByCatalogId(ctx, blueCat.PublicId) + hosts, _, err = repo.ListHostsByCatalogId(ctx, blueCat.PublicId) require.NoError(t, err) require.Len(t, hosts, 2) @@ -206,7 +206,7 @@ func TestHostSetMember_InsertDelete(t *testing.T) { assert.Equal(t, 2, count) assert.NoError(t, db.TestVerifyOplog(t, rw, blueHost3.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second))) assert.NoError(t, db.TestVerifyOplog(t, rw, blueHost4.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second))) - hosts, err = repo.ListHostsByCatalogId(ctx, blueCat.PublicId) + hosts, _, err = repo.ListHostsByCatalogId(ctx, blueCat.PublicId) require.NoError(t, err) require.Len(t, hosts, 0) } diff --git a/internal/host/plugin/ids.go b/internal/host/plugin/ids.go index 0516abead..ab8e77e4e 100644 --- a/internal/host/plugin/ids.go +++ b/internal/host/plugin/ids.go @@ -10,7 +10,7 @@ import ( ) func init() { - if err := host.Register(Subtype, HostCatalogPrefix, HostSetPrefix); err != nil { + if err := host.Register(Subtype, HostCatalogPrefix, HostSetPrefix, HostPrefix); err != nil { panic(err) } } diff --git a/internal/host/plugin/repository_host.go b/internal/host/plugin/repository_host.go index 44649f915..5a01cdf10 100644 --- a/internal/host/plugin/repository_host.go +++ b/internal/host/plugin/repository_host.go @@ -6,33 +6,38 @@ import ( "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" + hostplugin "github.com/hashicorp/boundary/internal/plugin/host" ) // LookupHost will look up a host in the repository. If the host is not // found, it will return nil, nil. All options are ignored. -func (r *Repository) LookupHost(ctx context.Context, publicId string, opt ...Option) (*Host, error) { +func (r *Repository) LookupHost(ctx context.Context, publicId string, opt ...Option) (*Host, *hostplugin.Plugin, error) { const op = "plugin.(Repository).LookupHost" if publicId == "" { - return nil, errors.New(ctx, errors.InvalidParameter, op, "no public id") + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "no public id") } ha := &hostAgg{ PublicId: publicId, } if err := r.reader.LookupByPublicId(ctx, ha); err != nil { if errors.IsNotFoundError(err) { - return nil, nil + return nil, nil, nil } - return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", publicId))) + return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", publicId))) } - return ha.toHost(), nil + plg, err := r.getPlugin(ctx, ha.PluginId) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op) + } + return ha.toHost(), plg, nil } // ListHostsByCatalogId returns a slice of Hosts for the catalogId. // WithLimit is the only option supported. -func (r *Repository) ListHostsByCatalogId(ctx context.Context, catalogId string, opt ...Option) ([]*Host, error) { +func (r *Repository) ListHostsByCatalogId(ctx context.Context, catalogId string, opt ...Option) ([]*Host, *hostplugin.Plugin, error) { const op = "plugin.(Repository).ListHostsByCatalogId" if catalogId == "" { - return nil, errors.New(ctx, errors.InvalidParameter, op, "no catalog id") + return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "no catalog id") } opts := getOpts(opt...) limit := r.defaultLimit @@ -45,9 +50,15 @@ func (r *Repository) ListHostsByCatalogId(ctx context.Context, catalogId string, switch { case err != nil: - return nil, errors.Wrap(ctx, err, op) - case hostAggs == nil: - return nil, nil + return nil, nil, errors.Wrap(ctx, err, op) + case len(hostAggs) == 0: + return nil, nil, nil + } + + pluginId := hostAggs[0].PluginId + plg, err := r.getPlugin(ctx, pluginId) + if err != nil { + return nil, nil, errors.Wrap(ctx, err, op) } hosts := make([]*Host, 0, len(hostAggs)) @@ -55,7 +66,7 @@ func (r *Repository) ListHostsByCatalogId(ctx context.Context, catalogId string, hosts = append(hosts, ha.toHost()) } - return hosts, nil + return hosts, plg, nil } // ListHostsBySetId returns a slice of Hosts for the given set IDs. diff --git a/internal/host/plugin/repository_host_set.go b/internal/host/plugin/repository_host_set.go index 3c14e0178..6e3344244 100644 --- a/internal/host/plugin/repository_host_set.go +++ b/internal/host/plugin/repository_host_set.go @@ -143,7 +143,7 @@ func (r *Repository) CreateSet(ctx context.Context, scopeId string, s *HostSet, } // The set now exists in the plugin, sync it immediately. - r.scheduler.UpdateJobNextRunInAtLeast(ctx, setSyncJobName, 0) + _ = r.scheduler.UpdateJobNextRunInAtLeast(ctx, setSyncJobName, 0) plg, err := r.getPlugin(ctx, c.GetPluginId()) if err != nil { diff --git a/internal/host/plugin/repository_host_test.go b/internal/host/plugin/repository_host_test.go index 63075a98c..002d2f660 100644 --- a/internal/host/plugin/repository_host_test.go +++ b/internal/host/plugin/repository_host_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" hostplg "github.com/hashicorp/boundary/internal/plugin/host" + hostplgstore "github.com/hashicorp/boundary/internal/plugin/host/store" "github.com/hashicorp/boundary/internal/scheduler" plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" "github.com/stretchr/testify/assert" @@ -225,7 +226,8 @@ func TestJob_UpsertHosts(t *testing.T) { require.NotNil(repo) // Check again, but via performing an explicit list - got, err = repo.ListHostsByCatalogId(ctx, in.catalog.GetPublicId()) + var gotPlg *hostplg.Plugin + got, gotPlg, err = repo.ListHostsByCatalogId(ctx, in.catalog.GetPublicId()) require.NoError(err) require.NotNil(got) assert.Len(got, len(in.phs)) @@ -240,6 +242,14 @@ func TestJob_UpsertHosts(t *testing.T) { }), ), ) + assert.Empty( + cmp.Diff( + plg, + gotPlg, + cmpopts.IgnoreUnexported(hostplg.Plugin{}, hostplgstore.Plugin{}), + cmpopts.IgnoreTypes(×tamp.Timestamp{}), + ), + ) // Now individually call read on each host, cache the matching set // IDs, and then check membership @@ -248,7 +258,7 @@ func TestJob_UpsertHosts(t *testing.T) { for _, setId := range exp.SetIds { setIdMap[setId] = append(setIdMap[setId], exp.GetPublicId()) } - got, err := repo.LookupHost(ctx, exp.GetPublicId()) + got, gotPlg, err := repo.LookupHost(ctx, exp.GetPublicId()) require.NoError(err) require.NotNil(got) assert.NotEmpty(got.SetIds) @@ -260,6 +270,14 @@ func TestJob_UpsertHosts(t *testing.T) { cmpopts.IgnoreTypes(×tamp.Timestamp{}), ), ) + assert.Empty( + cmp.Diff( + plg, + gotPlg, + cmpopts.IgnoreUnexported(hostplg.Plugin{}, hostplgstore.Plugin{}), + cmpopts.IgnoreTypes(×tamp.Timestamp{}), + ), + ) } for setId, expHostIds := range setIdMap { got, err = repo.ListHostsBySetIds(ctx, []string{setId}) diff --git a/internal/proto/local/controller/api/resources/hosts/v1/host.proto b/internal/proto/local/controller/api/resources/hosts/v1/host.proto index 1a7794329..48b37ee90 100644 --- a/internal/proto/local/controller/api/resources/hosts/v1/host.proto +++ b/internal/proto/local/controller/api/resources/hosts/v1/host.proto @@ -50,6 +50,12 @@ message Host { // The attributes that are applicable to the specific Host type. google.protobuf.Struct attributes = 110 [(custom_options.v1.generate_sdk_option) = true]; + // Output only. The list of ip addresses associated with this host. + repeated string ip_addresses = 120; + + // Output only. The list of dns addresses associated with this host. + repeated string dns_names = 130; + // Output only. The available actions on this resource for this user. repeated string authorized_actions = 300 [json_name="authorized_actions"]; } diff --git a/internal/servers/controller/controller.go b/internal/servers/controller/controller.go index 76ae32e0d..164259274 100644 --- a/internal/servers/controller/controller.go +++ b/internal/servers/controller/controller.go @@ -172,7 +172,11 @@ func New(ctx context.Context, conf *Config) (*Controller, error) { } // TODO: the RunJobsLimit is temporary until a better fix gets in. This // currently caps the scheduler at running 10 jobs per interval. - c.scheduler, err = scheduler.New(c.conf.RawConfig.Controller.Name, jobRepoFn, scheduler.WithRunJobsLimit(10)) + schedulerOpts := []scheduler.Option{scheduler.WithRunJobsLimit(10)} + if c.conf.RawConfig.Controller.SchedulerRunJobInterval > 0 { + schedulerOpts = append(schedulerOpts, scheduler.WithRunJobsInterval(c.conf.RawConfig.Controller.SchedulerRunJobInterval)) + } + c.scheduler, err = scheduler.New(c.conf.RawConfig.Controller.Name, jobRepoFn, schedulerOpts...) if err != nil { return nil, fmt.Errorf("error creating new scheduler: %w", err) } diff --git a/internal/servers/controller/handler.go b/internal/servers/controller/handler.go index 22ea5eddf..8ab4c3b19 100644 --- a/internal/servers/controller/handler.go +++ b/internal/servers/controller/handler.go @@ -103,7 +103,7 @@ func handleGrpcGateway(c *Controller, props HandlerProperties) (http.Handler, er } } if _, ok := currentServices[services.HostService_ServiceDesc.ServiceName]; !ok { - hs, err := hosts.NewService(c.StaticHostRepoFn) + hs, err := hosts.NewService(c.StaticHostRepoFn, c.PluginHostRepoFn) if err != nil { return nil, fmt.Errorf("failed to create host handler service: %w", err) } diff --git a/internal/servers/controller/handlers/hosts/host_service.go b/internal/servers/controller/handlers/hosts/host_service.go index b5d1ac62a..e62cfffc7 100644 --- a/internal/servers/controller/handlers/hosts/host_service.go +++ b/internal/servers/controller/handlers/hosts/host_service.go @@ -10,9 +10,11 @@ import ( "github.com/hashicorp/boundary/internal/errors" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/host" + "github.com/hashicorp/boundary/internal/host/plugin" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/host/static/store" "github.com/hashicorp/boundary/internal/perms" + hostplugin "github.com/hashicorp/boundary/internal/plugin/host" "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/controller/auth" "github.com/hashicorp/boundary/internal/servers/controller/common" @@ -21,6 +23,7 @@ import ( "github.com/hashicorp/boundary/internal/types/resource" "github.com/hashicorp/boundary/internal/types/subtypes" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hosts" + "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/plugins" "google.golang.org/grpc/codes" "google.golang.org/protobuf/types/known/wrapperspb" ) @@ -56,18 +59,22 @@ type Service struct { pbs.UnimplementedHostServiceServer staticRepoFn common.StaticRepoFactory + pluginRepoFn common.PluginHostRepoFactory } var _ pbs.HostServiceServer = Service{} // NewService returns a host Service which handles host related requests to boundary and uses the provided // repositories for storage and retrieval. -func NewService(repoFn common.StaticRepoFactory) (Service, error) { +func NewService(repoFn common.StaticRepoFactory, pluginRepoFn common.PluginHostRepoFactory) (Service, error) { const op = "hosts.NewService" if repoFn == nil { return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing static repository") } - return Service{staticRepoFn: repoFn}, nil + if pluginRepoFn == nil { + return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing plugin host repository") + } + return Service{staticRepoFn: repoFn, pluginRepoFn: pluginRepoFn}, nil } func (s Service) ListHosts(ctx context.Context, req *pbs.ListHostsRequest) (*pbs.ListHostsResponse, error) { @@ -78,7 +85,7 @@ func (s Service) ListHosts(ctx context.Context, req *pbs.ListHostsRequest) (*pbs if authResults.Error != nil { return nil, authResults.Error } - hl, err := s.listFromRepo(ctx, req.GetHostCatalogId()) + hl, plg, err := s.listFromRepo(ctx, req.GetHostCatalogId()) if err != nil { return nil, err } @@ -107,6 +114,9 @@ func (s Service) ListHosts(ctx context.Context, req *pbs.ListHostsRequest) (*pbs outputFields := authResults.FetchOutputFields(res, action.List).SelfOrDefaults(authResults.UserId) outputOpts := make([]handlers.Option, 0, 3) outputOpts = append(outputOpts, handlers.WithOutputFields(&outputFields)) + if plg != nil { + outputOpts = append(outputOpts, handlers.WithPlugin(plg)) + } if outputFields.Has(globals.ScopeField) { outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope)) } @@ -114,7 +124,12 @@ func (s Service) ListHosts(ctx context.Context, req *pbs.ListHostsRequest) (*pbs outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions)) } - item, err := toProto(ctx, item, nil, outputOpts...) + var hostSetIds []string + switch h := item.(type) { + case *plugin.Host: + hostSetIds = h.SetIds + } + item, err := toProto(ctx, item, hostSetIds, outputOpts...) if err != nil { return nil, err } @@ -137,7 +152,7 @@ func (s Service) GetHost(ctx context.Context, req *pbs.GetHostRequest) (*pbs.Get if authResults.Error != nil { return nil, authResults.Error } - h, err := s.getFromRepo(ctx, req.GetId()) + h, plg, err := s.getFromRepo(ctx, req.GetId()) if err != nil { return nil, err } @@ -149,6 +164,9 @@ func (s Service) GetHost(ctx context.Context, req *pbs.GetHostRequest) (*pbs.Get outputOpts := make([]handlers.Option, 0, 3) outputOpts = append(outputOpts, handlers.WithOutputFields(&outputFields)) + if plg != nil { + outputOpts = append(outputOpts, handlers.WithPlugin(plg)) + } if outputFields.Has(globals.ScopeField) { outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope)) } @@ -156,7 +174,12 @@ func (s Service) GetHost(ctx context.Context, req *pbs.GetHostRequest) (*pbs.Get outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, h.GetPublicId(), IdActions).Strings())) } - item, err := toProto(ctx, h, nil, outputOpts...) + var hostSetIds []string + switch h := h.(type) { + case *plugin.Host: + hostSetIds = h.SetIds + } + item, err := toProto(ctx, h, hostSetIds, outputOpts...) if err != nil { return nil, err } @@ -259,19 +282,38 @@ func (s Service) DeleteHost(ctx context.Context, req *pbs.DeleteHostRequest) (*p return nil, nil } -func (s Service) getFromRepo(ctx context.Context, id string) (*static.Host, error) { - repo, err := s.staticRepoFn() - if err != nil { - return nil, err +func (s Service) getFromRepo(ctx context.Context, id string) (host.Host, *plugins.PluginInfo, error) { + var h host.Host + var plg *plugins.PluginInfo + switch host.SubtypeFromId(id) { + case static.Subtype: + repo, err := s.staticRepoFn() + if err != nil { + return nil, nil, err + } + h, err = repo.LookupHost(ctx, id) + if err != nil { + return nil, nil, err + } + if h == nil { + return nil, nil, handlers.NotFoundErrorf("Host %q doesn't exist.", id) + } + case plugin.Subtype: + repo, err := s.pluginRepoFn() + if err != nil { + return nil, nil, err + } + ph, hPlg, err := repo.LookupHost(ctx, id) + if err != nil { + return nil, nil, err + } + if ph == nil { + return nil, nil, handlers.NotFoundErrorf("Host %q doesn't exist.", id) + } + h = ph + plg = toPluginInfo(hPlg) } - h, err := repo.LookupHost(ctx, id) - if err != nil { - return nil, err - } - if h == nil { - return nil, handlers.NotFoundErrorf("Host %q doesn't exist.", id) - } - return h, nil + return h, plg, nil } func (s Service) createInRepo(ctx context.Context, scopeId, catalogId string, item *pb.Host) (*static.Host, error) { @@ -361,21 +403,47 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, return rows > 0, nil } -func (s Service) listFromRepo(ctx context.Context, catalogId string) ([]*static.Host, error) { - repo, err := s.staticRepoFn() - if err != nil { - return nil, err +func (s Service) listFromRepo(ctx context.Context, catalogId string) ([]host.Host, *plugins.PluginInfo, error) { + var hosts []host.Host + var plg *plugins.PluginInfo + switch host.SubtypeFromId(catalogId) { + case static.Subtype: + repo, err := s.staticRepoFn() + if err != nil { + return nil, nil, err + } + hl, err := repo.ListHosts(ctx, catalogId) + if err != nil { + return nil, nil, err + } + for _, h := range hl { + hosts = append(hosts, h) + } + case plugin.Subtype: + repo, err := s.pluginRepoFn() + if err != nil { + return nil, nil, err + } + hl, hlPlg, err := repo.ListHostsByCatalogId(ctx, catalogId) + if err != nil { + return nil, nil, err + } + for _, h := range hl { + hosts = append(hosts, h) + } + plg = toPluginInfo(hlPlg) } - hl, err := repo.ListHosts(ctx, catalogId) - if err != nil { - return nil, err - } - return hl, nil + return hosts, plg, nil } -func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Type) (*static.HostCatalog, auth.VerifyResults) { +func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Type) (host.Catalog, auth.VerifyResults) { res := auth.VerifyResults{} - repo, err := s.staticRepoFn() + staticRepo, err := s.staticRepoFn() + if err != nil { + res.Error = err + return nil, res + } + pluginRepo, err := s.pluginRepoFn() if err != nil { res.Error = err return nil, res @@ -387,33 +455,72 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty case action.List, action.Create: parentId = id default: - h, err := repo.LookupHost(ctx, id) + switch host.SubtypeFromId(id) { + case static.Subtype: + h, err := staticRepo.LookupHost(ctx, id) + if err != nil { + res.Error = err + return nil, res + } + if h == nil { + res.Error = handlers.NotFoundError() + return nil, res + } + parentId = h.GetCatalogId() + case plugin.Subtype: + h, _, err := pluginRepo.LookupHost(ctx, id) + if err != nil { + res.Error = err + return nil, res + } + if h == nil { + res.Error = handlers.NotFoundError() + return nil, res + } + parentId = h.GetCatalogId() + } + opts = append(opts, auth.WithId(id)) + } + + var cat host.Catalog + switch host.SubtypeFromId(id) { + case static.Subtype: + cat, err = staticRepo.LookupCatalog(ctx, parentId) if err != nil { res.Error = err return nil, res } - if h == nil { + if cat == nil { + res.Error = handlers.NotFoundError() + return nil, res + } + case plugin.Subtype: + cat, _, err = pluginRepo.LookupCatalog(ctx, parentId) + if err != nil { + res.Error = err + return nil, res + } + if cat == nil { res.Error = handlers.NotFoundError() return nil, res } - parentId = h.GetCatalogId() - opts = append(opts, auth.WithId(id)) - } - - cat, err := repo.LookupCatalog(ctx, parentId) - if err != nil { - res.Error = err - return nil, res - } - if cat == nil { - res.Error = handlers.NotFoundError() - return nil, res } opts = append(opts, auth.WithScopeId(cat.GetScopeId()), auth.WithPin(parentId)) return cat, auth.Verify(ctx, opts...) } -func toProto(ctx context.Context, in *static.Host, hostSets []*static.HostSet, opt ...handlers.Option) (*pb.Host, error) { +func toPluginInfo(plg *hostplugin.Plugin) *plugins.PluginInfo { + if plg == nil { + return nil + } + return &plugins.PluginInfo{ + Id: plg.GetPublicId(), + Name: plg.GetName(), + Description: plg.GetDescription(), + } +} + +func toProto(ctx context.Context, in host.Host, hostSetIds []string, opt ...handlers.Option) (*pb.Host, error) { opts := handlers.GetOpts(opt...) if opts.WithOutputFields == nil { return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building host proto") @@ -428,7 +535,12 @@ func toProto(ctx context.Context, in *static.Host, hostSets []*static.HostSet, o out.HostCatalogId = in.GetCatalogId() } if outputFields.Has(globals.TypeField) { - out.Type = static.Subtype.String() + switch in.(type) { + case *static.Host: + out.Type = static.Subtype.String() + case *plugin.Host: + out.Type = plugin.Subtype.String() + } } if outputFields.Has(globals.DescriptionField) && in.GetDescription() != "" { out.Description = &wrapperspb.StringValue{Value: in.GetDescription()} @@ -443,7 +555,12 @@ func toProto(ctx context.Context, in *static.Host, hostSets []*static.HostSet, o out.UpdatedTime = in.GetUpdateTime().GetTimestamp() } if outputFields.Has(globals.VersionField) { - out.Version = in.GetVersion() + switch in.(type) { + case *static.Host: + out.Version = in.GetVersion() + case *plugin.Host: + out.Version = 1 + } } if outputFields.Has(globals.ScopeField) { out.Scope = opts.WithScope @@ -452,16 +569,31 @@ func toProto(ctx context.Context, in *static.Host, hostSets []*static.HostSet, o out.AuthorizedActions = opts.WithAuthorizedActions } if outputFields.Has(globals.HostSetIdsField) { - for _, hs := range hostSets { - out.HostSetIds = append(out.HostSetIds, hs.GetPublicId()) + for _, hs := range hostSetIds { + out.HostSetIds = append(out.HostSetIds, hs) } } if outputFields.Has(globals.AttributesField) { - st, err := handlers.ProtoToStruct(&pb.StaticHostAttributes{Address: wrapperspb.String(in.GetAddress())}) - if err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to convert static attribute to struct: %s", err) + switch h := in.(type) { + case *static.Host: + st, err := handlers.ProtoToStruct(&pb.StaticHostAttributes{Address: wrapperspb.String(h.GetAddress())}) + if err != nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to convert static attribute to struct: %s", err) + } + out.Attributes = st + } + } + if outputFields.Has(globals.PluginField) { + out.Plugin = opts.WithPlugin + } + switch h := in.(type) { + case *plugin.Host: + if outputFields.Has(globals.IpAddressesField) { + out.IpAddresses = h.IpAddresses + } + if outputFields.Has(globals.DnsNamesField) { + out.DnsNames = h.DnsNames } - out.Attributes = st } return &out, nil } @@ -481,7 +613,7 @@ func validateGetRequest(req *pbs.GetHostRequest) error { badFields["id"] = "Improperly formatted identifier used." } return badFields - }, req, static.HostPrefix) + }, req, static.HostPrefix, plugin.HostPrefix) } func validateCreateRequest(req *pbs.CreateHostRequest) error { @@ -493,11 +625,17 @@ func validateCreateRequest(req *pbs.CreateHostRequest) error { switch host.SubtypeFromId(req.GetItem().GetHostCatalogId()) { case static.Subtype: if req.GetItem().GetType() != "" && req.GetItem().GetType() != static.Subtype.String() { - badFields["type"] = "Doesn't match the parent resource's type." + badFields[globals.TypeField] = "Doesn't match the parent resource's type." + } + if len(req.GetItem().GetIpAddresses()) > 0 { + badFields[globals.IpAddressesField] = "This field is not supported for this host type." + } + if len(req.GetItem().GetDnsNames()) > 0 { + badFields[globals.DnsNamesField] = "This field is not supported for this host type." } attrs := &pb.StaticHostAttributes{} if err := handlers.StructToProto(req.GetItem().GetAttributes(), attrs); err != nil { - badFields["attributes"] = "Attribute fields do not match the expected format." + badFields[globals.AttributesField] = "Attribute fields do not match the expected format." } if attrs.GetAddress() == nil || len(attrs.GetAddress().GetValue()) < static.MinHostAddressLength || @@ -513,6 +651,8 @@ func validateCreateRequest(req *pbs.CreateHostRequest) error { default: badFields["attributes.address"] = fmt.Sprintf("Error parsing address: %v.", err) } + case plugin.Subtype: + badFields[globals.HostCatalogIdField] = "Cannot manually create hosts for this type of catalog." } return badFields }) @@ -524,8 +664,7 @@ func validateUpdateRequest(req *pbs.UpdateHostRequest) error { switch host.SubtypeFromId(req.GetId()) { case static.Subtype: if req.GetItem().GetType() != "" && req.GetItem().GetType() != static.Subtype.String() { - badFields["type"] = "Cannot modify the resource type." - + badFields[globals.TypeField] = "Cannot modify the resource type." attrs := &pb.StaticHostAttributes{} if err := handlers.StructToProto(req.GetItem().GetAttributes(), attrs); err != nil { badFields["attributes"] = "Attribute fields do not match the expected format." @@ -539,6 +678,8 @@ func validateUpdateRequest(req *pbs.UpdateHostRequest) error { } } } + case plugin.Subtype: + badFields[globals.IdField] = "Cannot modify this type of host." default: badFields["id"] = "Improperly formatted identifier used." } @@ -547,12 +688,19 @@ func validateUpdateRequest(req *pbs.UpdateHostRequest) error { } func validateDeleteRequest(req *pbs.DeleteHostRequest) error { - return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, static.HostPrefix) + return handlers.ValidateDeleteRequest(func() map[string]string { + badFields := map[string]string{} + switch host.SubtypeFromId(req.GetId()) { + case plugin.Subtype: + badFields[globals.IdField] = "Cannot manually delete this type of host." + } + return badFields + }, req, static.HostPrefix) } func validateListRequest(req *pbs.ListHostsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(handlers.Id(req.GetHostCatalogId()), static.HostCatalogPrefix) { + if !handlers.ValidId(handlers.Id(req.GetHostCatalogId()), static.HostCatalogPrefix, plugin.HostCatalogPrefix) { badFields["host_catalog_id"] = "The field is incorrectly formatted." } if _, err := handlers.NewFilter(req.GetFilter()); err != nil { diff --git a/internal/servers/controller/handlers/hosts/host_service_test.go b/internal/servers/controller/handlers/hosts/host_service_test.go index b7cfc7a65..61b7d58fd 100644 --- a/internal/servers/controller/handlers/hosts/host_service_test.go +++ b/internal/servers/controller/handlers/hosts/host_service_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "sort" "strings" "testing" @@ -11,15 +12,20 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/boundary/internal/db" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/host/plugin" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" + hostplugin "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/hashicorp/boundary/internal/scheduler" "github.com/hashicorp/boundary/internal/servers/controller/auth" "github.com/hashicorp/boundary/internal/servers/controller/handlers" "github.com/hashicorp/boundary/internal/servers/controller/handlers/hosts" "github.com/hashicorp/boundary/internal/types/scope" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hosts" + "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/plugins" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" + plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/genproto/protobuf/field_mask" @@ -28,11 +34,12 @@ import ( "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" + "google.golang.org/protobuf/types/known/wrapperspb" ) var testAuthorizedActions = []string{"no-op", "read", "update", "delete"} -func TestGet(t *testing.T) { +func TestGet_Static(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) @@ -46,6 +53,10 @@ func TestGet(t *testing.T) { org, proj := iam.TestScopes(t, iamRepo) rw := db.New(conn) + sche := scheduler.TestScheduler(t, conn, wrapper) + pluginRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms, sche, map[string]plgpb.HostPluginServiceClient{}) + } repoFn := func() (*static.Repository, error) { return static.NewRepository(rw, rw, kms) } @@ -98,8 +109,8 @@ func TestGet(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := hosts.NewService(repoFn) - require.NoError(err, "Couldn't create a new host set service.") + s, err := hosts.NewService(repoFn, pluginRepoFn) + require.NoError(err, "Couldn't create a new host service.") got, gErr := s.GetHost(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), tc.req) if tc.err != nil { @@ -115,7 +126,106 @@ func TestGet(t *testing.T) { } } -func TestList(t *testing.T) { +func TestGet_Plugin(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + + org, proj := iam.TestScopes(t, iamRepo) + + plg := hostplugin.TestPlugin(t, conn, "test") + plgm := map[string]plgpb.HostPluginServiceClient{ + plg.GetPublicId(): plugin.NewWrappingPluginClient(&plugin.TestPluginServer{}), + } + + rw := db.New(conn) + sche := scheduler.TestScheduler(t, conn, wrapper) + pluginRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms, sche, plgm) + } + repoFn := func() (*static.Repository, error) { + return static.NewRepository(rw, rw, kms) + } + hc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId()) + h := plugin.TestHost(t, conn, hc.GetPublicId(), "test") + hs := plugin.TestSet(t, conn, kms, sche, hc, plgm) + plugin.TestSetMembers(t, conn, hs.GetPublicId(), []*plugin.Host{h}) + + pHost := &pb.Host{ + HostCatalogId: hc.GetPublicId(), + Id: h.GetPublicId(), + CreatedTime: h.CreateTime.GetTimestamp(), + UpdatedTime: h.UpdateTime.GetTimestamp(), + Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()}, + Type: plugin.Subtype.String(), + Plugin: &plugins.PluginInfo{ + Id: plg.GetPublicId(), + Name: plg.GetName(), + Description: plg.GetDescription(), + }, + HostSetIds: []string{hs.GetPublicId()}, + AuthorizedActions: testAuthorizedActions, + } + + cases := []struct { + name string + req *pbs.GetHostRequest + res *pbs.GetHostResponse + err error + }{ + { + name: "Get an Existing Host", + req: &pbs.GetHostRequest{Id: h.GetPublicId()}, + res: &pbs.GetHostResponse{Item: pHost}, + }, + { + name: "non existing", + req: &pbs.GetHostRequest{Id: plugin.HostPrefix + "_DoesntExis"}, + res: nil, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "Wrong id prefix", + req: &pbs.GetHostRequest{Id: "j_1234567890"}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "space in id", + req: &pbs.GetHostRequest{Id: plugin.HostPrefix + "_1 23456789"}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + s, err := hosts.NewService(repoFn, pluginRepoFn) + require.NoError(err, "Couldn't create a new host service.") + + got, gErr := s.GetHost(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), tc.req) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "GetHost(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + return + } + require.NoError(gErr) + + if tc.res != nil { + tc.res.Item.Version = 1 + } + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "GetHost(%q) got response %q, wanted %q", tc.req, got, tc.res) + }) + } +} + +func TestList_Static(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) kms := kms.TestKms(t, conn, wrapper) @@ -128,6 +238,10 @@ func TestList(t *testing.T) { org, proj := iam.TestScopes(t, iamRepo) rw := db.New(conn) + sche := scheduler.TestScheduler(t, conn, wrapper) + pluginRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms, sche, map[string]plgpb.HostPluginServiceClient{}) + } repoFn := func() (*static.Repository, error) { return static.NewRepository(rw, rw, kms) } @@ -185,7 +299,7 @@ func TestList(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := hosts.NewService(repoFn) + s, err := hosts.NewService(repoFn, pluginRepoFn) require.NoError(err, "Couldn't create new host set service.") // Test non-anonymous listing @@ -211,6 +325,124 @@ func TestList(t *testing.T) { } } +func TestList_Plugin(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + + org, proj := iam.TestScopes(t, iamRepo) + plg := hostplugin.TestPlugin(t, conn, "test") + plgm := map[string]plgpb.HostPluginServiceClient{ + plg.GetPublicId(): plugin.NewWrappingPluginClient(&plugin.TestPluginServer{}), + } + + rw := db.New(conn) + sche := scheduler.TestScheduler(t, conn, wrapper) + pluginRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms, sche, plgm) + } + repoFn := func() (*static.Repository, error) { + return static.NewRepository(rw, rw, kms) + } + hcs := plugin.TestCatalogs(t, conn, proj.GetPublicId(), plg.GetPublicId(), 2) + hc, hcNoHosts := hcs[0], hcs[1] + hs := plugin.TestSet(t, conn, kms, sche, hc, plgm) + + var wantHs []*pb.Host + for i := 0; i < 10; i++ { + h := plugin.TestHost(t, conn, hc.GetPublicId(), fmt.Sprintf("host %d", i)) + plugin.TestSetMembers(t, conn, hs.GetPublicId(), []*plugin.Host{h}) + wantHs = append(wantHs, &pb.Host{ + Id: h.GetPublicId(), + HostCatalogId: h.GetCatalogId(), + Plugin: &plugins.PluginInfo{ + Id: plg.GetPublicId(), + Name: plg.GetName(), + Description: plg.GetDescription(), + }, + Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()}, + CreatedTime: h.GetCreateTime().GetTimestamp(), + UpdatedTime: h.GetUpdateTime().GetTimestamp(), + HostSetIds: []string{hs.GetPublicId()}, + Version: 1, + Type: plugin.Subtype.String(), + AuthorizedActions: testAuthorizedActions, + }) + } + sort.Slice(wantHs, func(i, j int) bool { + return wantHs[i].GetId() < wantHs[j].GetId() + }) + + cases := []struct { + name string + req *pbs.ListHostsRequest + res *pbs.ListHostsResponse + err error + }{ + { + name: "List Many Hosts", + req: &pbs.ListHostsRequest{HostCatalogId: hc.GetPublicId()}, + res: &pbs.ListHostsResponse{Items: wantHs}, + }, + { + name: "List No Hosts", + req: &pbs.ListHostsRequest{HostCatalogId: hcNoHosts.GetPublicId()}, + res: &pbs.ListHostsResponse{}, + }, + { + name: "Filter to One Host", + req: &pbs.ListHostsRequest{HostCatalogId: hc.GetPublicId(), Filter: fmt.Sprintf(`"/item/id"==%q`, wantHs[1].GetId())}, + res: &pbs.ListHostsResponse{Items: wantHs[1:2]}, + }, + { + name: "Filter to No Hosts", + req: &pbs.ListHostsRequest{HostCatalogId: hc.GetPublicId(), Filter: `"/item/id"=="doesnt match"`}, + res: &pbs.ListHostsResponse{}, + }, + { + name: "Filter Bad Format", + req: &pbs.ListHostsRequest{HostCatalogId: hc.GetPublicId(), Filter: `"//id/"=="bad"`}, + err: handlers.InvalidArgumentErrorf("bad format", nil), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + s, err := hosts.NewService(repoFn, pluginRepoFn) + require.NoError(err, "Couldn't create new host set service.") + + // Test non-anonymous listing + got, gErr := s.ListHosts(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), tc.req) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "ListHosts(%q) got error %v, wanted %v", tc.req, gErr, tc.err) + return + } + require.NoError(gErr) + + sort.Slice(got.Items, func(i, j int) bool { + return got.Items[i].GetId() < got.Items[j].GetId() + }) + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListHosts(%q) got response %q, wanted %q", tc.req, got, tc.res) + + // Test anonymous listing + got, gErr = s.ListHosts(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId(), auth.WithUserId(auth.AnonymousUserId)), tc.req) + require.NoError(gErr) + assert.Len(got.Items, len(tc.res.Items)) + for _, item := range got.GetItems() { + require.Nil(item.CreatedTime) + require.Nil(item.UpdatedTime) + require.Zero(item.Version) + } + }) + } +} + func TestDelete(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") @@ -225,13 +457,21 @@ func TestDelete(t *testing.T) { _, proj := iam.TestScopes(t, iamRepo) rw := db.New(conn) + sche := scheduler.TestScheduler(t, conn, wrapper) + pluginRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms, sche, map[string]plgpb.HostPluginServiceClient{}) + } repoFn := func() (*static.Repository, error) { return static.NewRepository(rw, rw, kms) } hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] - s, err := hosts.NewService(repoFn) + plg := hostplugin.TestPlugin(t, conn, "test") + pluginHc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId()) + pluginH := plugin.TestHost(t, conn, pluginHc.GetPublicId(), "test") + + s, err := hosts.NewService(repoFn, pluginRepoFn) require.NoError(t, err, "Couldn't create a new host set service.") cases := []struct { @@ -248,6 +488,14 @@ func TestDelete(t *testing.T) { Id: h.GetPublicId(), }, }, + { + name: "Delete a plugin Host", + scopeId: proj.GetPublicId(), + req: &pbs.DeleteHostRequest{ + Id: pluginH.GetPublicId(), + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, { name: "Delete bad id Host", scopeId: proj.GetPublicId(), @@ -293,13 +541,17 @@ func TestDelete_twice(t *testing.T) { _, proj := iam.TestScopes(t, iamRepo) rw := db.New(conn) + sche := scheduler.TestScheduler(t, conn, wrapper) + pluginRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms, sche, map[string]plgpb.HostPluginServiceClient{}) + } repoFn := func() (*static.Repository, error) { return static.NewRepository(rw, rw, kms) } hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] h := static.TestHosts(t, conn, hc.GetPublicId(), 1)[0] - s, err := hosts.NewService(repoFn) + s, err := hosts.NewService(repoFn, pluginRepoFn) require.NoError(err, "Couldn't create a new host set service.") req := &pbs.DeleteHostRequest{ Id: h.GetPublicId(), @@ -326,11 +578,18 @@ func TestCreate(t *testing.T) { org, proj := iam.TestScopes(t, iamRepo) rw := db.New(conn) + sche := scheduler.TestScheduler(t, conn, wrapper) + pluginRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms, sche, map[string]plgpb.HostPluginServiceClient{}) + } repoFn := func() (*static.Repository, error) { return static.NewRepository(rw, rw, kms) } hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0] + plg := hostplugin.TestPlugin(t, conn, "test") + pluginHc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId()) + defaultHcCreated := hc.GetCreateTime().GetTimestamp().AsTime() cases := []struct { @@ -365,6 +624,14 @@ func TestCreate(t *testing.T) { }, }, }, + { + name: "Create a plugin Host", + req: &pbs.CreateHostRequest{Item: &pb.Host{ + HostCatalogId: pluginHc.GetPublicId(), + Type: "plugin", + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, { name: "Create with empty address", req: &pbs.CreateHostRequest{Item: &pb.Host{ @@ -454,7 +721,7 @@ func TestCreate(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := hosts.NewService(repoFn) + s, err := hosts.NewService(repoFn, pluginRepoFn) require.NoError(err, "Failed to create a new host set service.") got, gErr := s.CreateHost(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), tc.req) @@ -498,6 +765,10 @@ func TestUpdate(t *testing.T) { org, proj := iam.TestScopes(t, iamRepo) rw := db.New(conn) + sche := scheduler.TestScheduler(t, conn, wrapper) + pluginRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms, sche, map[string]plgpb.HostPluginServiceClient{}) + } repoFn := func() (*static.Repository, error) { return static.NewRepository(rw, rw, kms) } @@ -525,7 +796,7 @@ func TestUpdate(t *testing.T) { Id: h.GetPublicId(), } - tested, err := hosts.NewService(repoFn) + tested, err := hosts.NewService(repoFn, pluginRepoFn) require.NoError(t, err, "Failed to create a new host set service.") cases := []struct { @@ -887,3 +1158,42 @@ func TestUpdate(t *testing.T) { }) } } + +func TestUpdate_Plugin(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + + iamRepo := iam.TestRepo(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iamRepo, nil + } + + _, proj := iam.TestScopes(t, iamRepo) + + rw := db.New(conn) + sche := scheduler.TestScheduler(t, conn, wrapper) + pluginRepoFn := func() (*plugin.Repository, error) { + return plugin.NewRepository(rw, rw, kms, sche, map[string]plgpb.HostPluginServiceClient{}) + } + repoFn := func() (*static.Repository, error) { + return static.NewRepository(rw, rw, kms) + } + + plg := hostplugin.TestPlugin(t, conn, "test") + hc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId()) + h := plugin.TestHost(t, conn, hc.GetPublicId(), "test") + + tested, err := hosts.NewService(repoFn, pluginRepoFn) + require.NoError(t, err) + + got, err := tested.UpdateHost(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), &pbs.UpdateHostRequest{ + Id: h.GetPublicId(), + Item: &pb.Host{Name: wrapperspb.String("foo")}, + UpdateMask: &field_mask.FieldMask{Paths: []string{"name"}}, + }) + assert.Nil(t, got) + require.Error(t, err) + assert.True(t, errors.Is(err, handlers.ApiErrorWithCode(codes.InvalidArgument))) +} diff --git a/internal/servers/controller/testing.go b/internal/servers/controller/testing.go index 11691ad98..1a20eb842 100644 --- a/internal/servers/controller/testing.go +++ b/internal/servers/controller/testing.go @@ -385,6 +385,10 @@ type TestControllerOpts struct { // The amount of time to wait before marking connections as closed when a // worker has not reported in StatusGracePeriodDuration time.Duration + + // The amount of time between the scheduler waking up to run it's registered + // jobs. + SchedulerRunJobInterval time.Duration } func NewTestController(t *testing.T, opts *TestControllerOpts) *TestController { @@ -478,6 +482,7 @@ func NewTestController(t *testing.T, opts *TestControllerOpts) *TestController { t.Fatal(err) } } + opts.Config.Controller.SchedulerRunJobInterval = opts.SchedulerRunJobInterval if err := tc.b.SetupEventing(tc.b.Logger, tc.b.StderrLock, opts.Config.Controller.Name, base.WithEventerConfig(opts.Config.Eventing)); err != nil { t.Fatal(err) diff --git a/internal/tests/api/hostcatalogs/host_catalog_test.go b/internal/tests/api/hostcatalogs/host_catalog_test.go index 12fd5679c..5e60d3eeb 100644 --- a/internal/tests/api/hostcatalogs/host_catalog_test.go +++ b/internal/tests/api/hostcatalogs/host_catalog_test.go @@ -59,6 +59,52 @@ func TestList(t *testing.T) { assert.Equal(filterItem.Id, ul.Items[0].Id) } +func TestList_Plugin(t *testing.T) { + assert, require := assert.New(t), require.New(t) + tc := controller.NewTestController(t, nil) + defer tc.Shutdown() + + client := tc.Client() + token := tc.Token() + client.SetToken(token.Token) + _, proj := iam.TestScopes(t, tc.IamRepo(), iam.WithUserId(token.UserId)) + catalogClient := hostcatalogs.NewClient(client) + + ul, err := catalogClient.List(tc.Context(), proj.GetPublicId()) + require.NoError(err) + assert.Empty(ul.Items) + + var expected []*hostcatalogs.HostCatalog + for i := 0; i < 10; i++ { + expected = append(expected, &hostcatalogs.HostCatalog{Name: fmt.Sprint(i)}) + } + + cr, err := catalogClient.Create(tc.Context(), "plugin", proj.GetPublicId(), + hostcatalogs.WithName(expected[0].Name), hostcatalogs.WithPluginId("pl_1234567890")) + require.NoError(err) + expected[0] = cr.Item + + ul, err = catalogClient.List(tc.Context(), proj.GetPublicId()) + require.NoError(err) + assert.ElementsMatch(comparableCatalogSlice(expected[:1]), comparableCatalogSlice(ul.Items)) + + for i := 1; i < 10; i++ { + cr, err = catalogClient.Create(tc.Context(), "static", proj.GetPublicId(), hostcatalogs.WithName(expected[i].Name)) + require.NoError(err) + expected[i] = cr.Item + } + ul, err = catalogClient.List(tc.Context(), proj.GetPublicId()) + require.NoError(err) + assert.ElementsMatch(comparableCatalogSlice(expected), comparableCatalogSlice(ul.Items)) + + filterItem := ul.Items[3] + ul, err = catalogClient.List(tc.Context(), proj.GetPublicId(), + hostcatalogs.WithFilter(fmt.Sprintf(`"/item/id"==%q`, filterItem.Id))) + require.NoError(err) + assert.Len(ul.Items, 1) + assert.Equal(filterItem.Id, ul.Items[0].Id) +} + func comparableCatalogSlice(in []*hostcatalogs.HostCatalog) []hostcatalogs.HostCatalog { var filtered []hostcatalogs.HostCatalog for _, i := range in { diff --git a/internal/tests/api/hosts/host_test.go b/internal/tests/api/hosts/host_test.go index 85d8ac0b0..0c4391758 100644 --- a/internal/tests/api/hosts/host_test.go +++ b/internal/tests/api/hosts/host_test.go @@ -4,10 +4,12 @@ import ( "fmt" "net/http" "testing" + "time" "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/hostcatalogs" "github.com/hashicorp/boundary/api/hosts" + "github.com/hashicorp/boundary/api/hostsets" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/servers/controller" @@ -80,6 +82,53 @@ func comparableHostSlice(in []*hosts.Host) []hosts.Host { return filtered } +func TestPluginHosts(t *testing.T) { + assert, require := assert.New(t), require.New(t) + tc := controller.NewTestController(t, &controller.TestControllerOpts{SchedulerRunJobInterval: 100 * time.Millisecond}) + defer tc.Shutdown() + + client := tc.Client() + token := tc.Token() + client.SetToken(token.Token) + _, proj := iam.TestScopes(t, tc.IamRepo(), iam.WithUserId(token.UserId)) + + hc, err := hostcatalogs.NewClient(client).Create(tc.Context(), "plugin", proj.GetPublicId(), + hostcatalogs.WithPluginId("pl_1234567890")) + require.NotNil(hc) + + hset, err := hostsets.NewClient(client).Create(tc.Context(), hc.Item.Id, hostsets.WithAttributes(map[string]interface{}{ + "host_info": []interface{}{ + map[string]interface{}{ + "external_id": "test1", + "ip_addresses": []string{"10.0.0.1", "192.168.1.1"}, + "dns_names": []string{"foo.hashicorp.com", "boundaryproject.io"}, + }, + map[string]interface{}{ + "external_id": "test2", + "ip_addresses": []string{"10.0.0.2", "192.168.1.2"}, + "dns_names": []string{"foo2.hashicorp.com", "boundaryproject2.io"}, + }, + }, + })) + require.NoError(err) + require.NotNil(hset) + time.Sleep(1*time.Second) + + hClient := hosts.NewClient(client) + hl, err := hClient.List(tc.Context(), hc.Item.Id) + require.NoError(err) + require.Len(hl.Items, 2) + + h, err := hClient.Read(tc.Context(), hl.Items[0].Id) + require.NoError(err) + assert.Equal(hl.Items[0], h.Item) + + _, err = hClient.Update(tc.Context(), h.Item.Id, h.Item.Version, hosts.WithName("foo")) + require.Error(err) + _, err = hClient.Delete(tc.Context(), h.Item.Id) + require.Error(err) +} + func TestCrud(t *testing.T) { assert, require := assert.New(t), require.New(t) tc := controller.NewTestController(t, nil) diff --git a/internal/tests/api/hostsets/host_set_test.go b/internal/tests/api/hostsets/host_set_test.go index eb78af142..3316e09f3 100644 --- a/internal/tests/api/hostsets/host_set_test.go +++ b/internal/tests/api/hostsets/host_set_test.go @@ -102,6 +102,57 @@ func TestList(t *testing.T) { assert.Equal(filterItem.Id, ul.Items[0].Id) } +func TestList_Plugin(t *testing.T) { + assert, require := assert.New(t), require.New(t) + tc := controller.NewTestController(t, nil) + defer tc.Shutdown() + + client := tc.Client() + token := tc.Token() + client.SetToken(token.Token) + _, proj := iam.TestScopes(t, tc.IamRepo(), iam.WithUserId(token.UserId)) + + hc, err := hostcatalogs.NewClient(client).Create(tc.Context(), "plugin", proj.GetPublicId(), + hostcatalogs.WithPluginId("pl_1234567890")) + require.NoError(err) + require.NotNil(hc) + + hClient := hostsets.NewClient(client) + + ul, err := hClient.List(tc.Context(), hc.Item.Id) + require.NoError(err) + assert.Empty(ul.Items) + + var expected []*hostsets.HostSet + for i := 0; i < 10; i++ { + expected = append(expected, &hostsets.HostSet{Name: fmt.Sprint(i)}) + } + + hcr, err := hClient.Create(tc.Context(), hc.Item.Id, hostsets.WithName(expected[0].Name)) + require.NoError(err) + expected[0] = hcr.Item + + ul, err = hClient.List(tc.Context(), hc.Item.Id) + require.NoError(err) + assert.ElementsMatch(comparableSetSlice(expected[:1]), comparableSetSlice(ul.Items)) + + for i := 1; i < 10; i++ { + hcr, err = hClient.Create(tc.Context(), hc.Item.Id, hostsets.WithName(expected[i].Name)) + require.NoError(err) + expected[i] = hcr.Item + } + ul, err = hClient.List(tc.Context(), hc.Item.Id) + require.NoError(err) + assert.ElementsMatch(comparableSetSlice(expected), comparableSetSlice(ul.Items)) + + filterItem := ul.Items[3] + ul, err = hClient.List(tc.Context(), hc.Item.Id, + hostsets.WithFilter(fmt.Sprintf(`"/item/id"==%q`, filterItem.Id))) + require.NoError(err) + assert.Len(ul.Items, 1) + assert.Equal(filterItem.Id, ul.Items[0].Id) +} + func comparableSetSlice(in []*hostsets.HostSet) []hostsets.HostSet { var filtered []hostsets.HostSet for _, i := range in { diff --git a/sdk/pbs/controller/api/resources/hosts/host.pb.go b/sdk/pbs/controller/api/resources/hosts/host.pb.go index f568bde37..e0dec3b5c 100644 --- a/sdk/pbs/controller/api/resources/hosts/host.pb.go +++ b/sdk/pbs/controller/api/resources/hosts/host.pb.go @@ -57,6 +57,10 @@ type Host struct { HostSetIds []string `protobuf:"bytes,100,rep,name=host_set_ids,proto3" json:"host_set_ids,omitempty"` // The attributes that are applicable to the specific Host type. Attributes *structpb.Struct `protobuf:"bytes,110,opt,name=attributes,proto3" json:"attributes,omitempty"` + // Output only. The list of ip addresses associated with this host. + IpAddresses []string `protobuf:"bytes,120,rep,name=ip_addresses,json=ipAddresses,proto3" json:"ip_addresses,omitempty"` + // Output only. The list of dns addresses associated with this host. + DnsNames []string `protobuf:"bytes,130,rep,name=dns_names,json=dnsNames,proto3" json:"dns_names,omitempty"` // Output only. The available actions on this resource for this user. AuthorizedActions []string `protobuf:"bytes,300,rep,name=authorized_actions,proto3" json:"authorized_actions,omitempty"` } @@ -177,6 +181,20 @@ func (x *Host) GetAttributes() *structpb.Struct { return nil } +func (x *Host) GetIpAddresses() []string { + if x != nil { + return x.IpAddresses + } + return nil +} + +func (x *Host) GetDnsNames() []string { + if x != nil { + return x.DnsNames + } + return nil +} + func (x *Host) GetAuthorizedActions() []string { if x != nil { return x.AuthorizedActions @@ -254,7 +272,7 @@ var file_controller_api_resources_hosts_v1_host_proto_rawDesc = []byte{ 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2a, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, - 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xbc, + 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xfd, 0x05, 0x0a, 0x04, 0x48, 0x6f, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x28, 0x0a, 0x0f, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, @@ -295,23 +313,27 @@ var file_controller_api_resources_hosts_v1_host_proto_rawDesc = []byte{ 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, - 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x2f, 0x0a, 0x12, - 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x75, 0x0a, - 0x14, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x48, 0x6f, 0x73, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x5d, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x42, 0x25, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1d, 0x0a, 0x12, - 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x61, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x12, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x07, 0x61, 0x64, 0x64, - 0x72, 0x65, 0x73, 0x73, 0x42, 0x4c, 0x5a, 0x4a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, - 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x3b, 0x68, 0x6f, 0x73, - 0x74, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x21, 0x0a, 0x0c, + 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x78, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0b, 0x69, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, + 0x1c, 0x0a, 0x09, 0x64, 0x6e, 0x73, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x82, 0x01, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x08, 0x64, 0x6e, 0x73, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x2f, 0x0a, + 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, + 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x75, + 0x0a, 0x14, 0x53, 0x74, 0x61, 0x74, 0x69, 0x63, 0x48, 0x6f, 0x73, 0x74, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x5d, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x25, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1d, 0x0a, + 0x12, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x61, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x12, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x07, 0x61, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x42, 0x4c, 0x5a, 0x4a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x73, 0x3b, 0x68, 0x6f, + 0x73, 0x74, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var (