Static Host: Lookup, List and Delete (#283)

* Implement LookupHost

* Implement ListHosts

* Implement DeleteHost
This commit is contained in:
Michael Gaffney authored and GitHub committed 2020-08-19 14:14:14 -04:00
1 parent eaae887bbe
commit a413ad7b84
6 files changed
+356 -13

No files matched your search

+10
View File
@@ -16,6 +16,7 @@ type Option func(*options)
type options struct {
withName string
withDescription string
withLimit int
}
func getDefaultOptions() options {
@@ -38,3 +39,12 @@ func WithName(name string) Option {
o.withName = name
}
}
// WithLimit provides an option to provide a limit. Intentionally allowing
// negative integers. If WithLimit < 0, then unlimited results are
// returned. If WithLimit == 0, then default limits are used for results.
func WithLimit(l int) Option {
return func(o *options) {
o.withLimit = l
}
}
+8 -3
View File
@@ -8,17 +8,22 @@ import (
func Test_GetOpts(t *testing.T) {
t.Parallel()
assert := assert.New(t)
t.Run("WithName", func(t *testing.T) {
opts := getOpts(WithName("test"))
testOpts := getDefaultOptions()
testOpts.withName = "test"
assert.Equal(opts, testOpts)
assert.Equal(t, opts, testOpts)
})
t.Run("WithDescription", func(t *testing.T) {
opts := getOpts(WithDescription("test desc"))
testOpts := getDefaultOptions()
testOpts.withDescription = "test desc"
assert.Equal(opts, testOpts)
assert.Equal(t, opts, testOpts)
})
t.Run("WithLimit", func(t *testing.T) {
opts := getOpts(WithLimit(5))
testOpts := getDefaultOptions()
testOpts.withLimit = 5
assert.Equal(t, opts, testOpts)
})
}
+16 -5
View File
@@ -14,12 +14,16 @@ type Repository struct {
reader db.Reader
writer db.Writer
wrapper wrapping.Wrapper
// 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.
func NewRepository(r db.Reader, w db.Writer, wrapper wrapping.Wrapper) (*Repository, error) {
// 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, wrapper wrapping.Wrapper, opt ...Option) (*Repository, error) {
switch {
case r == nil:
return nil, fmt.Errorf("db.Reader: %w", db.ErrNilParameter)
@@ -29,9 +33,16 @@ func NewRepository(r db.Reader, w db.Writer, wrapper wrapping.Wrapper) (*Reposit
return nil, fmt.Errorf("wrapping.Wrapper: %w", db.ErrNilParameter)
}
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,
wrapper: wrapper,
reader: r,
writer: w,
wrapper: wrapper,
defaultLimit: opts.withLimit,
}, nil
}
+68
View File
@@ -2,6 +2,7 @@ package static
import (
"context"
"errors"
"fmt"
"strings"
@@ -139,3 +140,70 @@ func (r *Repository) UpdateHost(ctx context.Context, h *Host, version uint32, fi
return returnedHost, rowsUpdated, nil
}
// 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) {
if publicId == "" {
return nil, fmt.Errorf("lookup: static host: missing public id %w", db.ErrInvalidParameter)
}
h := allocHost()
h.PublicId = publicId
if err := r.reader.LookupByPublicId(ctx, h); err != nil {
if errors.Is(err, db.ErrRecordNotFound) {
return nil, nil
}
return nil, fmt.Errorf("lookup: static host: failed %w for %s", err, publicId)
}
return h, nil
}
// ListHosts returns a slice of Hosts for the catalogId.
// WithLimit is the only option supported.
func (r *Repository) ListHosts(ctx context.Context, catalogId string, opt ...Option) ([]*Host, error) {
if catalogId == "" {
return nil, fmt.Errorf("list: static host: missing catalog id: %w", db.ErrInvalidParameter)
}
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 hosts []*Host
err := r.reader.SearchWhere(ctx, &hosts, "catalog_id = ?", []interface{}{catalogId}, db.WithLimit(limit))
if err != nil {
return nil, fmt.Errorf("list: static host: %w", err)
}
return hosts, nil
}
// DeleteHost deletes the host for the provided id from the repository
// returning a count of the number of records deleted. All options are
// ignored.
func (r *Repository) DeleteHost(ctx context.Context, publicId string, opt ...Option) (int, error) {
if publicId == "" {
return db.NoRowsAffected, fmt.Errorf("delete: static host: missing public id: %w", db.ErrInvalidParameter)
}
h := allocHost()
h.PublicId = publicId
var rowsDeleted int
_, err := r.writer.DoTx(
ctx, db.StdRetryCnt, db.ExpBackoff{},
func(_ db.Reader, w db.Writer) (err error) {
dh := h.clone()
rowsDeleted, err = w.Delete(ctx, dh, db.WithOplog(r.wrapper, h.oplog(oplog.OpType_OP_TYPE_DELETE)))
if err == nil && rowsDeleted > 1 {
return db.ErrMultipleRecords
}
return err
},
)
if err != nil {
return db.NoRowsAffected, fmt.Errorf("delete: static host: %s: %w", publicId, err)
}
return rowsDeleted, nil
}
@@ -6,6 +6,8 @@ import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/boundary/internal/db"
dbassert "github.com/hashicorp/boundary/internal/db/assert"
"github.com/hashicorp/boundary/internal/host/static/store"
@@ -13,6 +15,7 @@ import (
"github.com/hashicorp/boundary/internal/oplog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/testing/protocmp"
)
func TestRepository_CreateHost(t *testing.T) {
@@ -723,3 +726,231 @@ func TestRepository_UpdateHost(t *testing.T) {
assert.NoError(db.TestVerifyOplog(t, rw, hA.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)))
})
}
func TestRepository_LookupHost(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
_, prj := iam.TestScopes(t, conn)
catalog := testCatalogs(t, conn, prj.PublicId, 1)[0]
host := testHosts(t, conn, catalog.PublicId, 1)[0]
hostId, err := newHostId()
require.NoError(t, err)
var tests = []struct {
name string
in string
want *Host
wantIsErr error
}{
{
name: "with-no-public-id",
wantIsErr: db.ErrInvalidParameter,
},
{
name: "with-non-existing-host-id",
in: hostId,
},
{
name: "with-existing-host-id",
in: host.PublicId,
want: host,
},
}
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, wrapper)
assert.NoError(err)
require.NotNil(repo)
got, err := repo.LookupHost(context.Background(), tt.in)
if tt.wantIsErr != nil {
assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err)
assert.Nil(got)
return
}
require.NoError(err)
assert.EqualValues(tt.want, got)
})
}
}
func TestRepository_ListHosts(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
_, prj := iam.TestScopes(t, conn)
catalogs := testCatalogs(t, conn, prj.PublicId, 2)
catalogA, catalogB := catalogs[0], catalogs[1]
hosts := testHosts(t, conn, catalogA.PublicId, 3)
var tests = []struct {
name string
in string
opts []Option
want []*Host
wantIsErr error
}{
{
name: "with-no-catalog-id",
wantIsErr: db.ErrInvalidParameter,
},
{
name: "Catalog-with-no-hosts",
in: catalogB.PublicId,
want: []*Host{},
},
{
name: "Catalog-with-hosts",
in: catalogA.PublicId,
want: hosts,
},
}
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, wrapper)
assert.NoError(err)
require.NotNil(repo)
got, err := repo.ListHosts(context.Background(), tt.in, tt.opts...)
if tt.wantIsErr != nil {
assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err)
assert.Nil(got)
return
}
require.NoError(err)
opts := []cmp.Option{
cmpopts.SortSlices(func(x, y *Host) bool { return x.PublicId < y.PublicId }),
protocmp.Transform(),
}
assert.Empty(cmp.Diff(tt.want, got, opts...))
})
}
}
func TestRepository_ListHosts_Limits(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
_, prj := iam.TestScopes(t, conn)
catalog := testCatalogs(t, conn, prj.PublicId, 1)[0]
count := 10
hosts := testHosts(t, conn, catalog.PublicId, count)
var 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, wrapper, tt.repoOpts...)
assert.NoError(err)
require.NotNil(repo)
got, err := repo.ListHosts(context.Background(), hosts[0].CatalogId, tt.listOpts...)
require.NoError(err)
assert.Len(got, tt.wantLen)
})
}
}
func TestRepository_DeleteHost(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
_, prj := iam.TestScopes(t, conn)
catalog := testCatalogs(t, conn, prj.PublicId, 1)[0]
host := testHosts(t, conn, catalog.PublicId, 1)[0]
newHostId, err := newHostId()
require.NoError(t, err)
var tests = []struct {
name string
in string
want int
wantIsErr error
}{
{
name: "With no public id",
wantIsErr: db.ErrInvalidParameter,
},
{
name: "With non existing host id",
in: newHostId,
want: 0,
},
{
name: "With existing host id",
in: host.PublicId,
want: 1,
},
}
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, wrapper)
assert.NoError(err)
require.NotNil(repo)
got, err := repo.DeleteHost(context.Background(), tt.in)
if tt.wantIsErr != nil {
assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err)
assert.Zero(got)
return
}
require.NoError(err)
assert.EqualValues(tt.want, got)
})
}
}
+23 -5
View File
@@ -6,6 +6,7 @@ import (
wrapping "github.com/hashicorp/go-kms-wrapping"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/hashicorp/boundary/internal/db"
)
@@ -19,6 +20,7 @@ func TestRepository_New(t *testing.T) {
r db.Reader
w db.Writer
wrapper wrapping.Wrapper
opts []Option
}
var tests = []struct {
@@ -35,9 +37,25 @@ func TestRepository_New(t *testing.T) {
wrapper: wrapper,
},
want: &Repository{
reader: rw,
writer: rw,
reader: rw,
writer: rw,
wrapper: wrapper,
defaultLimit: db.DefaultLimit,
},
},
{
name: "valid-with-limit",
args: args{
r: rw,
w: rw,
wrapper: wrapper,
opts: []Option{WithLimit(5)},
},
want: &Repository{
reader: rw,
writer: rw,
wrapper: wrapper,
defaultLimit: 5,
},
},
{
@@ -84,15 +102,15 @@ func TestRepository_New(t *testing.T) {
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
got, err := NewRepository(tt.args.r, tt.args.w, tt.args.wrapper)
assert, require := assert.New(t), require.New(t)
got, err := NewRepository(tt.args.r, tt.args.w, tt.args.wrapper, tt.args.opts...)
if tt.wantIsErr != nil {
assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err)
assert.Nil(got)
return
}
assert.NoError(err)
assert.NotNil(got)
require.NotNil(got)
assert.Equal(tt.want, got)
})
}