mirror of https://github.com/hashicorp/boundary
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
462 lines
13 KiB
462 lines
13 KiB
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package plugin
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"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/db/timestamp"
|
|
"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"
|
|
"github.com/hashicorp/boundary/internal/plugin"
|
|
"github.com/hashicorp/boundary/internal/plugin/loopback"
|
|
plgstore "github.com/hashicorp/boundary/internal/plugin/store"
|
|
"github.com/hashicorp/boundary/internal/scheduler"
|
|
plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestJob_UpsertHosts(t *testing.T) {
|
|
ctx := context.Background()
|
|
conn, _ := db.TestSetup(t, "postgres")
|
|
rw := db.New(conn)
|
|
wrapper := db.TestWrapper(t)
|
|
sched := scheduler.TestScheduler(t, conn, wrapper)
|
|
kms := kms.TestKms(t, conn, wrapper)
|
|
iamRepo := iam.TestRepo(t, conn, wrapper)
|
|
_, prj := iam.TestScopes(t, iamRepo)
|
|
|
|
plg := plugin.TestPlugin(t, conn, "create")
|
|
plgm := map[string]plgpb.HostPluginServiceClient{
|
|
plg.GetPublicId(): loopback.NewWrappingPluginHostClient(&plgpb.UnimplementedHostPluginServiceServer{}),
|
|
}
|
|
|
|
catalog := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId())
|
|
const setCount int = 3
|
|
setIds := make([]string, 0, setCount)
|
|
for i := 0; i < setCount; i++ {
|
|
set := TestSet(t, conn, kms, sched, catalog, plgm)
|
|
setIds = append(setIds, set.GetPublicId())
|
|
}
|
|
sort.Strings(setIds)
|
|
phs, exp := TestExternalHosts(t, catalog, setIds, setCount)
|
|
phs[2].Name = phs[0].Name
|
|
exp[2].Name = exp[0].Name
|
|
|
|
type input struct {
|
|
catalog *HostCatalog
|
|
sets []string
|
|
phs []*plgpb.ListHostsResponseHost
|
|
exp []*Host
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
in func() *input
|
|
opts []Option
|
|
wantIsErr errors.Code
|
|
}{
|
|
{
|
|
name: "nil-hosts",
|
|
in: func() *input {
|
|
return &input{
|
|
catalog: catalog,
|
|
sets: setIds,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "no-external-id-hosts",
|
|
in: func() *input {
|
|
testPhs, _ := TestExternalHosts(t, catalog, setIds, setCount)
|
|
testPhs[1].ExternalId = ""
|
|
return &input{
|
|
catalog: catalog,
|
|
sets: setIds,
|
|
phs: testPhs,
|
|
}
|
|
},
|
|
wantIsErr: errors.InvalidParameter,
|
|
},
|
|
{
|
|
name: "nil-catalog",
|
|
in: func() *input {
|
|
return &input{
|
|
sets: setIds,
|
|
phs: phs,
|
|
}
|
|
},
|
|
wantIsErr: errors.InvalidParameter,
|
|
},
|
|
{
|
|
name: "no-catalog-id",
|
|
in: func() *input {
|
|
cat := catalog.clone()
|
|
cat.PublicId = ""
|
|
return &input{
|
|
catalog: cat,
|
|
sets: setIds,
|
|
phs: phs,
|
|
}
|
|
},
|
|
wantIsErr: errors.InvalidParameter,
|
|
},
|
|
{
|
|
name: "no-project-id",
|
|
in: func() *input {
|
|
cat := catalog.clone()
|
|
cat.ProjectId = ""
|
|
return &input{
|
|
catalog: cat,
|
|
sets: setIds,
|
|
phs: phs,
|
|
}
|
|
},
|
|
wantIsErr: errors.InvalidParameter,
|
|
},
|
|
{
|
|
name: "nil-sets",
|
|
in: func() *input {
|
|
return &input{
|
|
catalog: catalog,
|
|
phs: phs,
|
|
}
|
|
},
|
|
wantIsErr: errors.InvalidParameter,
|
|
},
|
|
{
|
|
name: "no-sets",
|
|
in: func() *input {
|
|
return &input{
|
|
catalog: catalog,
|
|
sets: make([]string, 0),
|
|
phs: phs,
|
|
}
|
|
},
|
|
wantIsErr: errors.InvalidParameter,
|
|
},
|
|
{
|
|
name: "valid", // Note: this also tests duplicate names
|
|
in: func() *input {
|
|
return &input{
|
|
catalog: catalog,
|
|
sets: setIds,
|
|
phs: phs,
|
|
exp: exp,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "valid-changed-values",
|
|
in: func() *input {
|
|
ph := phs[1]
|
|
e := exp[1]
|
|
newIp := testGetIpv4Address(t)
|
|
newName := testGetDnsName(t)
|
|
ph.IpAddresses = append(ph.IpAddresses, newIp)
|
|
e.IpAddresses = append(e.IpAddresses, newIp)
|
|
ph.DnsNames = append(ph.DnsNames, newName)
|
|
e.DnsNames = append(e.DnsNames, newName)
|
|
// These are sorted by the repo function, so we need to match
|
|
sort.Strings(e.IpAddresses)
|
|
sort.Strings(e.DnsNames)
|
|
|
|
ph.Name, ph.Description = ph.Description, ph.Name
|
|
e.Name, e.Description = e.Description, e.Name
|
|
|
|
ph.SetIds = ph.SetIds[0 : len(ph.SetIds)-1]
|
|
e.SetIds = e.SetIds[0 : len(e.SetIds)-1]
|
|
e.Version++
|
|
return &input{
|
|
catalog: catalog,
|
|
sets: setIds,
|
|
phs: phs,
|
|
exp: exp,
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
job, err := newSetSyncJob(ctx, rw, rw, kms, plgm)
|
|
require.NoError(err)
|
|
require.NotNil(job)
|
|
in := tt.in()
|
|
got, err := job.upsertAndCleanHosts(ctx, in.catalog, in.sets, in.phs, 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, fmt.Sprintf("%v", in.catalog))
|
|
|
|
// Basic tests
|
|
assert.Len(got, len(in.phs))
|
|
for _, h := range in.exp {
|
|
assert.NoError(db.TestVerifyOplog(t, rw, h.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)))
|
|
}
|
|
|
|
// Make sure outputs match. Ignore timestamps.
|
|
assert.Empty(
|
|
cmp.Diff(
|
|
in.exp,
|
|
got,
|
|
cmpopts.IgnoreUnexported(Host{}, store.Host{}),
|
|
cmpopts.IgnoreTypes(×tamp.Timestamp{}),
|
|
cmpopts.SortSlices(func(x, y *Host) bool {
|
|
return x.GetPublicId() < y.GetPublicId()
|
|
}),
|
|
cmpopts.SortSlices(func(x, y string) bool {
|
|
return x < y
|
|
}),
|
|
),
|
|
)
|
|
|
|
repo, err := NewRepository(ctx, rw, rw, kms, sched, plgm)
|
|
require.NoError(err)
|
|
require.NotNil(repo)
|
|
|
|
// Check again, but via performing an explicit list
|
|
var gotPlg *plugin.Plugin
|
|
got, gotPlg, _, err = repo.listHosts(ctx, in.catalog.GetPublicId())
|
|
require.NoError(err)
|
|
assert.Len(got, len(in.phs))
|
|
assert.Empty(
|
|
cmp.Diff(
|
|
in.exp,
|
|
got,
|
|
cmpopts.IgnoreUnexported(Host{}, store.Host{}),
|
|
cmpopts.IgnoreTypes(×tamp.Timestamp{}),
|
|
cmpopts.SortSlices(func(x, y *Host) bool {
|
|
return x.GetPublicId() < y.GetPublicId()
|
|
}),
|
|
cmpopts.SortSlices(func(x, y string) bool {
|
|
return x < y
|
|
}),
|
|
),
|
|
)
|
|
plg := plg
|
|
if len(in.phs) == 0 {
|
|
plg = nil
|
|
}
|
|
assert.Empty(
|
|
cmp.Diff(
|
|
plg,
|
|
gotPlg,
|
|
cmpopts.IgnoreUnexported(plugin.Plugin{}, plgstore.Plugin{}),
|
|
cmpopts.IgnoreTypes(×tamp.Timestamp{}),
|
|
),
|
|
)
|
|
|
|
// Now individually call read on each host, cache the matching set
|
|
// IDs, and then check membership
|
|
setIdMap := make(map[string][]string)
|
|
for _, exp := range in.exp {
|
|
for _, setId := range exp.SetIds {
|
|
setIdMap[setId] = append(setIdMap[setId], exp.GetPublicId())
|
|
}
|
|
got, gotPlg, err := repo.LookupHost(ctx, exp.GetPublicId())
|
|
require.NoError(err)
|
|
require.NotNil(got)
|
|
assert.NotEmpty(got.SetIds)
|
|
assert.Empty(
|
|
cmp.Diff(
|
|
exp,
|
|
got,
|
|
cmpopts.IgnoreUnexported(Host{}, store.Host{}),
|
|
cmpopts.IgnoreTypes(×tamp.Timestamp{}),
|
|
cmpopts.SortSlices(func(x, y string) bool {
|
|
return x < y
|
|
}),
|
|
),
|
|
)
|
|
assert.Empty(
|
|
cmp.Diff(
|
|
plg,
|
|
gotPlg,
|
|
cmpopts.IgnoreUnexported(plugin.Plugin{}, plgstore.Plugin{}),
|
|
cmpopts.IgnoreTypes(×tamp.Timestamp{}),
|
|
),
|
|
)
|
|
}
|
|
for setId, expHostIds := range setIdMap {
|
|
got, err = repo.ListHostsBySetIds(ctx, []string{setId})
|
|
require.NoError(err)
|
|
var gotHostIds []string
|
|
for _, h := range got {
|
|
gotHostIds = append(gotHostIds, h.GetPublicId())
|
|
}
|
|
assert.ElementsMatch(expHostIds, gotHostIds)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRepository_ListHostsByCatalogId(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
conn, _ := db.TestSetup(t, "postgres")
|
|
wrapper := db.TestWrapper(t)
|
|
kms := kms.TestKms(t, conn, wrapper)
|
|
iamRepo := iam.TestRepo(t, conn, wrapper)
|
|
_, proj1 := iam.TestScopes(t, iamRepo)
|
|
sched := scheduler.TestScheduler(t, conn, wrapper)
|
|
plg := plugin.TestPlugin(t, conn, "test")
|
|
plgm := map[string]plgpb.HostPluginServiceClient{
|
|
plg.GetPublicId(): &loopback.WrappingPluginHostClient{Server: &loopback.TestPluginServer{}},
|
|
}
|
|
catalog := TestCatalog(t, conn, proj1.GetPublicId(), plg.GetPublicId())
|
|
|
|
total := 5
|
|
for i := 0; i < total; i++ {
|
|
TestHost(t, conn, catalog.GetPublicId(), fmt.Sprintf("ext-id-%d", i))
|
|
}
|
|
|
|
rw := db.New(conn)
|
|
repo, err := NewRepository(ctx, rw, rw, kms, sched, plgm)
|
|
require.NoError(t, err)
|
|
|
|
t.Run("no-options", func(t *testing.T) {
|
|
got, retPlg, ttime, err := repo.listHosts(ctx, catalog.GetPublicId())
|
|
require.NoError(t, err)
|
|
assert.Equal(t, total, len(got))
|
|
assert.Equal(t, retPlg, plg)
|
|
// Transaction timestamp should be within ~10 seconds of now
|
|
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
|
|
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
|
|
})
|
|
|
|
t.Run("withStartPageAfter", func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
|
|
page1, retPlg, ttime, err := repo.listHosts(
|
|
context.Background(),
|
|
catalog.GetPublicId(),
|
|
WithLimit(2),
|
|
)
|
|
require.NoError(err)
|
|
require.Len(page1, 2)
|
|
assert.Equal(retPlg, plg)
|
|
assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
|
|
assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
|
|
page2, retPlg, ttime, err := repo.listHosts(
|
|
context.Background(),
|
|
catalog.GetPublicId(),
|
|
WithLimit(2),
|
|
WithStartPageAfterItem(page1[1]),
|
|
)
|
|
require.NoError(err)
|
|
require.Len(page2, 2)
|
|
assert.Equal(retPlg, plg)
|
|
assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
|
|
assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
|
|
for _, item := range page1 {
|
|
assert.NotEqual(item.GetPublicId(), page2[0].GetPublicId())
|
|
assert.NotEqual(item.GetPublicId(), page2[1].GetPublicId())
|
|
}
|
|
page3, retPlg, ttime, err := repo.listHosts(
|
|
context.Background(),
|
|
catalog.GetPublicId(),
|
|
WithLimit(2),
|
|
WithStartPageAfterItem(page2[1]),
|
|
)
|
|
require.NoError(err)
|
|
require.Len(page3, 1)
|
|
assert.Equal(retPlg, plg)
|
|
assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
|
|
assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
|
|
for _, item := range page2 {
|
|
assert.NotEqual(item.GetPublicId(), page3[0].GetPublicId())
|
|
}
|
|
page4, retPlg, ttime, err := repo.listHosts(
|
|
context.Background(),
|
|
catalog.GetPublicId(),
|
|
WithLimit(2),
|
|
WithStartPageAfterItem(page3[0]),
|
|
)
|
|
require.NoError(err)
|
|
require.Empty(page4)
|
|
require.Empty(retPlg)
|
|
assert.True(time.Now().Before(ttime.Add(10 * time.Second)))
|
|
assert.True(time.Now().After(ttime.Add(-10 * time.Second)))
|
|
})
|
|
}
|
|
|
|
func Test_listDeletedHostIds(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
conn, _ := db.TestSetup(t, "postgres")
|
|
wrapper := db.TestWrapper(t)
|
|
testKms := kms.TestKms(t, conn, wrapper)
|
|
sched := scheduler.TestScheduler(t, conn, wrapper)
|
|
plg := plugin.TestPlugin(t, conn, "test")
|
|
plgm := map[string]plgpb.HostPluginServiceClient{
|
|
plg.GetPublicId(): &loopback.WrappingPluginHostClient{Server: &loopback.TestPluginServer{}},
|
|
}
|
|
|
|
rw := db.New(conn)
|
|
repo, err := NewRepository(ctx, rw, rw, testKms, sched, plgm)
|
|
require.NoError(t, err)
|
|
|
|
// Expect no entries
|
|
deletedIds, ttime, err := repo.listDeletedHostIds(ctx, time.Now().AddDate(-1, 0, 0))
|
|
require.NoError(t, err)
|
|
require.Empty(t, deletedIds)
|
|
// Transaction timestamp should be within ~10 seconds of now
|
|
assert.True(t, time.Now().Before(ttime.Add(10*time.Second)))
|
|
assert.True(t, time.Now().After(ttime.Add(-10*time.Second)))
|
|
|
|
// It's not possible to delete a plugin host, so this is kinda hard to test
|
|
}
|
|
|
|
func Test_estimatedHostCount(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
conn, _ := db.TestSetup(t, "postgres")
|
|
sqlDb, err := conn.SqlDB(ctx)
|
|
require.NoError(t, err)
|
|
wrapper := db.TestWrapper(t)
|
|
testKms := kms.TestKms(t, conn, wrapper)
|
|
iamRepo := iam.TestRepo(t, conn, wrapper)
|
|
_, proj1 := iam.TestScopes(t, iamRepo)
|
|
sched := scheduler.TestScheduler(t, conn, wrapper)
|
|
plg := plugin.TestPlugin(t, conn, "test")
|
|
plgm := map[string]plgpb.HostPluginServiceClient{
|
|
plg.GetPublicId(): &loopback.WrappingPluginHostClient{Server: &loopback.TestPluginServer{}},
|
|
}
|
|
catalog := TestCatalog(t, conn, proj1.GetPublicId(), plg.GetPublicId())
|
|
|
|
rw := db.New(conn)
|
|
repo, err := NewRepository(ctx, rw, rw, testKms, sched, plgm)
|
|
require.NoError(t, err)
|
|
|
|
// Check total entries at start, expect 0
|
|
numItems, err := repo.estimatedHostCount(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, numItems)
|
|
|
|
// Create a host, expect 1 entries
|
|
TestHost(t, conn, catalog.GetPublicId(), "ext-id")
|
|
// Run analyze to update estimate
|
|
_, err = sqlDb.ExecContext(ctx, "analyze")
|
|
require.NoError(t, err)
|
|
numItems, err = repo.estimatedHostCount(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, numItems)
|
|
}
|