Create Plugin Host Set Handler for Create, List, Get (#1518)

This commit is contained in:
Todd Knight authored and GitHub committed 2021-09-13 15:02:04 -07:00
1 parent e208800f77
commit 715982148f
5 files changed
+734 -102

No files matched your search

@@ -4,6 +4,7 @@ import (
"github.com/hashicorp/boundary/internal/auth/oidc"
"github.com/hashicorp/boundary/internal/auth/password"
"github.com/hashicorp/boundary/internal/credential/vault"
pluginhost "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/servers"
@@ -19,6 +20,7 @@ type (
PasswordAuthRepoFactory func() (*password.Repository, error)
ServersRepoFactory func() (*servers.Repository, error)
StaticRepoFactory func() (*static.Repository, error)
PluginHostRepoFactory func() (*pluginhost.Repository, error)
SessionRepoFactory func() (*session.Repository, error)
TargetRepoFactory func() (*target.Repository, error)
)
@@ -12,6 +12,7 @@ import (
"github.com/hashicorp/boundary/internal/cmd/config"
"github.com/hashicorp/boundary/internal/credential/vault"
"github.com/hashicorp/boundary/internal/db"
"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"
@@ -53,6 +54,7 @@ type Controller struct {
ServersRepoFn common.ServersRepoFactory
SessionRepoFn common.SessionRepoFactory
StaticHostRepoFn common.StaticRepoFactory
PluginHostRepoFn common.PluginHostRepoFactory
TargetRepoFn common.TargetRepoFactory
scheduler *scheduler.Scheduler
@@ -131,6 +133,9 @@ func New(ctx context.Context, conf *Config) (*Controller, error) {
c.StaticHostRepoFn = func() (*static.Repository, error) {
return static.NewRepository(dbase, dbase, c.kms)
}
c.PluginHostRepoFn = func() (*plugin.Repository, error) {
return plugin.NewRepository(dbase, dbase, c.kms)
}
c.AuthTokenRepoFn = func() (*authtoken.Repository, error) {
return authtoken.NewRepository(dbase, dbase, c.kms,
authtoken.WithTokenTimeToLiveDuration(c.conf.RawConfig.Controller.AuthTokenTimeToLiveDuration),
+1 -1
View File
@@ -92,7 +92,7 @@ func handleGrpcGateway(c *Controller, props HandlerProperties) (http.Handler, er
if err := services.RegisterHostCatalogServiceHandlerServer(ctx, mux, hcs); err != nil {
return nil, fmt.Errorf("failed to register host catalog service handler: %w", err)
}
hss, err := host_sets.NewService(c.StaticHostRepoFn)
hss, err := host_sets.NewService(c.StaticHostRepoFn, c.PluginHostRepoFn)
if err != nil {
return nil, fmt.Errorf("failed to create host set handler service: %w", err)
}
@@ -2,12 +2,15 @@ package host_sets
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/hashicorp/boundary/globals"
"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"
@@ -20,6 +23,7 @@ import (
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
"github.com/hashicorp/go-secure-stdlib/strutil"
"google.golang.org/grpc/codes"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
@@ -57,18 +61,22 @@ type Service struct {
pbs.UnimplementedHostSetServiceServer
staticRepoFn common.StaticRepoFactory
pluginRepoFn common.PluginHostRepoFactory
}
var _ pbs.HostSetServiceServer = Service{}
// NewService returns a host set Service which handles host set related requests to boundary and uses the provided
// repositories for storage and retrieval.
func NewService(repoFn common.StaticRepoFactory) (Service, error) {
func NewService(staticRepoFn common.StaticRepoFactory, pluginRepoFn common.PluginHostRepoFactory) (Service, error) {
const op = "host_sets.NewService"
if repoFn == nil {
if staticRepoFn == 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 repository")
}
return Service{staticRepoFn: staticRepoFn, pluginRepoFn: pluginRepoFn}, nil
}
func (s Service) ListHostSets(ctx context.Context, req *pbs.ListHostSetsRequest) (*pbs.ListHostSetsResponse, error) {
@@ -217,7 +225,7 @@ func (s Service) UpdateHostSet(ctx context.Context, req *pbs.UpdateHostSetReques
if authResults.Error != nil {
return nil, authResults.Error
}
hs, hosts, err := s.updateInRepo(ctx, authResults.Scope.GetId(), cat.GetPublicId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem())
hs, hosts, err := s.updateInRepo(ctx, authResults.Scope.GetId(), cat.GetPublicId(), req)
if err != nil {
return nil, err
}
@@ -373,63 +381,96 @@ func (s Service) RemoveHostSetHosts(ctx context.Context, req *pbs.RemoveHostSetH
return &pbs.RemoveHostSetHostsResponse{Item: item}, nil
}
func (s Service) getFromRepo(ctx context.Context, id string) (*static.HostSet, []*static.Host, error) {
repo, err := s.staticRepoFn()
if err != nil {
return nil, nil, err
func (s Service) getFromRepo(ctx context.Context, id string) (host.Set, []host.Host, error) {
var hs host.Set
var hl []host.Host
switch host.SubtypeFromId(id) {
case static.Subtype:
repo, err := s.staticRepoFn()
if err != nil {
return nil, nil, err
}
hset, hosts, err := repo.LookupSet(ctx, id)
if err != nil {
return nil, nil, err
}
if hset == nil {
return nil, nil, handlers.NotFoundErrorf("Host Set %q doesn't exist.", id)
}
for _, h := range hosts {
hl = append(hl, h)
}
hs = hset
case plugin.Subtype:
repo, err := s.pluginRepoFn()
if err != nil {
return nil, nil, err
}
hset, err := repo.LookupSet(ctx, id)
if err != nil {
return nil, nil, err
}
if hset == nil {
return nil, nil, handlers.NotFoundErrorf("Host Set %q doesn't exist.", id)
}
hs = hset
}
h, m, err := repo.LookupSet(ctx, id)
if err != nil {
return nil, nil, err
}
if h == nil {
return nil, nil, handlers.NotFoundErrorf("Host Set %q doesn't exist.", id)
}
return h, m, nil
return hs, hl, nil
}
func (s Service) createInRepo(ctx context.Context, scopeId, catalogId string, item *pb.HostSet) (*static.HostSet, error) {
func (s Service) createInRepo(ctx context.Context, scopeId, catalogId string, item *pb.HostSet) (host.Set, error) {
const op = "host_sets.(Service).createInRepo"
var opts []static.Option
if item.GetName() != nil {
opts = append(opts, static.WithName(item.GetName().GetValue()))
if item == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing item")
}
if item.GetDescription() != nil {
opts = append(opts, static.WithDescription(item.GetDescription().GetValue()))
var hSet host.Set
switch host.SubtypeFromId(catalogId) {
case static.Subtype:
h, err := toStorageStaticSet(ctx, catalogId, item)
if err != nil {
return nil, err
}
repo, err := s.staticRepoFn()
if err != nil {
return nil, err
}
out, err := repo.CreateSet(ctx, scopeId, h)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create host set"))
}
if out == nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create host set but no error returned from repository.")
}
hSet = out
case plugin.Subtype:
h, err := toStoragePluginSet(ctx, catalogId, item)
repo, err := s.pluginRepoFn()
if err != nil {
return nil, err
}
out, err := repo.CreateSet(ctx, scopeId, h)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create host set"))
}
if out == nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create host set but no error returned from repository.")
}
hSet = out
default:
return nil, errors.New(ctx, errors.InvalidParameter, op, "unrecognized catalog type")
}
h, err := static.NewHostSet(catalogId, opts...)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("Unable to build host set for creation"))
}
repo, err := s.staticRepoFn()
if err != nil {
return nil, err
}
out, err := repo.CreateSet(ctx, scopeId, h)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create host set"))
}
if out == nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create host set but no error returned from repository.")
}
return out, nil
return hSet, nil
}
func (s Service) updateInRepo(ctx context.Context, scopeId, catalogId, id string, mask []string, item *pb.HostSet) (*static.HostSet, []*static.Host, error) {
func (s Service) updateInRepo(ctx context.Context, scopeId, catalogId string, req *pbs.UpdateHostSetRequest) (host.Set, []host.Host, error) {
const op = "host_sets.(Service).updateInRepo"
var opts []static.Option
if desc := item.GetDescription(); desc != nil {
opts = append(opts, static.WithDescription(desc.GetValue()))
}
if name := item.GetName(); name != nil {
opts = append(opts, static.WithName(name.GetValue()))
}
h, err := static.NewHostSet(catalogId, opts...)
item := req.GetItem()
h, err := toStorageStaticSet(ctx, catalogId, item)
if err != nil {
return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("Unable to build host set for update"))
}
h.PublicId = id
dbMask := maskManager.Translate(mask)
h.PublicId = req.GetId()
dbMask := maskManager.Translate(req.GetUpdateMask().GetPaths())
if len(dbMask) == 0 {
return nil, nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."})
}
@@ -442,9 +483,13 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, catalogId, id string
return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update host set"))
}
if rowsUpdated == 0 {
return nil, nil, handlers.NotFoundErrorf("Host Set %q doesn't exist or incorrect version provided.", id)
return nil, nil, handlers.NotFoundErrorf("Host Set %q doesn't exist or incorrect version provided.", req.GetId())
}
return out, m, nil
var hl []host.Host
for _, h := range m {
hl = append(hl, h)
}
return out, hl, nil
}
func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, error) {
@@ -460,19 +505,39 @@ 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.HostSet, error) {
repo, err := s.staticRepoFn()
if err != nil {
return nil, err
func (s Service) listFromRepo(ctx context.Context, catalogId string) ([]host.Set, error) {
const op = "host_sets.(Service).listFromRepo"
var sets []host.Set
switch host.SubtypeFromId(catalogId) {
case static.Subtype:
repo, err := s.staticRepoFn()
if err != nil {
return nil, err
}
sl, err := repo.ListSets(ctx, catalogId)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
for _, a := range sl {
sets = append(sets, a)
}
case plugin.Subtype:
repo, err := s.pluginRepoFn()
if err != nil {
return nil, err
}
sl, err := repo.ListSets(ctx, catalogId)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
for _, a := range sl {
sets = append(sets, a)
}
}
hl, err := repo.ListSets(ctx, catalogId)
if err != nil {
return nil, err
}
return hl, nil
return sets, nil
}
func (s Service) addInRepo(ctx context.Context, scopeId, setId string, hostIds []string, version uint32) (*static.HostSet, []*static.Host, error) {
func (s Service) addInRepo(ctx context.Context, scopeId, setId string, hostIds []string, version uint32) (*static.HostSet, []host.Host, error) {
const op = "host_sets.(Service).addInRepo"
repo, err := s.staticRepoFn()
if err != nil {
@@ -489,10 +554,14 @@ func (s Service) addInRepo(ctx context.Context, scopeId, setId string, hostIds [
if out == nil {
return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup host set after adding hosts to it.")
}
return out, m, nil
var hl []host.Host
for _, h := range m {
hl = append(hl, h)
}
return out, hl, nil
}
func (s Service) setInRepo(ctx context.Context, scopeId, setId string, hostIds []string, version uint32) (*static.HostSet, []*static.Host, error) {
func (s Service) setInRepo(ctx context.Context, scopeId, setId string, hostIds []string, version uint32) (*static.HostSet, []host.Host, error) {
const op = "host_sets.(Service).setInRepo"
repo, err := s.staticRepoFn()
if err != nil {
@@ -510,10 +579,14 @@ func (s Service) setInRepo(ctx context.Context, scopeId, setId string, hostIds [
if out == nil {
return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup host set after setting hosts for it.")
}
return out, m, nil
var hl []host.Host
for _, h := range m {
hl = append(hl, h)
}
return out, hl, nil
}
func (s Service) removeInRepo(ctx context.Context, scopeId, setId string, hostIds []string, version uint32) (*static.HostSet, []*static.Host, error) {
func (s Service) removeInRepo(ctx context.Context, scopeId, setId string, hostIds []string, version uint32) (*static.HostSet, []host.Host, error) {
const op = "host_sets.(Service).removeInRepo"
repo, err := s.staticRepoFn()
if err != nil {
@@ -530,12 +603,22 @@ func (s Service) removeInRepo(ctx context.Context, scopeId, setId string, hostId
if out == nil {
return nil, nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to lookup host set after removing hosts from it.")
}
return out, m, nil
var hl []host.Host
for _, h := range m {
hl = append(hl, h)
}
return out, hl, 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
@@ -547,33 +630,66 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty
case action.List, action.Create:
parentId = id
default:
set, _, err := repo.LookupSet(ctx, id)
if err != nil {
res.Error = err
return nil, res
}
if set == nil {
res.Error = handlers.NotFoundError()
return nil, res
var set host.Set
switch host.SubtypeFromId(id) {
case static.Subtype:
ss, _, err := staticRepo.LookupSet(ctx, id)
if err != nil {
res.Error = err
return nil, res
}
if ss == nil {
res.Error = handlers.NotFoundError()
return nil, res
}
set = ss
case plugin.Subtype:
ps, err := pluginRepo.LookupSet(ctx, id)
if err != nil {
res.Error = err
return nil, res
}
if ps == nil {
res.Error = handlers.NotFoundError()
return nil, res
}
set = ps
}
parentId = set.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
var cat host.Catalog
switch host.SubtypeFromId(id) {
case static.Subtype:
cs, err := staticRepo.LookupCatalog(ctx, parentId)
if err != nil {
res.Error = err
return nil, res
}
if cs == nil {
res.Error = handlers.NotFoundError()
return nil, res
}
cat = cs
case plugin.Subtype:
pc, err := pluginRepo.LookupCatalog(ctx, parentId)
if err != nil {
res.Error = err
return nil, res
}
if pc == nil {
res.Error = handlers.NotFoundError()
return nil, res
}
cat = pc
}
opts = append(opts, auth.WithScopeId(cat.GetScopeId()), auth.WithPin(parentId))
return cat, auth.Verify(ctx, opts...)
}
func toProto(ctx context.Context, in *static.HostSet, hosts []*static.Host, opt ...handlers.Option) (*pb.HostSet, error) {
func toProto(ctx context.Context, in host.Set, hosts []host.Host, opt ...handlers.Option) (*pb.HostSet, error) {
const op = "host_set_service.toProto"
opts := handlers.GetOpts(opt...)
if opts.WithOutputFields == nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building hostset proto")
@@ -588,7 +704,16 @@ func toProto(ctx context.Context, in *static.HostSet, hosts []*static.Host, opt
out.HostCatalogId = in.GetCatalogId()
}
if outputFields.Has(globals.TypeField) {
out.Type = static.Subtype.String()
switch host.SubtypeFromId(in.GetPublicId()) {
case static.Subtype:
out.Type = static.Subtype.String()
case plugin.Subtype:
out.Type = "plugin"
idParts := strings.Split(in.GetPublicId(), "_")
if len(idParts) > 2 {
out.Type = idParts[1]
}
}
}
if outputFields.Has(globals.DescriptionField) && in.GetDescription() != "" {
out.Description = &wrapperspb.StringValue{Value: in.GetDescription()}
@@ -616,9 +741,62 @@ func toProto(ctx context.Context, in *static.HostSet, hosts []*static.Host, opt
out.HostIds = append(out.HostIds, h.GetPublicId())
}
}
if outputFields.Has(globals.AttributesField) {
switch h := in.(type) {
case *plugin.HostSet:
attrs := map[string]interface{}{}
err := json.Unmarshal(h.Attributes, &attrs)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if len(attrs) > 0 {
out.Attributes, err = structpb.NewStruct(attrs)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
}
}
}
return &out, nil
}
func toStorageStaticSet(ctx context.Context, catalogId string, item *pb.HostSet) (*static.HostSet, error) {
const op = "host_set_service.toStorageStaticSet"
var opts []static.Option
if item.GetName() != nil {
opts = append(opts, static.WithName(item.GetName().GetValue()))
}
if item.GetDescription() != nil {
opts = append(opts, static.WithDescription(item.GetDescription().GetValue()))
}
hs, err := static.NewHostSet(catalogId, opts...)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("Unable to build host set for creation"))
}
return hs, nil
}
func toStoragePluginSet(ctx context.Context, catalogId string, item *pb.HostSet) (*plugin.HostSet, error) {
const op = "host_set_service.toStoragePluginSet"
var opts []plugin.Option
if item.GetName() != nil {
opts = append(opts, plugin.WithName(item.GetName().GetValue()))
}
if item.GetDescription() != nil {
opts = append(opts, plugin.WithDescription(item.GetDescription().GetValue()))
}
if item.GetAttributes() != nil {
opts = append(opts, plugin.WithAttributes(item.GetAttributes().AsMap()))
}
hs, err := plugin.NewHostSet(ctx, catalogId, opts...)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("Unable to build host set for creation"))
}
return hs, nil
}
// A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce
// requirements on the structure of the request. They verify that:
// * The path passed in is correctly formatted
@@ -627,13 +805,25 @@ func toProto(ctx context.Context, in *static.HostSet, hosts []*static.Host, opt
// * The type asserted by the ID and/or field is known
// * If relevant, the type derived from the id prefix matches what is claimed by the type field
func validateGetRequest(req *pbs.GetHostSetRequest) error {
return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, static.HostSetPrefix)
pluginPrefix := plugin.HostSetPrefix
idParts := strings.Split(req.GetId(), "_")
if len(idParts) > 2 && idParts[0] == plugin.HostSetPrefix {
pluginPrefix = strings.Join(idParts[:2], "_")
}
return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, static.HostSetPrefix, pluginPrefix)
}
func validateCreateRequest(req *pbs.CreateHostSetRequest) error {
return handlers.ValidateCreateRequest(req.GetItem(), func() map[string]string {
badFields := map[string]string{}
if !handlers.ValidId(handlers.Id(req.GetItem().GetHostCatalogId()), static.HostCatalogPrefix) {
pluginPrefix := plugin.HostCatalogPrefix
idParts := strings.Split(req.GetItem().GetHostCatalogId(), "_")
if len(idParts) > 2 && idParts[0] == plugin.HostCatalogPrefix {
pluginPrefix = strings.Join(idParts[:2], "_")
}
if !handlers.ValidId(handlers.Id(req.GetItem().GetHostCatalogId()), static.HostCatalogPrefix, pluginPrefix) {
badFields["host_catalog_id"] = "The field is incorrectly formatted."
}
switch host.SubtypeFromId(req.GetItem().GetHostCatalogId()) {
@@ -641,6 +831,20 @@ func validateCreateRequest(req *pbs.CreateHostSetRequest) error {
if req.GetItem().GetType() != "" && req.GetItem().GetType() != static.Subtype.String() {
badFields["type"] = "Doesn't match the parent resource's type."
}
case plugin.Subtype:
// TODO: Remove this check when plugin id prefixes can differ from plugin name
if req.GetItem().GetType() == "" {
break
}
idParts := strings.Split(req.GetItem().GetHostCatalogId(), "_")
if len(idParts) < 2 {
badFields["host_catalog_id"] = "The field is incorrectly formatted for a plugin id."
break
}
plgType := idParts[1]
if plgType != req.GetItem().GetType() {
badFields["type"] = "This type must match the type of the host catalog."
}
}
return badFields
})
@@ -664,8 +868,14 @@ func validateDeleteRequest(req *pbs.DeleteHostSetRequest) error {
}
func validateListRequest(req *pbs.ListHostSetsRequest) error {
pluginPrefix := plugin.HostCatalogPrefix
idParts := strings.Split(req.GetHostCatalogId(), "_")
if len(idParts) > 2 && idParts[0] == plugin.HostCatalogPrefix {
pluginPrefix = strings.Join(idParts[:2], "_")
}
badFields := map[string]string{}
if !handlers.ValidId(handlers.Id(req.GetHostCatalogId()), static.HostCatalogPrefix) {
if !handlers.ValidId(handlers.Id(req.GetHostCatalogId()), static.HostCatalogPrefix, pluginPrefix) {
badFields["host_catalog_id"] = "The field is incorrectly formatted."
}
if _, err := handlers.NewFilter(req.GetFilter()); err != nil {
@@ -11,9 +11,11 @@ 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"
"github.com/hashicorp/boundary/internal/plugin/host"
"github.com/hashicorp/boundary/internal/servers/controller/auth"
"github.com/hashicorp/boundary/internal/servers/controller/handlers"
"github.com/hashicorp/boundary/internal/servers/controller/handlers/host_sets"
@@ -27,12 +29,13 @@ import (
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb"
)
var testAuthorizedActions = []string{"no-op", "read", "update", "delete", "add-hosts", "set-hosts", "remove-hosts"}
func TestGet(t *testing.T) {
func TestGet_Static(t *testing.T) {
t.Parallel()
conn, _ := db.TestSetup(t, "postgres")
wrapper := db.TestWrapper(t)
@@ -49,6 +52,9 @@ func TestGet(t *testing.T) {
repoFn := func() (*static.Repository, error) {
return static.NewRepository(rw, rw, kms)
}
pluginRepoFn := func() (*plugin.Repository, error) {
return plugin.NewRepository(rw, rw, kms)
}
hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0]
hs := static.TestSets(t, conn, hc.GetPublicId(), 1)[0]
@@ -105,7 +111,7 @@ func TestGet(t *testing.T) {
req := proto.Clone(toMerge).(*pbs.GetHostSetRequest)
proto.Merge(req, tc.req)
s, err := host_sets.NewService(repoFn)
s, err := host_sets.NewService(repoFn, pluginRepoFn)
require.NoError(err, "Couldn't create a new host set service.")
got, gErr := s.GetHostSet(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), req)
@@ -122,7 +128,8 @@ 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)
@@ -138,6 +145,101 @@ func TestList(t *testing.T) {
repoFn := func() (*static.Repository, error) {
return static.NewRepository(rw, rw, kms)
}
pluginRepoFn := func() (*plugin.Repository, error) {
return plugin.NewRepository(rw, rw, kms)
}
name := "test"
plg := host.TestPlugin(t, conn, name, name)
hc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId())
hs := plugin.TestSet(t, conn, hc.GetPublicId())
toMerge := &pbs.GetHostSetRequest{}
pHost := &pb.HostSet{
HostCatalogId: hc.GetPublicId(),
Id: hs.GetPublicId(),
CreatedTime: hs.CreateTime.GetTimestamp(),
UpdatedTime: hs.UpdateTime.GetTimestamp(),
Type: name,
Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()},
AuthorizedActions: testAuthorizedActions,
}
cases := []struct {
name string
req *pbs.GetHostSetRequest
res *pbs.GetHostSetResponse
err error
}{
{
name: "Get an Existing Host",
req: &pbs.GetHostSetRequest{Id: hs.GetPublicId()},
res: &pbs.GetHostSetResponse{Item: pHost},
},
{
name: "Get a non existing Host Set",
req: &pbs.GetHostSetRequest{Id: static.HostSetPrefix + "_DoesntExis"},
res: nil,
err: handlers.ApiErrorWithCode(codes.NotFound),
},
{
name: "Wrong id prefix",
req: &pbs.GetHostSetRequest{Id: "j_1234567890"},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "space in id",
req: &pbs.GetHostSetRequest{Id: static.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)
req := proto.Clone(toMerge).(*pbs.GetHostSetRequest)
proto.Merge(req, tc.req)
s, err := host_sets.NewService(repoFn, pluginRepoFn)
require.NoError(err, "Couldn't create a new host set service.")
got, gErr := s.GetHostSet(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), req)
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "GetHostSet(%+v) got error %v, wanted %v", req, gErr, tc.err)
return
}
require.NoError(gErr)
if tc.res != nil {
tc.res.Item.Version = 1
}
assert.Empty(cmp.Diff(got.GetItem(), tc.res.GetItem(), protocmp.Transform()), "GetHostSet(%q) got response %q, wanted %q", 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)
iamRepo := iam.TestRepo(t, conn, wrapper)
iamRepoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
org, proj := iam.TestScopes(t, iamRepo)
rw := db.New(conn)
repoFn := func() (*static.Repository, error) {
return static.NewRepository(rw, rw, kms)
}
pluginRepoFn := func() (*plugin.Repository, error) {
return plugin.NewRepository(rw, rw, kms)
}
hcs := static.TestCatalogs(t, conn, proj.GetPublicId(), 2)
hc, hcNoHosts := hcs[0], hcs[1]
@@ -190,7 +292,108 @@ 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 := host_sets.NewService(repoFn)
s, err := host_sets.NewService(repoFn, pluginRepoFn)
require.NoError(err, "Couldn't create new host set service.")
// Test with non-anon user
got, gErr := s.ListHostSets(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), tc.req)
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "ListHostSets(%q) got error %v, wanted %v", tc.req, gErr, tc.err)
return
}
require.NoError(gErr)
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListHostSets(%q) got response %q, wanted %q", tc.req, got, tc.res)
// Test with anon user
got, gErr = s.ListHostSets(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)
require.Empty(item.HostIds)
}
})
}
}
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)
rw := db.New(conn)
repoFn := func() (*static.Repository, error) {
return static.NewRepository(rw, rw, kms)
}
pluginRepoFn := func() (*plugin.Repository, error) {
return plugin.NewRepository(rw, rw, kms)
}
name := "test"
plg := host.TestPlugin(t, conn, name, name)
hc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId())
hcNoHosts := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId())
var wantHs []*pb.HostSet
for i := 0; i < 10; i++ {
h := plugin.TestSet(t, conn, hc.GetPublicId())
wantHs = append(wantHs, &pb.HostSet{
Id: h.GetPublicId(),
HostCatalogId: h.GetCatalogId(),
Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()},
CreatedTime: h.GetCreateTime().GetTimestamp(),
UpdatedTime: h.GetUpdateTime().GetTimestamp(),
Version: h.GetVersion(),
Type: name,
AuthorizedActions: testAuthorizedActions,
})
}
cases := []struct {
name string
req *pbs.ListHostSetsRequest
res *pbs.ListHostSetsResponse
err error
}{
{
name: "List Many Host Sets",
req: &pbs.ListHostSetsRequest{HostCatalogId: hc.GetPublicId()},
res: &pbs.ListHostSetsResponse{Items: wantHs},
},
{
name: "List No Host Sets",
req: &pbs.ListHostSetsRequest{HostCatalogId: hcNoHosts.GetPublicId()},
res: &pbs.ListHostSetsResponse{},
},
{
name: "Filter To One Host Set",
req: &pbs.ListHostSetsRequest{HostCatalogId: hc.GetPublicId(), Filter: fmt.Sprintf(`"/item/id"==%q`, wantHs[1].GetId())},
res: &pbs.ListHostSetsResponse{Items: wantHs[1:2]},
},
{
name: "Filter To No Host Sets",
req: &pbs.ListHostSetsRequest{HostCatalogId: hc.GetPublicId(), Filter: `"/item/name"=="doesnt match"`},
res: &pbs.ListHostSetsResponse{},
},
{
name: "Filter Bad Format",
req: &pbs.ListHostSetsRequest{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 := host_sets.NewService(repoFn, pluginRepoFn)
require.NoError(err, "Couldn't create new host set service.")
// Test with non-anon user
@@ -234,10 +437,13 @@ func TestDelete(t *testing.T) {
repoFn := func() (*static.Repository, error) {
return static.NewRepository(rw, rw, kms)
}
pluginRepoFn := func() (*plugin.Repository, error) {
return plugin.NewRepository(rw, rw, kms)
}
hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0]
h := static.TestSets(t, conn, hc.GetPublicId(), 1)[0]
s, err := host_sets.NewService(repoFn)
s, err := host_sets.NewService(repoFn, pluginRepoFn)
require.NoError(t, err, "Couldn't create a new host set service.")
cases := []struct {
@@ -310,10 +516,13 @@ func TestDelete_twice(t *testing.T) {
repoFn := func() (*static.Repository, error) {
return static.NewRepository(rw, rw, kms)
}
plgRepoFn := func() (*plugin.Repository, error) {
return plugin.NewRepository(rw, rw, kms)
}
hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0]
h := static.TestSets(t, conn, hc.GetPublicId(), 1)[0]
s, err := host_sets.NewService(repoFn)
s, err := host_sets.NewService(repoFn, plgRepoFn)
require.NoError(err, "Couldn't create a new host set service.")
req := &pbs.DeleteHostSetRequest{
Id: h.GetPublicId(),
@@ -326,7 +535,7 @@ func TestDelete_twice(t *testing.T) {
assert.True(errors.Is(gErr, handlers.ApiErrorWithCode(codes.NotFound)), "Expected permission denied for the second delete.")
}
func TestCreate(t *testing.T) {
func TestCreate_Static(t *testing.T) {
t.Parallel()
conn, _ := db.TestSetup(t, "postgres")
wrapper := db.TestWrapper(t)
@@ -343,6 +552,9 @@ func TestCreate(t *testing.T) {
repoFn := func() (*static.Repository, error) {
return static.NewRepository(rw, rw, kms)
}
plgRepoFn := func() (*plugin.Repository, error) {
return plugin.NewRepository(rw, rw, kms)
}
hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0]
defaultHcCreated := hc.GetCreateTime().GetTimestamp().AsTime()
@@ -434,7 +646,7 @@ func TestCreate(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
s, err := host_sets.NewService(repoFn)
s, err := host_sets.NewService(repoFn, plgRepoFn)
require.NoError(err, "Failed to create a new host set service.")
got, gErr := s.CreateHostSet(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), tc.req)
@@ -466,6 +678,197 @@ func TestCreate(t *testing.T) {
}
}
func TestCreate_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)
rw := db.New(conn)
repoFn := func() (*static.Repository, error) {
return static.NewRepository(rw, rw, kms)
}
plgRepoFn := func() (*plugin.Repository, error) {
return plugin.NewRepository(rw, rw, kms)
}
name := "test"
plg := host.TestPlugin(t, conn, name, name)
hc := plugin.TestCatalog(t, conn, proj.GetPublicId(), plg.GetPublicId())
testAttrs, err := structpb.NewStruct(map[string]interface{}{
"int": 1,
"zero int": 0,
"string": "foo",
"zero string": "",
"bytes": []byte("bar"),
"zero bytes": nil,
"bool": true,
"zero bool": false,
"nested": map[string]interface{}{
"int": 1,
"zero int": 0,
"string": "foo",
"zero string": "",
"bytes": []byte("bar"),
"zero bytes": nil,
"bool": true,
"zero bool": false,
},
})
require.NoError(t, err)
defaultHcCreated := hc.GetCreateTime().GetTimestamp().AsTime()
cases := []struct {
name string
req *pbs.CreateHostSetRequest
res *pbs.CreateHostSetResponse
err error
}{
{
name: "No Attributes",
req: &pbs.CreateHostSetRequest{Item: &pb.HostSet{
HostCatalogId: hc.GetPublicId(),
Name: &wrappers.StringValue{Value: "No Attributes"},
Description: &wrappers.StringValue{Value: "desc"},
Type: name,
}},
res: &pbs.CreateHostSetResponse{
Uri: fmt.Sprintf("host-sets/%s_%s_", plugin.HostSetPrefix, name),
Item: &pb.HostSet{
HostCatalogId: hc.GetPublicId(),
Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()},
Name: &wrappers.StringValue{Value: "No Attributes"},
Description: &wrappers.StringValue{Value: "desc"},
Type: name,
AuthorizedActions: testAuthorizedActions,
},
},
},
{
name: "With Attributes",
req: &pbs.CreateHostSetRequest{Item: &pb.HostSet{
HostCatalogId: hc.GetPublicId(),
Name: &wrappers.StringValue{Value: "With Attributes"},
Description: &wrappers.StringValue{Value: "desc"},
Type: name,
Attributes: testAttrs,
}},
res: &pbs.CreateHostSetResponse{
Uri: fmt.Sprintf("host-sets/%s_%s_", plugin.HostSetPrefix, name),
Item: &pb.HostSet{
HostCatalogId: hc.GetPublicId(),
Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()},
Name: &wrappers.StringValue{Value: "With Attributes"},
Description: &wrappers.StringValue{Value: "desc"},
Type: name,
AuthorizedActions: testAuthorizedActions,
Attributes: testAttrs,
},
},
},
{
name: "Create with mismatched type/name",
req: &pbs.CreateHostSetRequest{Item: &pb.HostSet{
HostCatalogId: hc.GetPublicId(),
Name: &wrappers.StringValue{Value: "name"},
Description: &wrappers.StringValue{Value: "desc"},
Type: "ThisIsMadeUp",
}},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Create with no type",
req: &pbs.CreateHostSetRequest{Item: &pb.HostSet{
HostCatalogId: hc.GetPublicId(),
Name: &wrappers.StringValue{Value: "no type name"},
Description: &wrappers.StringValue{Value: "no type desc"},
}},
res: &pbs.CreateHostSetResponse{
Uri: fmt.Sprintf("host-sets/%s_%s_", plugin.HostSetPrefix, name),
Item: &pb.HostSet{
HostCatalogId: hc.GetPublicId(),
Scope: &scopes.ScopeInfo{Id: proj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: org.GetPublicId()},
Name: &wrappers.StringValue{Value: "no type name"},
Description: &wrappers.StringValue{Value: "no type desc"},
Type: name,
AuthorizedActions: testAuthorizedActions,
},
},
},
{
name: "Can't specify Id",
req: &pbs.CreateHostSetRequest{Item: &pb.HostSet{
HostCatalogId: hc.GetPublicId(),
Id: "not allowed to be set",
}},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Can't specify Created Time",
req: &pbs.CreateHostSetRequest{Item: &pb.HostSet{
HostCatalogId: hc.GetPublicId(),
CreatedTime: timestamppb.Now(),
}},
res: nil,
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "Can't specify Update Time",
req: &pbs.CreateHostSetRequest{Item: &pb.HostSet{
HostCatalogId: hc.GetPublicId(),
UpdatedTime: timestamppb.Now(),
}},
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 := host_sets.NewService(repoFn, plgRepoFn)
require.NoError(err, "Failed to create a new host set service.")
got, gErr := s.CreateHostSet(auth.DisabledAuthTestContext(iamRepoFn, proj.GetPublicId()), tc.req)
if tc.err != nil {
require.Error(gErr)
assert.True(errors.Is(gErr, tc.err), "CreateHostSet(%+v) got error %v, wanted %v", tc.req, gErr, tc.err)
return
}
require.NoError(gErr)
if got != nil {
assert.Contains(got.GetUri(), tc.res.GetUri())
assert.True(strings.HasPrefix(got.GetItem().GetId(), plugin.HostSetPrefix), got.GetItem().GetId())
gotCreateTime := got.GetItem().GetCreatedTime().AsTime()
require.NoError(err, "Error converting proto to timestamp.")
gotUpdateTime := got.GetItem().GetUpdatedTime().AsTime()
require.NoError(err, "Error converting proto to timestamp")
// Verify it is a set created after the test setup's default set
assert.True(gotCreateTime.After(defaultHcCreated), "New set should have been created after default set. Was created %v, which is after %v", gotCreateTime, defaultHcCreated)
assert.True(gotUpdateTime.After(defaultHcCreated), "New set should have been updated after default set. Was updated %v, which is after %v", gotUpdateTime, defaultHcCreated)
// Clear all values which are hard to compare against.
got.Uri, tc.res.Uri = "", ""
got.Item.Id, tc.res.Item.Id = "", ""
got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil
}
if tc.res != nil {
tc.res.Item.Version = 1
}
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateHostSet(%q) got response %q, wanted %q", tc.req, got, tc.res)
})
}
}
func TestUpdate(t *testing.T) {
t.Parallel()
conn, _ := db.TestSetup(t, "postgres")
@@ -512,7 +915,10 @@ func TestUpdate(t *testing.T) {
Id: hs.GetPublicId(),
}
tested, err := host_sets.NewService(repoFn)
plgRepoFn := func() (*plugin.Repository, error) {
return plugin.NewRepository(rw, rw, kms)
}
tested, err := host_sets.NewService(repoFn, plgRepoFn)
require.NoError(t, err, "Failed to create a new host set service.")
cases := []struct {
@@ -848,7 +1254,10 @@ func TestAddHostSetHosts(t *testing.T) {
repoFn := func() (*static.Repository, error) {
return static.NewRepository(rw, rw, kms)
}
s, err := host_sets.NewService(repoFn)
plgRepoFn := func() (*plugin.Repository, error) {
return plugin.NewRepository(rw, rw, kms)
}
s, err := host_sets.NewService(repoFn, plgRepoFn)
require.NoError(t, err, "Error when getting new host set service.")
hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0]
@@ -965,7 +1374,10 @@ func TestSetHostSetHosts(t *testing.T) {
repoFn := func() (*static.Repository, error) {
return static.NewRepository(rw, rw, kms)
}
s, err := host_sets.NewService(repoFn)
plgRepoFn := func() (*plugin.Repository, error) {
return plugin.NewRepository(rw, rw, kms)
}
s, err := host_sets.NewService(repoFn, plgRepoFn)
require.NoError(t, err, "Error when getting new host set service.")
hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0]
@@ -1078,7 +1490,10 @@ func TestRemoveHostSetHosts(t *testing.T) {
repoFn := func() (*static.Repository, error) {
return static.NewRepository(rw, rw, kms)
}
s, err := host_sets.NewService(repoFn)
plgRepoFn := func() (*plugin.Repository, error) {
return plugin.NewRepository(rw, rw, kms)
}
s, err := host_sets.NewService(repoFn, plgRepoFn)
require.NoError(t, err, "Error when getting new host set service.")
hc := static.TestCatalogs(t, conn, proj.GetPublicId(), 1)[0]