Skip to content

Commit

Permalink
feat(host): Add host set information to static host (#1828)
Browse files Browse the repository at this point in the history
* feat(host): Add host set information to static host

Updates LookupHost and ListHost API to include sets a static host is part of, as like with plugin hosts.

* test: updated TestList_Plugin and TestCrud to be more consistent under race conditions
  • Loading branch information
A-440Hz authored Feb 1, 2022
1 parent 84f6cc6 commit fa00a06
Show file tree
Hide file tree
Showing 9 changed files with 305 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
begin;

-- static_host_with_set_memberships is used for associating a static host instance with all its related host sets
-- in the set_ids column. Currently there are no size limits.
create view static_host_with_set_memberships as
select
h.public_id,
h.create_time,
h.update_time,
h.name,
h.description,
h.catalog_id,
h.address,
h.version,
-- the string_agg(..) column will be null if there are no associated value objects
string_agg(distinct hsm.set_id, '|') as set_ids
from
static_host h
left outer join static_host_set_member hsm on h.public_id = hsm.host_id
group by h.public_id;
comment on view static_host_with_set_memberships is
'static host with its associated host sets';

commit;
1 change: 1 addition & 0 deletions internal/host/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ type Host interface {
GetAddress() string
GetIpAddresses() []string
GetDnsNames() []string
GetSetIds() []string
}
6 changes: 6 additions & 0 deletions internal/host/plugin/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ func (h *Host) oplog(op oplog.OpType) oplog.Metadata {
return metadata
}

// GetSetIds returns host set ids
func (h *Host) GetSetIds() []string {
return h.SetIds
}

// hostAgg is a view that aggregates the host's value objects in to
// string fields delimited with the aggregateDelimiter of "|"
type hostAgg struct {
Expand Down Expand Up @@ -158,6 +163,7 @@ func (agg *hostAgg) TableName() string {
return "host_plugin_host_with_value_obj_and_set_memberships"
}

// GetPublicId returns the host public id as a string
func (agg *hostAgg) GetPublicId() string {
return agg.PublicId
}
70 changes: 68 additions & 2 deletions internal/host/static/host.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package static

import (
"sort"
"strings"

"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/host/static/store"
"github.com/hashicorp/boundary/internal/oplog"
Expand All @@ -15,7 +19,8 @@ const (
// A Host contains a static address.
type Host struct {
*store.Host
tableName string `gorm:"-"`
SetIds []string `gorm:"-"`
tableName string `gorm:"-"`
}

// NewHost creates a new in memory Host for address assigned to catalogId.
Expand Down Expand Up @@ -70,9 +75,18 @@ func allocHost() *Host {

func (h *Host) clone() *Host {
cp := proto.Clone(h.Host)
return &Host{
nh := &Host{
Host: cp.(*store.Host),
}
switch {
case h.SetIds == nil:
case len(h.SetIds) == 0:
nh.SetIds = make([]string, 0)
default:
nh.SetIds = make([]string, len(h.SetIds))
copy(nh.SetIds, h.SetIds)
}
return nh
}

func (h *Host) oplog(op oplog.OpType) oplog.Metadata {
Expand All @@ -86,3 +100,55 @@ func (h *Host) oplog(op oplog.OpType) oplog.Metadata {
}
return metadata
}

// GetSetIds returns host set ids
func (h *Host) GetSetIds() []string {
return h.SetIds
}

type hostAgg struct {
PublicId string `gorm:"primary_key"`
CatalogId string
Name string
Description string
CreateTime *timestamp.Timestamp
UpdateTime *timestamp.Timestamp
Version uint32
Address string
SetIds string
}

func (agg *hostAgg) toHost() *Host {
h := allocHost()
h.PublicId = agg.PublicId
h.CatalogId = agg.CatalogId
h.Name = agg.Name
h.Description = agg.Description
h.CreateTime = agg.CreateTime
h.UpdateTime = agg.UpdateTime
h.Version = agg.Version
h.Address = agg.Address
h.SetIds = agg.getSetIds()
return h
}

// TableName returns the table name for gorm
func (agg *hostAgg) TableName() string {
return "static_host_with_set_memberships"
}

// GetPublicId returns the host public id as a string
func (agg *hostAgg) GetPublicId() string {
return agg.PublicId
}

// GetSetIds returns a list of all associated host sets to the host
func (agg *hostAgg) getSetIds() []string {
const aggregateDelimiter = "|"
var ids []string
if agg.SetIds != "" {
ids = strings.Split(agg.SetIds, aggregateDelimiter)
sort.Strings(ids)
}
return ids
}
25 changes: 19 additions & 6 deletions internal/host/static/repository_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,13 @@ func (r *Repository) UpdateHost(ctx context.Context, scopeId string, h *Host, ve
if rowsUpdated > 1 {
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been updated")
}
ha := &hostAgg{
PublicId: h.PublicId,
}
if err := r.reader.LookupByPublicId(ctx, ha); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to lookup host after update"))
}
returnedHost.SetIds = ha.getSetIds()
return nil
},
)
Expand Down Expand Up @@ -203,15 +210,16 @@ func (r *Repository) LookupHost(ctx context.Context, publicId string, opt ...Opt
if publicId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "no public id")
}
h := allocHost()
h.PublicId = publicId
if err := r.reader.LookupByPublicId(ctx, h); err != nil {
ha := &hostAgg{
PublicId: publicId,
}
if err := r.reader.LookupByPublicId(ctx, ha); err != nil {
if errors.IsNotFoundError(err) {
return nil, nil
}
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", publicId)))
}
return h, nil
return ha.toHost(), nil
}

// ListHosts returns a slice of Hosts for the catalogId.
Expand All @@ -227,11 +235,16 @@ func (r *Repository) ListHosts(ctx context.Context, catalogId string, opt ...Opt
// 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))
var aggs []*hostAgg
err := r.reader.SearchWhere(ctx, &aggs, "catalog_id = ?", []interface{}{catalogId}, db.WithLimit(limit))
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
hosts := make([]*Host, 0, len(aggs))
for _, ha := range aggs {
hosts = append(hosts, ha.toHost())
}

return hosts, nil
}

Expand Down
147 changes: 147 additions & 0 deletions internal/host/static/repository_host_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package static

import (
"context"
"sort"
"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/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/host/static/store"
"github.com/hashicorp/boundary/internal/iam"
Expand Down Expand Up @@ -604,6 +606,10 @@ func TestRepository_UpdateHost(t *testing.T) {
assert.NoError(err)
require.NotNil(orig)

set := TestSets(t, conn, catalog.GetPublicId(), 1)[0]
TestSetMembers(t, conn, set.PublicId, []*Host{orig})
wantSetIds := []string{set.PublicId}

if tt.chgFn != nil {
orig = tt.chgFn(orig)
}
Expand All @@ -628,6 +634,7 @@ func TestRepository_UpdateHost(t *testing.T) {
dbassert.IsNull(got, "name")
return
}
assert.Equal(wantSetIds, got.SetIds)
assert.Equal(tt.want.Name, got.Name)
if tt.want.Description == "" {
dbassert.IsNull(got, "description")
Expand Down Expand Up @@ -798,6 +805,72 @@ func TestRepository_LookupHost(t *testing.T) {
}
}

func TestRepository_LookupHost_HostSets(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)
catalog := TestCatalogs(t, conn, prj.PublicId, 1)[0]
hosts := TestHosts(t, conn, catalog.PublicId, 3)
hostA, hostB, hostC := hosts[0], hosts[1], hosts[2]
setB := TestSets(t, conn, catalog.PublicId, 1)[0]
setsC := TestSets(t, conn, catalog.PublicId, 5)
TestSetMembers(t, conn, setB.PublicId, []*Host{hostB})
hostB.SetIds = []string{setB.PublicId}
for _, s := range setsC {
hostC.SetIds = append(hostC.SetIds, s.PublicId)
TestSetMembers(t, conn, s.PublicId, []*Host{hostC})
}
sort.Slice(hostC.SetIds, func(i, j int) bool {
return hostC.SetIds[i] < hostC.SetIds[j]
})

tests := []struct {
name string
in string
want *Host
wantIsErr errors.Code
}{
{
name: "with-zero-hostsets",
in: hostA.PublicId,
want: hostA,
},
{
name: "with-one-hostset",
in: hostB.PublicId,
want: hostB,
},
{
name: "with-many-hostsets",
in: hostC.PublicId,
want: hostC,
},
}

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.LookupHost(context.Background(), tt.in)
assert.Empty(
cmp.Diff(
tt.want,
got,
cmpopts.IgnoreUnexported(Host{}, store.Host{}),
cmpopts.IgnoreTypes(&timestamp.Timestamp{}),
),
)
})
}
}

func TestRepository_ListHosts(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
Expand Down Expand Up @@ -857,6 +930,80 @@ func TestRepository_ListHosts(t *testing.T) {
}
}

func TestRepository_ListHosts_HostSets(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)

// testing for full and empty hostset associations
catalogs := TestCatalogs(t, conn, prj.PublicId, 3)
catalogA, catalogB, catalogC := catalogs[0], catalogs[1], catalogs[2]
hostsA := TestHosts(t, conn, catalogA.PublicId, 3)
setA := TestSets(t, conn, catalogA.PublicId, 1)[0]
TestSetMembers(t, conn, setA.PublicId, hostsA)
for _, h := range hostsA {
h.SetIds = []string{setA.PublicId}
}
hostsB := TestHosts(t, conn, catalogB.PublicId, 3)

// testing for mixed hosts with individual hostsets and empty hostsets
hostsC := TestHosts(t, conn, catalogC.PublicId, 5)
hostsC0 := TestHosts(t, conn, catalogC.PublicId, 2)
setC := TestSets(t, conn, catalogC.PublicId, 5)
for i, h := range hostsC {
h.SetIds = []string{setC[i].PublicId}
TestSetMembers(t, conn, setC[i].PublicId, []*Host{hostsC[i]})
}
hostsC = append(hostsC, hostsC0...)

tests := []struct {
name string
in string
want []*Host
}{
{
name: "with-hostsets",
in: catalogA.PublicId,
want: hostsA,
},
{
name: "empty-hostsets",
in: catalogB.PublicId,
want: hostsB,
},
{
name: "mixed-hostsets",
in: catalogC.PublicId,
want: hostsC,
},
}
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.ListHosts(context.Background(), tt.in)
require.NoError(err)
assert.Empty(
cmp.Diff(
tt.want,
got,
cmpopts.IgnoreUnexported(Host{}, store.Host{}),
cmpopts.IgnoreTypes(&timestamp.Timestamp{}),
cmpopts.SortSlices(func(x, y *Host) bool {
return x.GetPublicId() < y.GetPublicId()
}),
),
)
})
}
}

func TestRepository_ListHosts_Limits(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
Expand Down
Loading

0 comments on commit fa00a06

Please sign in to comment.