Read and List Plugin based Hosts (#1655)

* Add ability to read and list hosts.  Tests to follow.

* Add tests for reading and listing as well as validation checks to not modify hosts.

* Add IpAddress and DnsNames to the api resource.  Populate the plugin info.

* Adding go sdk tests for reading and listing hosts and ensuring update and deleting is disallowed.
This commit is contained in:
Todd authored and GitHub committed 2021-11-02 14:06:20 -07:00
1 parent 1312628a1e
commit 808b986834
19 files changed
+803 -107

No files matched your search

+2
View File
@@ -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
+2
View File
@@ -62,4 +62,6 @@ const (
PluginIdField = "plugin_id"
PluginField = "plugin"
PluginNameField = "plugin_name"
IpAddressesField = "ip_addresses"
DnsNamesField = "dns_names"
)
+6
View File
@@ -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) {
+16
View File
@@ -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": {
+4 -4
View File
@@ -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)
}
+1 -1
View File
@@ -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)
}
}
+22 -11
View File
@@ -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.
+1 -1
View File
@@ -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 {
+20 -2
View File
@@ -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(&timestamp.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(&timestamp.Timestamp{}),
),
)
assert.Empty(
cmp.Diff(
plg,
gotPlg,
cmpopts.IgnoreUnexported(hostplg.Plugin{}, hostplgstore.Plugin{}),
cmpopts.IgnoreTypes(&timestamp.Timestamp{}),
),
)
}
for setId, expHostIds := range setIdMap {
got, err = repo.ListHostsBySetIds(ctx, []string{setId})
@@ -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"];
}
+5 -1
View File
@@ -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)
}
+1 -1
View File
@@ -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)
}
@@ -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 {
@@ -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)))
}
+5
View File
@@ -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)
@@ -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 {
+49
View File
@@ -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)
@@ -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 {
@@ -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 (