diff --git a/internal/auth/subtype.go b/internal/auth/subtype.go index fbcd8e4d3..43c19cefc 100644 --- a/internal/auth/subtype.go +++ b/internal/auth/subtype.go @@ -1,7 +1,7 @@ package auth import ( - "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/boundary" "github.com/hashicorp/boundary/internal/types/subtypes" ) @@ -28,32 +28,17 @@ func Register(subtype subtypes.Subtype, prefixes ...string) error { // AuthMethod contains the common methods across all the different types of auth methods. type AuthMethod interface { - GetPublicId() string - GetCreateTime() *timestamp.Timestamp - GetUpdateTime() *timestamp.Timestamp - GetName() string - GetDescription() string + boundary.Resource GetScopeId() string - GetVersion() uint32 GetIsPrimaryAuthMethod() bool } type Account interface { - GetPublicId() string - GetCreateTime() *timestamp.Timestamp - GetUpdateTime() *timestamp.Timestamp - GetName() string - GetDescription() string + boundary.Resource GetAuthMethodId() string - GetVersion() uint32 } type ManagedGroup interface { - GetPublicId() string - GetCreateTime() *timestamp.Timestamp - GetUpdateTime() *timestamp.Timestamp - GetName() string - GetDescription() string + boundary.Resource GetAuthMethodId() string - GetVersion() uint32 } diff --git a/internal/host/plugin/host_catalog.go b/internal/host/plugin/host_catalog.go index 878e16274..9ac49f181 100644 --- a/internal/host/plugin/host_catalog.go +++ b/internal/host/plugin/host_catalog.go @@ -44,6 +44,12 @@ func NewHostCatalog(ctx context.Context, pluginId, scopeId string, opt ...Option return hc, nil } +func allocHostCatalog() *HostCatalog { + return &HostCatalog{ + HostCatalog: &store.HostCatalog{}, + } +} + func (c *HostCatalog) clone() *HostCatalog { cp := proto.Clone(c.HostCatalog) return &HostCatalog{ diff --git a/internal/host/plugin/host_catalog_test.go b/internal/host/plugin/host_catalog_test.go index c3244b6cd..ec45a6185 100644 --- a/internal/host/plugin/host_catalog_test.go +++ b/internal/host/plugin/host_catalog_test.go @@ -17,6 +17,7 @@ import ( ) func TestHostCatalog_Create(t *testing.T) { + ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) @@ -140,21 +141,21 @@ func TestHostCatalog_Create(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - got, err := NewHostCatalog(context.Background(), tt.args.pluginId, tt.args.scopeId, tt.args.opts...) + got, err := NewHostCatalog(ctx, tt.args.pluginId, tt.args.scopeId, tt.args.opts...) require.NoError(t, err) require.NotNil(t, got) assert.Emptyf(t, got.PublicId, "PublicId set") assert.Equal(t, tt.want, got) - id, err := newHostCatalogId() + id, err := newHostCatalogId(plg.GetIdPrefix()) assert.NoError(t, err) tt.want.PublicId = id got.PublicId = id w := db.New(conn) - err = w.Create(context.Background(), got) + err = w.Create(ctx, got) if tt.wantCreateErr { assert.Error(t, err) } else { @@ -164,7 +165,7 @@ func TestHostCatalog_Create(t *testing.T) { PublicId: id, }, } - require.NoError(t, w.LookupById(context.Background(), found)) + require.NoError(t, w.LookupById(ctx, found)) assert.Empty(t, cmp.Diff(got.HostCatalog, found.HostCatalog, protocmp.Transform()), "%q compared to %q", got.Attributes, found.Attributes) } }) @@ -181,28 +182,28 @@ func TestHostCatalog_Create_DuplicateNames(t *testing.T) { plg := host.TestPlugin(t, conn, "test1", "prefix1") plg2 := host.TestPlugin(t, conn, "test2", "prefix2") - got, err := NewHostCatalog(context.Background(), plg.GetPublicId(), prj.GetPublicId(), WithName("duplicate")) + got, err := NewHostCatalog(ctx, plg.GetPublicId(), prj.GetPublicId(), WithName("duplicate")) require.NoError(t, err) - got.PublicId, err = newHostCatalogId() + got.PublicId, err = newHostCatalogId(plg.GetIdPrefix()) require.NoError(t, err) w.Create(ctx, got) // Can't create another resource with the same pluginName in the same scope - got.PublicId, err = newHostCatalogId() + got.PublicId, err = newHostCatalogId(plg.GetIdPrefix()) require.NoError(t, err) assert.Error(t, w.Create(ctx, got)) // Can't create another resource with same pluginName in same scope even for different plugin - got, err = NewHostCatalog(context.Background(), plg2.GetPublicId(), prj.GetPublicId(), WithName("duplicate")) + got, err = NewHostCatalog(ctx, plg2.GetPublicId(), prj.GetPublicId(), WithName("duplicate")) require.NoError(t, err) - got.PublicId, err = newHostCatalogId() + got.PublicId, err = newHostCatalogId(plg2.GetIdPrefix()) require.NoError(t, err) assert.Error(t, w.Create(ctx, got)) // Can create another resource with same pluginName in different scope even for same plugin - got, err = NewHostCatalog(context.Background(), plg.GetPublicId(), prj2.GetPublicId(), WithName("duplicate")) + got, err = NewHostCatalog(ctx, plg.GetPublicId(), prj2.GetPublicId(), WithName("duplicate")) require.NoError(t, err) - got.PublicId, err = newHostCatalogId() + got.PublicId, err = newHostCatalogId(plg.GetIdPrefix()) require.NoError(t, err) assert.NoError(t, w.Create(ctx, got)) } diff --git a/internal/host/plugin/host_set.go b/internal/host/plugin/host_set.go index e40290bbb..a60c6f5fb 100644 --- a/internal/host/plugin/host_set.go +++ b/internal/host/plugin/host_set.go @@ -6,6 +6,8 @@ import ( "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/host/plugin/store" + "github.com/hashicorp/boundary/internal/oplog" + "google.golang.org/protobuf/proto" ) // A HostSet is a collection of hosts from the set's catalog. @@ -51,3 +53,28 @@ func (s *HostSet) TableName() string { func (s *HostSet) SetTableName(n string) { s.tableName = n } + +func allocHostSet() *HostSet { + return &HostSet{ + HostSet: &store.HostSet{}, + } +} + +func (s *HostSet) clone() *HostSet { + cp := proto.Clone(s.HostSet) + return &HostSet{ + HostSet: cp.(*store.HostSet), + } +} + +func (s *HostSet) oplog(op oplog.OpType) oplog.Metadata { + metadata := oplog.Metadata{ + "resource-public-id": []string{s.PublicId}, + "resource-type": []string{"plugin-host-set"}, + "op-type": []string{op.String()}, + } + if s.CatalogId != "" { + metadata["catalog-id"] = []string{s.CatalogId} + } + return metadata +} diff --git a/internal/host/plugin/host_set_test.go b/internal/host/plugin/host_set_test.go index 7a31b75f4..a0310fb80 100644 --- a/internal/host/plugin/host_set_test.go +++ b/internal/host/plugin/host_set_test.go @@ -145,7 +145,7 @@ func TestHostSet_Create(t *testing.T) { assert.Emptyf(t, got.PublicId, "PublicId set") assert.Equal(t, tt.want, got) - id, err := newHostSetId() + id, err := newHostSetId(plg.GetIdPrefix()) assert.NoError(t, err) tt.want.PublicId = id diff --git a/internal/host/plugin/ids.go b/internal/host/plugin/ids.go index c4d2af7e7..9de819e62 100644 --- a/internal/host/plugin/ids.go +++ b/internal/host/plugin/ids.go @@ -1,6 +1,8 @@ package plugin import ( + "fmt" + "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/host" @@ -22,18 +24,18 @@ const ( Subtype = subtypes.Subtype("plugin") ) -func newHostCatalogId() (string, error) { - // TODO: Take in a prefix portion as an argument. - id, err := db.NewPublicId(HostCatalogPrefix) +func newHostCatalogId(upre string) (string, error) { + prefix := fmt.Sprintf("%s_%s", HostCatalogPrefix, upre) + id, err := db.NewPublicId(prefix) if err != nil { return "", errors.WrapDeprecated(err, "plugin.newHostCatalogId") } return id, nil } -func newHostSetId() (string, error) { - // TODO: Take in a prefix portion as an argument. - id, err := db.NewPublicId(HostSetPrefix) +func newHostSetId(upre string) (string, error) { + prefix := fmt.Sprintf("%s_%s", HostSetPrefix, upre) + id, err := db.NewPublicId(prefix) if err != nil { return "", errors.WrapDeprecated(err, "plugin.newHostSetId") } diff --git a/internal/host/plugin/repository.go b/internal/host/plugin/repository.go new file mode 100644 index 000000000..298885732 --- /dev/null +++ b/internal/host/plugin/repository.go @@ -0,0 +1,47 @@ +package plugin + +import ( + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" +) + +// A Repository stores and retrieves the persistent types in the plugin +// package. It is not safe to use a repository concurrently. +type Repository struct { + reader db.Reader + writer db.Writer + kms *kms.Kms + // defaultLimit provides a default for limiting the number of results + // returned from the repo + defaultLimit int +} + +// NewRepository creates a new Repository. The returned repository should +// only be used for one transaction and it is not safe for concurrent go +// routines to access it. WithLimit option is used as a repo wide default +// limit applied to all ListX methods. +func NewRepository(r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*Repository, error) { + const op = "plugin.NewRepository" + switch { + case r == nil: + return nil, errors.NewDeprecated(errors.InvalidParameter, op, "db.Reader") + case w == nil: + return nil, errors.NewDeprecated(errors.InvalidParameter, op, "db.Writer") + case kms == nil: + return nil, errors.NewDeprecated(errors.InvalidParameter, op, "kms") + } + + opts := getOpts(opt...) + if opts.withLimit == 0 { + // zero signals the boundary defaults should be used. + opts.withLimit = db.DefaultLimit + } + + return &Repository{ + reader: r, + writer: w, + kms: kms, + defaultLimit: opts.withLimit, + }, nil +} diff --git a/internal/host/plugin/repository_host_set.go b/internal/host/plugin/repository_host_set.go new file mode 100644 index 000000000..d4e0a440f --- /dev/null +++ b/internal/host/plugin/repository_host_set.go @@ -0,0 +1,131 @@ +package plugin + +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + hostplg "github.com/hashicorp/boundary/internal/plugin/host" +) + +// CreateSet inserts s into the repository and returns a new HostSet +// containing the host set's PublicId. s is not changed. s must contain a +// valid CatalogId. s must not contain a PublicId. The PublicId is +// generated and assigned by this method. opt is ignored. +// +// Both s.Name and s.Description are optional. If s.Name is set, it must be +// unique within s.CatalogId. +func (r *Repository) CreateSet(ctx context.Context, scopeId string, s *HostSet, _ ...Option) (*HostSet, error) { + const op = "plugin.(Repository).CreateSet" + if s == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "nil HostSet") + } + if s.HostSet == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "nil embedded HostSet") + } + if s.CatalogId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no catalog id") + } + if s.PublicId != "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "public id not empty") + } + if scopeId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no scope id") + } + if s.Attributes == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "nil attributes") + } + s = s.clone() + + c := allocHostCatalog() + c.PublicId = s.GetCatalogId() + if err := r.reader.LookupByPublicId(ctx, c); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get host catalog")) + } + + // TODO: Contain the following plugin logic inside a plugin manager as well as + // calling the plugin which we have looked up. + plg := hostplg.NewPlugin("", "") + plg.PublicId = c.GetPluginId() + if err := r.reader.LookupByPublicId(ctx, plg); err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get host plugin")) + } + + id, err := newHostSetId(plg.GetIdPrefix()) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + s.PublicId = id + + oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + var newHostSet *HostSet + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + newHostSet = s.clone() + err := w.Create(ctx, newHostSet, db.WithOplog(oplogWrapper, s.oplog(oplog.OpType_OP_TYPE_CREATE))) + if err != nil { + return errors.Wrap(ctx, err, op) + } + return nil + }, + ) + + if err != nil { + if errors.IsUniqueError(err) { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in catalog: %s: name %s already exists", s.CatalogId, s.Name))) + } + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in catalog: %s", s.CatalogId))) + } + return newHostSet, nil +} + +// LookupSet will look up a host set in the repository and return the host +// set. If the host set is not found, it will return nil, nil. +// All options are ignored. +func (r *Repository) LookupSet(ctx context.Context, publicId string, _ ...Option) (*HostSet, error) { + const op = "plugin.(Repository).LookupSet" + if publicId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no public id") + } + + s := allocHostSet() + s.PublicId = publicId + + err := r.reader.LookupByPublicId(ctx, s) + if errors.IsNotFoundError(err) { + return nil, nil + } + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in %s", s.PublicId))) + } + + return s, nil +} + +// ListSets returns a slice of HostSets for the catalogId. WithLimit is the +// only option supported. +func (r *Repository) ListSets(ctx context.Context, catalogId string, opt ...Option) ([]*HostSet, error) { + const op = "plugin.(Repository).ListSets" + if catalogId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "no catalog id") + } + opts := getOpts(opt...) + limit := r.defaultLimit + if opts.withLimit != 0 { + // non-zero signals an override of the default limit for the repo. + limit = opts.withLimit + } + var sets []*HostSet + err := r.reader.SearchWhere(ctx, &sets, "catalog_id = ?", []interface{}{catalogId}, db.WithLimit(limit)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + return sets, nil +} diff --git a/internal/host/plugin/repository_host_set_test.go b/internal/host/plugin/repository_host_set_test.go new file mode 100644 index 000000000..ca63bf83a --- /dev/null +++ b/internal/host/plugin/repository_host_set_test.go @@ -0,0 +1,416 @@ +package plugin + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/host/plugin/store" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + hostplg "github.com/hashicorp/boundary/internal/plugin/host" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestRepository_CreateSet(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + _, prj := iam.TestScopes(t, iamRepo) + plg := hostplg.TestPlugin(t, conn, "create", "create") + catalog := TestCatalog(t, conn, plg.GetPublicId(), prj.PublicId) + attrs := []byte("{}") + + tests := []struct { + name string + in *HostSet + opts []Option + want *HostSet + wantIsErr errors.Code + }{ + { + name: "nil-HostSet", + wantIsErr: errors.InvalidParameter, + }, + { + name: "nil-embedded-HostSet", + in: &HostSet{}, + wantIsErr: errors.InvalidParameter, + }, + { + name: "invalid-no-catalog-id", + in: &HostSet{ + HostSet: &store.HostSet{ + Attributes: attrs, + }, + }, + wantIsErr: errors.InvalidParameter, + }, + { + name: "invalid-public-id-set", + in: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: catalog.PublicId, + PublicId: "abcd_OOOOOOOOOO", + Attributes: attrs, + }, + }, + wantIsErr: errors.InvalidParameter, + }, + { + name: "invalid-no-attribte", + in: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: catalog.PublicId, + }, + }, + wantIsErr: errors.InvalidParameter, + }, + { + name: "valid-no-options", + in: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: catalog.PublicId, + Attributes: attrs, + }, + }, + want: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: catalog.PublicId, + Attributes: attrs, + }, + }, + }, + { + name: "valid-with-name", + in: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: catalog.PublicId, + Name: "test-name-repo", + Attributes: attrs, + }, + }, + want: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: catalog.PublicId, + Name: "test-name-repo", + Attributes: attrs, + }, + }, + }, + { + name: "valid-with-description", + in: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: catalog.PublicId, + Description: ("test-description-repo"), + Attributes: attrs, + }, + }, + want: &HostSet{ + HostSet: &store.HostSet{ + CatalogId: catalog.PublicId, + Description: ("test-description-repo"), + Attributes: attrs, + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kms) + require.NoError(err) + require.NotNil(repo) + got, err := repo.CreateSet(context.Background(), prj.GetPublicId(), tt.in, tt.opts...) + if tt.wantIsErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "want err: %q got: %q", tt.wantIsErr, err) + assert.Nil(got) + return + } + require.NoError(err) + assert.Empty(tt.in.PublicId) + require.NotNil(got) + assert.True(strings.HasPrefix(got.GetPublicId(), HostSetPrefix)) + assert.NotSame(tt.in, got) + assert.Equal(tt.want.Name, got.GetName()) + assert.Equal(tt.want.Description, got.GetDescription()) + assert.Equal(got.GetCreateTime(), got.GetUpdateTime()) + assert.NoError(db.TestVerifyOplog(t, rw, got.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second))) + }) + } + + t.Run("invalid-duplicate-names", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kms) + require.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iamRepo) + catalog := TestCatalog(t, conn, plg.GetPublicId(), prj.PublicId) + + in := &HostSet{ + HostSet: &store.HostSet{ + CatalogId: catalog.PublicId, + Name: "test-name-repo", + Attributes: []byte("{}"), + }, + } + + got, err := repo.CreateSet(context.Background(), prj.GetPublicId(), in) + require.NoError(err) + require.NotNil(got) + assert.True(strings.HasPrefix(got.GetPublicId(), HostSetPrefix)) + assert.NotSame(in, got) + assert.Equal(in.Name, got.GetName()) + assert.Equal(in.Description, got.GetDescription()) + assert.Equal(got.GetCreateTime(), got.GetUpdateTime()) + + got2, err := repo.CreateSet(context.Background(), prj.GetPublicId(), in) + assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "want err code: %v got err: %v", errors.NotUnique, err) + assert.Nil(got2) + }) + + t.Run("valid-duplicate-names-diff-catalogs", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kms) + require.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iamRepo) + catalogA := TestCatalog(t, conn, plg.GetPublicId(), prj.PublicId) + catalogB := TestCatalog(t, conn, plg.GetPublicId(), prj.PublicId) + + in := &HostSet{ + HostSet: &store.HostSet{ + Name: "test-name-repo", + Attributes: []byte("{}"), + }, + } + in2 := in.clone() + + in.CatalogId = catalogA.PublicId + got, err := repo.CreateSet(context.Background(), prj.GetPublicId(), in) + require.NoError(err) + require.NotNil(got) + assert.True(strings.HasPrefix(got.GetPublicId(), HostSetPrefix)) + assert.NotSame(in, got) + assert.Equal(in.Name, got.GetName()) + assert.Equal(in.Description, got.GetDescription()) + assert.Equal(got.GetCreateTime(), got.GetUpdateTime()) + + in2.CatalogId = catalogB.PublicId + got2, err := repo.CreateSet(context.Background(), prj.GetPublicId(), in2) + require.NoError(err) + require.NotNil(got2) + assert.True(strings.HasPrefix(got.GetPublicId(), HostSetPrefix)) + assert.NotSame(in2, got2) + assert.Equal(in2.Name, got2.GetName()) + assert.Equal(in2.Description, got2.GetDescription()) + assert.Equal(got2.GetCreateTime(), got2.GetUpdateTime()) + }) +} + +func TestRepository_LookupSet(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + _, prj := iam.TestScopes(t, iamRepo) + plg := hostplg.TestPlugin(t, conn, "lookup", "lookup") + + catalog := TestCatalog(t, conn, plg.GetPublicId(), prj.PublicId) + hostSet := TestSet(t, conn, catalog.PublicId) + hostSetId, err := newHostSetId(plg.GetIdPrefix()) + require.NoError(t, err) + + tests := []struct { + name string + in string + want *HostSet + wantIsErr errors.Code + }{ + { + name: "with-no-public-id", + wantIsErr: errors.InvalidParameter, + }, + { + name: "with-non-existing-host-set-id", + in: hostSetId, + }, + { + name: "with-existing-host-set-id", + in: hostSet.PublicId, + want: hostSet, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kms) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.LookupSet(context.Background(), tt.in) + if tt.wantIsErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "want err: %q got: %q", tt.wantIsErr, err) + assert.Nil(got) + return + } + require.NoError(err) + assert.EqualValues(tt.want, got) + }) + } +} + +func TestRepository_ListSets(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + + _, prj := iam.TestScopes(t, iamRepo) + plg := hostplg.TestPlugin(t, conn, "list", "list") + catalogA := TestCatalog(t, conn, plg.GetPublicId(), prj.PublicId) + catalogB := TestCatalog(t, conn, plg.GetPublicId(), prj.PublicId) + + hostSets := []*HostSet{ + TestSet(t, conn, catalogA.PublicId), + TestSet(t, conn, catalogA.PublicId), + TestSet(t, conn, catalogA.PublicId), + } + + tests := []struct { + name string + in string + opts []Option + want []*HostSet + wantIsErr errors.Code + }{ + { + name: "with-no-catalog-id", + wantIsErr: errors.InvalidParameter, + }, + { + name: "Catalog-with-no-host-sets", + in: catalogB.PublicId, + want: []*HostSet{}, + }, + { + name: "Catalog-with-host-sets", + in: catalogA.PublicId, + want: hostSets, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kms) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.ListSets(context.Background(), tt.in, tt.opts...) + if tt.wantIsErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "want err: %q got: %q", tt.wantIsErr, err) + assert.Nil(got) + return + } + require.NoError(err) + opts := []cmp.Option{ + cmpopts.SortSlices(func(x, y *HostSet) bool { return x.PublicId < y.PublicId }), + protocmp.Transform(), + } + assert.Empty(cmp.Diff(tt.want, got, opts...)) + }) + } +} + +func TestRepository_ListSets_Limits(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + + _, prj := iam.TestScopes(t, iamRepo) + plg := hostplg.TestPlugin(t, conn, "listlimit", "listlimit") + catalog := TestCatalog(t, conn, plg.GetPublicId(), prj.PublicId) + count := 10 + var hostSets []*HostSet + for i := 0; i < count; i++ { + hostSets = append(hostSets, TestSet(t, conn, catalog.PublicId)) + } + + tests := []struct { + name string + repoOpts []Option + listOpts []Option + wantLen int + }{ + { + name: "With no limits", + wantLen: count, + }, + { + name: "With repo limit", + repoOpts: []Option{WithLimit(3)}, + wantLen: 3, + }, + { + name: "With negative repo limit", + repoOpts: []Option{WithLimit(-1)}, + wantLen: count, + }, + { + name: "With List limit", + listOpts: []Option{WithLimit(3)}, + wantLen: 3, + }, + { + name: "With negative List limit", + listOpts: []Option{WithLimit(-1)}, + wantLen: count, + }, + { + name: "With repo smaller than list limit", + repoOpts: []Option{WithLimit(2)}, + listOpts: []Option{WithLimit(6)}, + wantLen: 6, + }, + { + name: "With repo larger than list limit", + repoOpts: []Option{WithLimit(6)}, + listOpts: []Option{WithLimit(2)}, + wantLen: 2, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kms, tt.repoOpts...) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.ListSets(context.Background(), hostSets[0].CatalogId, tt.listOpts...) + require.NoError(err) + assert.Len(got, tt.wantLen) + }) + } +} diff --git a/internal/host/plugin/repository_test.go b/internal/host/plugin/repository_test.go new file mode 100644 index 000000000..6ef12a17e --- /dev/null +++ b/internal/host/plugin/repository_test.go @@ -0,0 +1,118 @@ +package plugin + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" +) + +func TestRepository_New(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + + type args struct { + r db.Reader + w db.Writer + kms *kms.Kms + opts []Option + } + + tests := []struct { + name string + args args + want *Repository + wantIsErr errors.Code + }{ + { + name: "valid", + args: args{ + r: rw, + w: rw, + kms: kmsCache, + }, + want: &Repository{ + reader: rw, + writer: rw, + kms: kmsCache, + defaultLimit: db.DefaultLimit, + }, + }, + { + name: "valid-with-limit", + args: args{ + r: rw, + w: rw, + kms: kmsCache, + opts: []Option{WithLimit(5)}, + }, + want: &Repository{ + reader: rw, + writer: rw, + kms: kmsCache, + defaultLimit: 5, + }, + }, + { + name: "nil-reader", + args: args{ + r: nil, + w: rw, + kms: kmsCache, + }, + want: nil, + wantIsErr: errors.InvalidParameter, + }, + { + name: "nil-writer", + args: args{ + r: rw, + w: nil, + kms: kmsCache, + }, + want: nil, + wantIsErr: errors.InvalidParameter, + }, + { + name: "nil-kms", + args: args{ + r: rw, + w: rw, + kms: nil, + }, + want: nil, + wantIsErr: errors.InvalidParameter, + }, + { + name: "all-nils", + args: args{ + r: nil, + w: nil, + kms: nil, + }, + want: nil, + wantIsErr: errors.InvalidParameter, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewRepository(tt.args.r, tt.args.w, tt.args.kms, tt.args.opts...) + if tt.wantIsErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "want err: %q got: %q", tt.wantIsErr, err) + assert.Nil(got) + return + } + assert.NoError(err) + require.NotNil(got) + assert.Equal(tt.want, got) + }) + } +} diff --git a/internal/host/plugin/testing.go b/internal/host/plugin/testing.go index 9dad4eae6..86a386ebc 100644 --- a/internal/host/plugin/testing.go +++ b/internal/host/plugin/testing.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/plugin/host" "github.com/jinzhu/gorm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,35 +17,51 @@ import ( func TestCatalog(t *testing.T, conn *gorm.DB, pluginId, scopeId string, opt ...Option) *HostCatalog { t.Helper() ctx := context.Background() + w := db.New(conn) + cat, err := NewHostCatalog(ctx, pluginId, scopeId, opt...) require.NoError(t, err) assert.NotNil(t, cat) - id, err := newHostCatalogId() + + plg := host.NewPlugin("", "") + plg.PublicId = pluginId + require.NoError(t, w.LookupByPublicId(ctx, plg)) + + id, err := newHostCatalogId(plg.GetIdPrefix()) assert.NoError(t, err) assert.NotEmpty(t, id) cat.PublicId = id - w := db.New(conn) require.NoError(t, w.Create(ctx, cat)) return cat } -// TestSet creates count number of plugin host sets in the provided DB +// TestSet creates a plugin host sets in the provided DB // with the provided catalog id. The catalog must have been created // previously. The test will fail if any errors are encountered. func TestSet(t *testing.T, conn *gorm.DB, catalogId string, opt ...Option) *HostSet { t.Helper() ctx := context.Background() + w := db.New(conn) + assert := assert.New(t) set, err := NewHostSet(ctx, catalogId, opt...) require.NoError(t, err) assert.NotNil(set) - id, err := newHostSetId() + + cg := allocHostCatalog() + cg.PublicId = catalogId + require.NoError(t, w.LookupByPublicId(ctx, cg)) + + plg := host.NewPlugin("", "") + plg.PublicId = cg.GetPluginId() + require.NoError(t, w.LookupByPublicId(ctx, plg)) + + id, err := newHostSetId(plg.GetIdPrefix()) assert.NoError(err) assert.NotEmpty(id) set.PublicId = id - w := db.New(conn) require.NoError(t, w.Create(ctx, set)) return set } diff --git a/internal/host/plugin/testing_test.go b/internal/host/plugin/testing_test.go index fe939e159..facc56027 100644 --- a/internal/host/plugin/testing_test.go +++ b/internal/host/plugin/testing_test.go @@ -1,6 +1,8 @@ package plugin import ( + "fmt" + "strings" "testing" "github.com/hashicorp/boundary/internal/db" @@ -24,6 +26,7 @@ func Test_TestCatalogs(t *testing.T) { cs := TestCatalog(t, conn, plg.GetPublicId(), proj.GetPublicId(), WithName("foo"), WithDescription("bar")) assert.NotEmpty(cs.GetPublicId()) + assert.True(strings.HasPrefix(cs.GetPublicId(), fmt.Sprintf("%s_%s", HostCatalogPrefix, "prefix"))) assert.Equal("foo", cs.GetName()) assert.Equal("bar", cs.GetDescription()) } @@ -43,6 +46,7 @@ func Test_TestSet(t *testing.T) { c := TestCatalog(t, conn, plg.GetPublicId(), prj.GetPublicId()) set := TestSet(t, conn, c.GetPublicId(), WithName("foo"), WithDescription("bar")) assert.NotEmpty(set.GetPublicId()) + assert.True(strings.HasPrefix(set.GetPublicId(), fmt.Sprintf("%s_%s", HostSetPrefix, "prefix"))) assert.Equal("foo", set.GetName()) assert.Equal("bar", set.GetDescription()) } diff --git a/internal/host/subtype.go b/internal/host/subtype.go index 1508eff93..2316d0785 100644 --- a/internal/host/subtype.go +++ b/internal/host/subtype.go @@ -1,6 +1,9 @@ package host -import "github.com/hashicorp/boundary/internal/types/subtypes" +import ( + "github.com/hashicorp/boundary/internal/boundary" + "github.com/hashicorp/boundary/internal/types/subtypes" +) var registry = subtypes.NewRegistry() @@ -22,3 +25,22 @@ func SubtypeFromId(id string) subtypes.Subtype { func Register(subtype subtypes.Subtype, prefixes ...string) error { return registry.Register(subtype, prefixes...) } + +// Catalog contains the common methods across all the different types of host catalogs. +type Catalog interface { + boundary.Resource + GetScopeId() string +} + +// Set contains the common methods across all the different types of host sets. +type Set interface { + boundary.Resource + GetCatalogId() string +} + +// Host contains the common methods across all the different types of hosts. +type Host interface { + boundary.Resource + GetCatalogId() string + GetAddress() string +}