Files
threshold/internal/host/plugin/repository_host_set_test.go
T
hashicorp-copywrite[bot]andJohan Brandhorst-Satzkorn 29da0bcb92 [COMPLIANCE] License changes (#3567)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* Update copyright file headers to BUS-1.1

* Rerun make gen

This will pick up the last of the license changes

* Revert "Rerun make gen"

This reverts commit dbe0968bffd92745d8663c6940debae098395a6b.

* Remove copywrite from generation script

The current version of copywrite is not sophisticated enough
to only apply the copywrite changes to certain subdirectories,
so to avoid accidentally relicensing our api and sdk modules,
we will stop running the copywrite script until such time
that it can do the right thing.

* License protobuf API as MPL

The protobuf API files are used to generate the SDK,
since we want the SDK to be MPL, the protobuf files
must also be MPL. This extends to all protobuf files
whose sources end up in the SDK directory, including:

- internal/proto/controller/api
- internal/proto/controller/custom_options
- internal/proto/plugin

It also includes the generated service directory in
internal/gen/controller/api/services, since they are
generated from the protobuf sources used for the SDK.

* Change website license file name

This makes the naming scheme consistent with the rest
of the repository.

* Add note on licensing to README

* Set copywrite license to BUSL

* Revert "Add note on licensing to README"

This reverts commit 4d865a91a7a4f11605f0dc4609355d7e3c3cfffe.

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
2023-08-11 10:31:53 -07:00

1806 lines
55 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package plugin
import (
"context"
"fmt"
"sort"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/boundary/globals"
"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"
"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/libs/patchstruct"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/plugin"
"github.com/hashicorp/boundary/internal/plugin/loopback"
"github.com/hashicorp/boundary/internal/scheduler"
plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/mitchellh/mapstructure"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
func TestRepository_CreateSet(t *testing.T) {
ctx := context.Background()
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)
sched := scheduler.TestScheduler(t, conn, wrapper)
_, prj := iam.TestScopes(t, iamRepo)
plg := plugin.TestPlugin(t, conn, "create")
unimplementedPlugin := plugin.TestPlugin(t, conn, "unimplemented")
catalog := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId())
unimplementedPluginCatalog := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId())
attrs := []byte{}
const normalizeToSliceKey = "normalize_to_slice"
tests := []struct {
name string
in *HostSet
opts []Option
want *HostSet
wantPluginCalled bool
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: "invalid-sync-interval-too-negative",
in: &HostSet{
HostSet: &store.HostSet{
CatalogId: catalog.PublicId,
SyncIntervalSeconds: -99,
Attributes: attrs,
},
},
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,
},
},
wantPluginCalled: true,
},
{
name: "valid-preferred-endpoints",
in: &HostSet{
HostSet: &store.HostSet{
CatalogId: catalog.PublicId,
Attributes: attrs,
},
PreferredEndpoints: []string{"cidr:1.2.3.4/32", "dns:a.b.c"},
},
want: &HostSet{
HostSet: &store.HostSet{
CatalogId: catalog.PublicId,
Attributes: attrs,
},
PreferredEndpoints: []string{"cidr:1.2.3.4/32", "dns:a.b.c"},
},
wantPluginCalled: true,
},
{
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,
},
},
wantPluginCalled: true,
},
{
name: "valid-sync-interval-disabled",
in: &HostSet{
HostSet: &store.HostSet{
CatalogId: catalog.PublicId,
SyncIntervalSeconds: -1,
Attributes: attrs,
},
},
want: &HostSet{
HostSet: &store.HostSet{
CatalogId: catalog.PublicId,
SyncIntervalSeconds: -1,
Attributes: attrs,
},
},
wantPluginCalled: true,
},
{
name: "valid-sync-interval-positive",
in: &HostSet{
HostSet: &store.HostSet{
CatalogId: catalog.PublicId,
SyncIntervalSeconds: 60,
Attributes: attrs,
},
},
want: &HostSet{
HostSet: &store.HostSet{
CatalogId: catalog.PublicId,
SyncIntervalSeconds: 60,
Attributes: attrs,
},
},
wantPluginCalled: true,
},
{
name: "valid-unimplemented-plugin",
in: &HostSet{
HostSet: &store.HostSet{
CatalogId: unimplementedPluginCatalog.PublicId,
Name: "valid-unimplemented-plugin",
Attributes: attrs,
},
},
want: &HostSet{
HostSet: &store.HostSet{
CatalogId: unimplementedPluginCatalog.PublicId,
Name: "valid-unimplemented-plugin",
Attributes: attrs,
},
},
wantPluginCalled: true,
},
{
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,
},
},
wantPluginCalled: true,
},
{
name: "valid-with-custom-attributes",
in: &HostSet{
HostSet: &store.HostSet{
CatalogId: catalog.PublicId,
Description: ("test-description-repo"),
Attributes: func() []byte {
st, err := structpb.NewStruct(map[string]any{
"k1": nil,
"removed": nil,
normalizeToSliceKey: "normalizeme",
})
require.NoError(t, err)
b, err := proto.Marshal(st)
require.NoError(t, err)
return b
}(),
},
},
want: &HostSet{
HostSet: &store.HostSet{
CatalogId: catalog.PublicId,
Description: ("test-description-repo"),
Attributes: func() []byte {
b, err := proto.Marshal(&structpb.Struct{Fields: map[string]*structpb.Value{
normalizeToSliceKey: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("normalizeme"),
},
}),
}})
require.NoError(t, err)
return b
}(),
},
},
wantPluginCalled: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
var origPluginAttrs, pluginReceivedAttrs *structpb.Struct
if tt.in != nil && tt.in.HostSet != nil && len(tt.in.Attributes) > 0 {
origPluginAttrs = new(structpb.Struct)
require.NoError(proto.Unmarshal(tt.in.Attributes, origPluginAttrs))
}
var pluginCalled bool
plgm := map[string]plgpb.HostPluginServiceClient{
plg.GetPublicId(): loopback.NewWrappingPluginHostClient(loopback.TestPluginHostServer{
NormalizeSetDataFn: func(_ context.Context, req *plgpb.NormalizeSetDataRequest) (*plgpb.NormalizeSetDataResponse, error) {
if req.Attributes == nil {
return new(plgpb.NormalizeSetDataResponse), nil
}
var attrs struct {
NormalizeToSlice string `mapstructure:"normalize_to_slice"`
}
require.NoError(mapstructure.Decode(req.Attributes.AsMap(), &attrs))
if attrs.NormalizeToSlice == "" {
return new(plgpb.NormalizeSetDataResponse), nil
}
retAttrs := proto.Clone(req.Attributes).(*structpb.Struct)
retAttrs.Fields[normalizeToSliceKey] = structpb.NewListValue(&structpb.ListValue{
Values: []*structpb.Value{structpb.NewStringValue(attrs.NormalizeToSlice)},
})
return &plgpb.NormalizeSetDataResponse{Attributes: retAttrs}, nil
},
OnCreateSetFn: func(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) {
pluginCalled = true
pluginReceivedAttrs = req.GetSet().GetAttributes()
return &plgpb.OnCreateSetResponse{}, nil
},
}),
unimplementedPlugin.GetPublicId(): loopback.NewWrappingPluginHostClient(loopback.TestPluginHostServer{OnCreateSetFn: func(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) {
pluginCalled = true
pluginReceivedAttrs = req.GetSet().GetAttributes()
return plgpb.UnimplementedHostPluginServiceServer{}.OnCreateSet(ctx, req)
}}),
}
repo, err := NewRepository(ctx, rw, rw, kms, sched, plgm)
require.NoError(err)
require.NotNil(repo)
got, plgInfo, err := repo.CreateSet(ctx, prj.GetPublicId(), tt.in, tt.opts...)
assert.Equal(tt.wantPluginCalled, pluginCalled)
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(), globals.PluginHostSetPrefix))
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.Equal(string(tt.want.GetAttributes()), string(got.GetAttributes()))
if origPluginAttrs != nil {
if normalizeVal := origPluginAttrs.Fields[normalizeToSliceKey]; normalizeVal != nil {
gotVal := pluginReceivedAttrs.Fields[normalizeToSliceKey]
require.NotNil(gotVal)
listVal := gotVal.GetListValue()
require.NotNil(listVal)
require.Len(listVal.Values, 1)
assert.Equal(normalizeVal.GetStringValue(), listVal.Values[0].GetStringValue())
origPluginAttrs.Fields[normalizeToSliceKey] = structpb.NewListValue(listVal)
tt.want.Attributes, err = proto.Marshal(origPluginAttrs)
require.NoError(err)
tt.want.Attributes, err = patchstruct.PatchBytes([]byte{}, tt.want.Attributes)
require.NoError(err)
}
}
wantedPluginAttributes := &structpb.Struct{}
require.NoError(proto.Unmarshal(tt.want.Attributes, wantedPluginAttributes))
assert.Empty(cmp.Diff(wantedPluginAttributes, pluginReceivedAttrs, protocmp.Transform()))
assert.Empty(cmp.Diff(plgInfo, plg, protocmp.Transform()))
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)
var pluginCalled bool
plgm := map[string]plgpb.HostPluginServiceClient{
plg.GetPublicId(): loopback.NewWrappingPluginHostClient(loopback.TestPluginHostServer{OnCreateSetFn: func(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) {
pluginCalled = true
return &plgpb.OnCreateSetResponse{}, nil
}}),
}
repo, err := NewRepository(ctx, rw, rw, kms, sched, plgm)
require.NoError(err)
require.NotNil(repo)
_, prj := iam.TestScopes(t, iamRepo)
catalog := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId())
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(pluginCalled)
assert.True(strings.HasPrefix(got.GetPublicId(), globals.PluginHostSetPrefix))
assert.NotSame(in, got)
assert.Equal(in.Name, got.GetName())
assert.Equal(in.Description, got.GetDescription())
assert.Equal(got.GetCreateTime(), got.GetUpdateTime())
// reset pluginCalled so we can see the duplicate name causes the plugin
// to not get called.
pluginCalled = false
got2, _, err := repo.CreateSet(context.Background(), prj.GetPublicId(), in)
assert.False(pluginCalled)
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)
var pluginCalled bool
plgm := map[string]plgpb.HostPluginServiceClient{
plg.GetPublicId(): loopback.NewWrappingPluginHostClient(loopback.TestPluginHostServer{OnCreateSetFn: func(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) {
pluginCalled = true
return &plgpb.OnCreateSetResponse{}, nil
}}),
}
repo, err := NewRepository(ctx, rw, rw, kms, sched, plgm)
require.NoError(err)
require.NotNil(repo)
_, prj := iam.TestScopes(t, iamRepo)
catalogA := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId())
catalogB := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId())
in := &HostSet{
HostSet: &store.HostSet{
Name: "test-name-repo",
Attributes: []byte{},
},
}
in2 := in.clone()
in.CatalogId = catalogA.PublicId
// reset pluginCalled so we can see that the plugin is called during this
// createSet call
pluginCalled = false
got, _, err := repo.CreateSet(context.Background(), prj.GetPublicId(), in)
assert.True(pluginCalled)
require.NoError(err)
require.NotNil(got)
assert.True(strings.HasPrefix(got.GetPublicId(), globals.PluginHostSetPrefix))
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
// reset pluginCalled so we can see that the plugin is called during this
// createSet call
pluginCalled = false
got2, _, err := repo.CreateSet(context.Background(), prj.GetPublicId(), in2)
require.NoError(err)
require.NotNil(got2)
assert.True(pluginCalled)
assert.True(strings.HasPrefix(got.GetPublicId(), globals.PluginHostSetPrefix))
assert.NotSame(in2, got2)
assert.Equal(in2.Name, got2.GetName())
assert.Equal(in2.Description, got2.GetDescription())
assert.Equal(got2.GetCreateTime(), got2.GetUpdateTime())
})
}
func TestRepository_UpdateSet(t *testing.T) {
ctx := context.Background()
dbConn, _ := db.TestSetup(t, "postgres")
dbRW := db.New(dbConn)
dbWrapper := db.TestWrapper(t)
sched := scheduler.TestScheduler(t, dbConn, dbWrapper)
dbKmsCache := kms.TestKms(t, dbConn, dbWrapper)
_, projectScope := iam.TestScopes(t, iam.TestRepo(t, dbConn, dbWrapper))
testPlugin := plugin.TestPlugin(t, dbConn, "test")
dummyPluginMap := map[string]plgpb.HostPluginServiceClient{
testPlugin.GetPublicId(): &loopback.WrappingPluginHostClient{Server: &plgpb.UnimplementedHostPluginServiceServer{}},
}
// Set up a test catalog and the secrets for it
testCatalog := TestCatalog(t, dbConn, projectScope.PublicId, testPlugin.GetPublicId())
testCatalogSecret, err := newHostCatalogSecret(ctx, testCatalog.GetPublicId(), mustStruct(map[string]any{
"one": "two",
}))
require.NoError(t, err)
scopeWrapper, err := dbKmsCache.GetWrapper(ctx, testCatalog.GetProjectId(), kms.KeyPurposeDatabase)
require.NoError(t, err)
require.NoError(t, testCatalogSecret.encrypt(ctx, scopeWrapper))
err = dbRW.Create(ctx, testCatalogSecret)
require.NoError(t, err)
const normalizeToSliceKey = "normalize_to_slice"
// Create a test duplicate set. We don't use this set, it just
// exists to ensure that we can test for conflicts when setting
// the name.
const testDuplicateSetName = "duplicate-set-name"
testDuplicateSet := TestSet(t, dbConn, dbKmsCache, sched, testCatalog, dummyPluginMap)
testDuplicateSet.Name = testDuplicateSetName
setsUpdated, err := dbRW.Update(ctx, testDuplicateSet, []string{"name"}, []string{})
require.NoError(t, err)
require.Equal(t, 1, setsUpdated)
// Define some helpers here to make the test table more readable.
type changeHostSetFunc func(s *HostSet) *HostSet
changeSetToNil := func() changeHostSetFunc {
return func(_ *HostSet) *HostSet {
return nil
}
}
changeEmbeddedSetToNil := func() changeHostSetFunc {
return func(s *HostSet) *HostSet {
s.HostSet = nil
return s
}
}
changePublicId := func(v string) changeHostSetFunc {
return func(s *HostSet) *HostSet {
s.PublicId = v
return s
}
}
changeName := func(v string) changeHostSetFunc {
return func(s *HostSet) *HostSet {
s.Name = v
return s
}
}
changeDescription := func(s string) changeHostSetFunc {
return func(c *HostSet) *HostSet {
c.Description = s
return c
}
}
changeSyncInterval := func(s int32) changeHostSetFunc {
return func(c *HostSet) *HostSet {
c.SyncIntervalSeconds = s
return c
}
}
changePreferredEndpoints := func(s []string) changeHostSetFunc {
return func(c *HostSet) *HostSet {
c.PreferredEndpoints = s
return c
}
}
changeAttributes := func(m map[string]any) changeHostSetFunc {
return func(c *HostSet) *HostSet {
c.Attributes = mustMarshal(m)
return c
}
}
changeAttributesNil := func() changeHostSetFunc {
return func(c *HostSet) *HostSet {
c.Attributes = nil
return c
}
}
// Define some checks that will be used in the below tests. Some of
// these are re-used, so we define them here. Most of these are
// assertions and no particular one is non-fatal in that they will
// stop execution. Note that these are executed after wantIsErr, so
// if that is set in an individual table test, these will not be
// executed.
//
// Note that we define some state here, similar to how we
// previously defined gotOnUpdateCatalogRequest above next the
// plugin map.
type checkHostSetFunc func(t *testing.T, got *HostSet)
checkVersion := func(want uint32) checkHostSetFunc {
return func(t *testing.T, got *HostSet) {
t.Helper()
assert.Equal(t, want, got.Version, "checkVersion")
}
}
checkName := func(want string) checkHostSetFunc {
return func(t *testing.T, got *HostSet) {
t.Helper()
assert.Equal(t, want, got.Name, "checkName")
}
}
checkDescription := func(want string) checkHostSetFunc {
return func(t *testing.T, got *HostSet) {
t.Helper()
assert.Equal(t, want, got.Description, "checkDescription")
}
}
checkPreferredEndpoints := func(want []string) checkHostSetFunc {
return func(t *testing.T, got *HostSet) {
t.Helper()
assert.Equal(t, want, got.PreferredEndpoints, "checkPreferredEndpoints")
}
}
checkAttributes := func(want map[string]any) checkHostSetFunc {
return func(t *testing.T, got *HostSet) {
t.Helper()
st := &structpb.Struct{}
require.NoError(t, proto.Unmarshal(got.Attributes, st))
assert.Empty(t, cmp.Diff(mustStruct(want), st, protocmp.Transform()))
}
}
checkSyncInterval := func(want int32) checkHostSetFunc {
return func(t *testing.T, got *HostSet) {
t.Helper()
assert.Equal(t, want, got.SyncIntervalSeconds, "checkSyncInterval")
}
}
checkNeedSync := func(want bool) checkHostSetFunc {
return func(t *testing.T, got *HostSet) {
t.Helper()
assert.Equal(t, want, got.NeedSync, "checkNeedSync")
}
}
checkVerifySetOplog := func(op oplog.OpType) checkHostSetFunc {
return func(t *testing.T, got *HostSet) {
t.Helper()
assert.NoError(t,
db.TestVerifyOplog(
t,
dbRW,
got.PublicId,
db.WithOperation(op),
db.WithCreateNotBefore(10*time.Second),
),
)
}
}
type checkPluginReqFunc func(t *testing.T, got *plgpb.OnUpdateSetRequest)
checkUpdateSetRequestCurrentNameNil := func() checkPluginReqFunc {
return func(t *testing.T, got *plgpb.OnUpdateSetRequest) {
t.Helper()
assert.Nil(t, got.CurrentSet.Name, "checkUpdateSetRequestCurrentNameNil")
}
}
checkUpdateSetRequestNewName := func(want string) checkPluginReqFunc {
return func(t *testing.T, got *plgpb.OnUpdateSetRequest) {
t.Helper()
assert.Equal(t, wrapperspb.String(want), got.NewSet.Name, "checkUpdateSetRequestNewName")
}
}
checkUpdateSetRequestNewNameNil := func() checkPluginReqFunc {
return func(t *testing.T, got *plgpb.OnUpdateSetRequest) {
t.Helper()
assert.Nil(t, got.NewSet.Name, "checkUpdateSetRequestNewNameNil")
}
}
checkUpdateSetRequestCurrentDescriptionNil := func() checkPluginReqFunc {
return func(t *testing.T, got *plgpb.OnUpdateSetRequest) {
t.Helper()
assert.Nil(t, got.CurrentSet.Description, "checkUpdateSetRequestCurrentDescriptionNil")
}
}
checkUpdateSetRequestNewDescription := func(want string) checkPluginReqFunc {
return func(t *testing.T, got *plgpb.OnUpdateSetRequest) {
t.Helper()
assert.Equal(t, wrapperspb.String(want), got.NewSet.Description, "checkUpdateSetRequestNewDescription")
}
}
checkUpdateSetRequestNewDescriptionNil := func() checkPluginReqFunc {
return func(t *testing.T, got *plgpb.OnUpdateSetRequest) {
t.Helper()
assert.Nil(t, got.NewSet.Description, "checkUpdateSetRequestNewDescriptionNil")
}
}
checkUpdateSetRequestCurrentPreferredEndpoints := func(want []string) checkPluginReqFunc {
return func(t *testing.T, got *plgpb.OnUpdateSetRequest) {
t.Helper()
assert.Equal(t, want, got.CurrentSet.PreferredEndpoints, "checkUpdateSetRequestCurrentPreferredEndpoints")
}
}
checkUpdateSetRequestNewPreferredEndpoints := func(want []string) checkPluginReqFunc {
return func(t *testing.T, got *plgpb.OnUpdateSetRequest) {
t.Helper()
assert.Equal(t, want, got.NewSet.PreferredEndpoints, "checkUpdateSetRequestNewPreferredEndpoints")
}
}
checkUpdateSetRequestNewPreferredEndpointsNil := func() checkPluginReqFunc {
return func(t *testing.T, got *plgpb.OnUpdateSetRequest) {
t.Helper()
assert.Nil(t, got.NewSet.PreferredEndpoints, "checkUpdateSetRequestNewPreferredEndpointsNil")
}
}
checkUpdateSetRequestCurrentAttributes := func(want map[string]any) checkPluginReqFunc {
return func(t *testing.T, got *plgpb.OnUpdateSetRequest) {
t.Helper()
assert.Empty(t, cmp.Diff(mustStruct(want), got.CurrentSet.GetAttributes(), protocmp.Transform()), "checkUpdateSetRequestCurrentAttributes")
}
}
checkUpdateSetRequestNewAttributes := func(want map[string]any) checkPluginReqFunc {
return func(t *testing.T, got *plgpb.OnUpdateSetRequest) {
t.Helper()
assert.Empty(t, cmp.Diff(mustStruct(want), got.NewSet.GetAttributes(), protocmp.Transform()), "checkUpdateSetRequestNewAttributes")
}
}
checkUpdateSetRequestPersistedSecrets := func(want map[string]any) checkPluginReqFunc {
return func(t *testing.T, got *plgpb.OnUpdateSetRequest) {
t.Helper()
assert.Empty(t, cmp.Diff(mustStruct(want), got.Persisted.Secrets, protocmp.Transform()), "checkUpdateSetRequestPersistedSecrets")
}
}
// Finally define a function for bringing the test subject host
// set.
setupBareHostSet := func(t *testing.T, ctx context.Context) (*HostSet, []*Host) {
t.Helper()
set := TestSet(
t,
dbConn,
dbKmsCache,
sched,
testCatalog,
dummyPluginMap,
)
t.Cleanup(func() {
t.Helper()
assert := assert.New(t)
n, err := dbRW.Delete(ctx, set)
assert.NoError(err)
assert.Equal(1, n)
})
return set, nil
}
// Create a host that will be verified as belonging to the created set below.
testHostExternIdPrefix := "test-host-external-id"
testHosts := make([]*Host, 3)
for i := 0; i <= 2; i++ {
testHosts[i] = TestHost(t, dbConn, testCatalog.PublicId, fmt.Sprintf("%s-%d", testHostExternIdPrefix, i))
}
// Finally define a function for bringing the test subject host
// set.
setupHostSet := func(t *testing.T, ctx context.Context) (*HostSet, []*Host) {
t.Helper()
require := require.New(t)
set := TestSet(
t,
dbConn,
dbKmsCache,
sched,
testCatalog,
dummyPluginMap,
WithPreferredEndpoints([]string{"cidr:192.168.0.0/24", "cidr:192.168.1.0/24", "cidr:172.16.0.0/12"}),
)
// Set some (default) attributes on our test set
set.Attributes = mustMarshal(map[string]any{
"foo": "bar",
})
// Set some fake sync detail to the set.
set.LastSyncTime = timestamp.New(time.Now())
set.NeedSync = false
numSetsUpdated, err := dbRW.Update(ctx, set, []string{"attributes", "LastSyncTime", "NeedSync"}, []string{})
require.NoError(err)
require.Equal(1, numSetsUpdated)
// Add some hosts to the host set.
TestSetMembers(t, dbConn, set.PublicId, testHosts)
t.Cleanup(func() {
t.Helper()
assert := assert.New(t)
n, err := dbRW.Delete(ctx, set)
assert.NoError(err)
assert.Equal(1, n)
})
return set, testHosts
}
tests := []struct {
name string
startingSet func(*testing.T, context.Context) (*HostSet, []*Host)
withProjectId *string
withEmptyPluginMap bool
withPluginError error
changeFuncs []changeHostSetFunc
version uint32
fieldMask []string
wantCheckSetFuncs []checkHostSetFunc
wantCheckPluginReqFuncs []checkPluginReqFunc
wantIsErr errors.Code
}{
{
name: "nil set",
startingSet: setupBareHostSet,
changeFuncs: []changeHostSetFunc{changeSetToNil()},
wantIsErr: errors.InvalidParameter,
},
{
name: "nil embedded set",
startingSet: setupBareHostSet,
changeFuncs: []changeHostSetFunc{changeEmbeddedSetToNil()},
wantIsErr: errors.InvalidParameter,
},
{
name: "missing public id",
startingSet: setupBareHostSet,
changeFuncs: []changeHostSetFunc{changePublicId("")},
wantIsErr: errors.InvalidParameter,
},
{
name: "missing project id",
startingSet: setupBareHostSet,
withProjectId: func() *string { a := ""; return &a }(),
wantIsErr: errors.InvalidParameter,
},
{
name: "empty field mask",
startingSet: setupBareHostSet,
fieldMask: nil, // Should be testing on len
wantIsErr: errors.EmptyFieldMask,
},
{
name: "bad set id",
startingSet: setupBareHostSet,
changeFuncs: []changeHostSetFunc{changePublicId("badid")},
fieldMask: []string{"name"},
wantIsErr: errors.RecordNotFound,
},
{
name: "version mismatch",
startingSet: setupBareHostSet,
changeFuncs: []changeHostSetFunc{changeName("foo")},
version: 4,
fieldMask: []string{"name"},
wantIsErr: errors.VersionMismatch,
},
{
name: "mismatched project id to catalog project",
startingSet: setupBareHostSet,
withProjectId: func() *string { a := "badid"; return &a }(),
fieldMask: []string{"name"},
wantIsErr: errors.InvalidParameter,
},
{
name: "plugin lookup error",
startingSet: setupBareHostSet,
withEmptyPluginMap: true,
fieldMask: []string{"name"},
wantIsErr: errors.Internal,
},
{
name: "plugin invocation error",
startingSet: setupBareHostSet,
withPluginError: errors.New(context.Background(), errors.Internal, "TestRepository_UpdateSet/plugin_invocation_error", "test plugin error"),
fieldMask: []string{"name"},
wantIsErr: errors.Internal,
},
{
name: "update name (duplicate)",
startingSet: setupBareHostSet,
changeFuncs: []changeHostSetFunc{changeName(testDuplicateSetName)},
fieldMask: []string{"name"},
wantIsErr: errors.NotUnique,
},
{
name: "update name",
startingSet: setupHostSet,
changeFuncs: []changeHostSetFunc{changeName("foo")},
fieldMask: []string{"name"},
wantCheckPluginReqFuncs: []checkPluginReqFunc{
checkUpdateSetRequestCurrentNameNil(),
checkUpdateSetRequestNewName("foo"),
checkUpdateSetRequestPersistedSecrets(map[string]any{
"one": "two",
}),
},
wantCheckSetFuncs: []checkHostSetFunc{
checkVersion(3),
checkName("foo"),
checkNeedSync(false),
checkVerifySetOplog(oplog.OpType_OP_TYPE_UPDATE),
},
},
{
name: "update name to same",
startingSet: setupHostSet,
changeFuncs: []changeHostSetFunc{changeName("")},
fieldMask: []string{"name"},
wantCheckPluginReqFuncs: []checkPluginReqFunc{
checkUpdateSetRequestCurrentNameNil(),
checkUpdateSetRequestNewNameNil(),
checkUpdateSetRequestPersistedSecrets(map[string]any{
"one": "two",
}),
},
wantCheckSetFuncs: []checkHostSetFunc{
checkVersion(2), // Version remains same even though row is updated
checkName(""),
checkNeedSync(false),
checkVerifySetOplog(oplog.OpType_OP_TYPE_UPDATE),
},
},
{
name: "update description",
startingSet: setupHostSet,
changeFuncs: []changeHostSetFunc{changeDescription("foo")},
fieldMask: []string{"description"},
wantCheckPluginReqFuncs: []checkPluginReqFunc{
checkUpdateSetRequestCurrentDescriptionNil(),
checkUpdateSetRequestNewDescription("foo"),
checkUpdateSetRequestPersistedSecrets(map[string]any{
"one": "two",
}),
},
wantCheckSetFuncs: []checkHostSetFunc{
checkVersion(3),
checkDescription("foo"),
checkNeedSync(false),
checkVerifySetOplog(oplog.OpType_OP_TYPE_UPDATE),
},
},
{
name: "update description to same",
startingSet: setupHostSet,
changeFuncs: []changeHostSetFunc{changeDescription("")},
fieldMask: []string{"description"},
wantCheckPluginReqFuncs: []checkPluginReqFunc{
checkUpdateSetRequestCurrentDescriptionNil(),
checkUpdateSetRequestNewDescriptionNil(),
checkUpdateSetRequestPersistedSecrets(map[string]any{
"one": "two",
}),
},
wantCheckSetFuncs: []checkHostSetFunc{
checkVersion(2), // Version remains same even though row is updated
checkDescription(""),
checkNeedSync(false),
checkVerifySetOplog(oplog.OpType_OP_TYPE_UPDATE),
},
},
{
name: "set sync interval",
startingSet: setupBareHostSet,
changeFuncs: []changeHostSetFunc{changeSyncInterval(42)},
fieldMask: []string{"SyncIntervalSeconds"},
wantCheckPluginReqFuncs: []checkPluginReqFunc{
checkUpdateSetRequestPersistedSecrets(map[string]any{
"one": "two",
}),
},
wantCheckSetFuncs: []checkHostSetFunc{
checkVersion(2),
checkNeedSync(true),
checkSyncInterval(42),
checkVerifySetOplog(oplog.OpType_OP_TYPE_UPDATE),
},
},
{
name: "add preferred endpoints",
startingSet: setupBareHostSet,
changeFuncs: []changeHostSetFunc{changePreferredEndpoints([]string{"cidr:10.0.0.0/24"})},
fieldMask: []string{"PreferredEndpoints"},
wantCheckPluginReqFuncs: []checkPluginReqFunc{
checkUpdateSetRequestCurrentPreferredEndpoints(nil),
checkUpdateSetRequestNewPreferredEndpoints([]string{"cidr:10.0.0.0/24"}),
checkUpdateSetRequestPersistedSecrets(map[string]any{
"one": "two",
}),
},
wantCheckSetFuncs: []checkHostSetFunc{
checkVersion(2),
checkPreferredEndpoints([]string{"cidr:10.0.0.0/24"}),
checkNeedSync(true),
checkVerifySetOplog(oplog.OpType_OP_TYPE_UPDATE),
},
},
{
name: "update preferred endpoints",
startingSet: setupHostSet,
changeFuncs: []changeHostSetFunc{changePreferredEndpoints([]string{"cidr:10.0.0.0/24"})},
fieldMask: []string{"PreferredEndpoints"},
wantCheckPluginReqFuncs: []checkPluginReqFunc{
checkUpdateSetRequestCurrentPreferredEndpoints([]string{"cidr:192.168.0.0/24", "cidr:192.168.1.0/24", "cidr:172.16.0.0/12"}),
checkUpdateSetRequestNewPreferredEndpoints([]string{"cidr:10.0.0.0/24"}),
checkUpdateSetRequestPersistedSecrets(map[string]any{
"one": "two",
}),
},
wantCheckSetFuncs: []checkHostSetFunc{
checkVersion(3),
checkPreferredEndpoints([]string{"cidr:10.0.0.0/24"}),
checkNeedSync(false),
checkVerifySetOplog(oplog.OpType_OP_TYPE_UPDATE),
},
},
{
name: "clear preferred endpoints",
startingSet: setupHostSet,
changeFuncs: []changeHostSetFunc{changePreferredEndpoints(nil)},
fieldMask: []string{"PreferredEndpoints"},
wantCheckPluginReqFuncs: []checkPluginReqFunc{
checkUpdateSetRequestCurrentPreferredEndpoints([]string{"cidr:192.168.0.0/24", "cidr:192.168.1.0/24", "cidr:172.16.0.0/12"}),
checkUpdateSetRequestNewPreferredEndpointsNil(),
checkUpdateSetRequestPersistedSecrets(map[string]any{
"one": "two",
}),
},
wantCheckSetFuncs: []checkHostSetFunc{
checkVersion(3),
checkPreferredEndpoints(nil),
checkNeedSync(false),
},
},
{
name: "update attributes (add)",
startingSet: setupHostSet,
changeFuncs: []changeHostSetFunc{changeAttributes(map[string]any{
"baz": "qux",
})},
fieldMask: []string{"attributes"},
wantCheckPluginReqFuncs: []checkPluginReqFunc{
checkUpdateSetRequestCurrentAttributes(map[string]any{
"foo": "bar",
}),
checkUpdateSetRequestNewAttributes(map[string]any{
"foo": "bar",
"baz": "qux",
}),
checkUpdateSetRequestPersistedSecrets(map[string]any{
"one": "two",
}),
},
wantCheckSetFuncs: []checkHostSetFunc{
checkVersion(3),
checkAttributes(map[string]any{
"foo": "bar",
"baz": "qux",
}),
checkNeedSync(true),
checkVerifySetOplog(oplog.OpType_OP_TYPE_UPDATE),
},
},
{
name: "update attributes (overwrite)",
startingSet: setupHostSet,
changeFuncs: []changeHostSetFunc{changeAttributes(map[string]any{
"foo": "baz",
normalizeToSliceKey: "normalizeme",
})},
fieldMask: []string{"attributes"},
wantCheckPluginReqFuncs: []checkPluginReqFunc{
checkUpdateSetRequestCurrentAttributes(map[string]any{
"foo": "bar",
}),
checkUpdateSetRequestNewAttributes(map[string]any{
"foo": "baz",
normalizeToSliceKey: []any{"normalizeme"},
}),
checkUpdateSetRequestPersistedSecrets(map[string]any{
"one": "two",
}),
},
wantCheckSetFuncs: []checkHostSetFunc{
checkVersion(3),
checkAttributes(map[string]any{
"foo": "baz",
normalizeToSliceKey: []any{"normalizeme"},
}),
checkNeedSync(true),
checkVerifySetOplog(oplog.OpType_OP_TYPE_UPDATE),
},
},
{
name: "update attributes (null)",
startingSet: setupHostSet,
changeFuncs: []changeHostSetFunc{changeAttributes(map[string]any{
"foo": nil,
})},
fieldMask: []string{"attributes"},
wantCheckPluginReqFuncs: []checkPluginReqFunc{
checkUpdateSetRequestCurrentAttributes(map[string]any{
"foo": "bar",
}),
checkUpdateSetRequestNewAttributes(map[string]any{}),
checkUpdateSetRequestPersistedSecrets(map[string]any{
"one": "two",
}),
},
wantCheckSetFuncs: []checkHostSetFunc{
checkVersion(3),
checkAttributes(map[string]any{}),
checkNeedSync(true),
checkVerifySetOplog(oplog.OpType_OP_TYPE_UPDATE),
},
},
{
name: "update attributes (full null)",
startingSet: setupHostSet,
changeFuncs: []changeHostSetFunc{changeAttributesNil()},
fieldMask: []string{"attributes"},
wantCheckPluginReqFuncs: []checkPluginReqFunc{
checkUpdateSetRequestCurrentAttributes(map[string]any{
"foo": "bar",
}),
checkUpdateSetRequestNewAttributes(map[string]any{}),
checkUpdateSetRequestPersistedSecrets(map[string]any{
"one": "two",
}),
},
wantCheckSetFuncs: []checkHostSetFunc{
checkVersion(3),
checkAttributes(map[string]any{}),
checkNeedSync(true),
checkVerifySetOplog(oplog.OpType_OP_TYPE_UPDATE),
},
},
{
name: "update attributes (combined)",
startingSet: setupHostSet,
changeFuncs: []changeHostSetFunc{changeAttributes(map[string]any{
"a": "b",
"foo": "baz",
})},
fieldMask: []string{"attributes.a", "attributes.foo"},
wantCheckPluginReqFuncs: []checkPluginReqFunc{
checkUpdateSetRequestCurrentAttributes(map[string]any{
"foo": "bar",
}),
checkUpdateSetRequestNewAttributes(map[string]any{
"a": "b",
"foo": "baz",
}),
checkUpdateSetRequestPersistedSecrets(map[string]any{
"one": "two",
}),
},
wantCheckSetFuncs: []checkHostSetFunc{
checkVersion(3),
checkAttributes(map[string]any{
"a": "b",
"foo": "baz",
}),
checkNeedSync(true),
checkVerifySetOplog(oplog.OpType_OP_TYPE_UPDATE),
},
},
{
name: "update name and preferred endpoints",
startingSet: setupHostSet,
changeFuncs: []changeHostSetFunc{
changePreferredEndpoints([]string{"cidr:10.0.0.0/24"}),
changeName("foo"),
},
fieldMask: []string{"name", "PreferredEndpoints"},
wantCheckPluginReqFuncs: []checkPluginReqFunc{
checkUpdateSetRequestCurrentNameNil(),
checkUpdateSetRequestNewName("foo"),
checkUpdateSetRequestCurrentPreferredEndpoints([]string{"cidr:192.168.0.0/24", "cidr:192.168.1.0/24", "cidr:172.16.0.0/12"}),
checkUpdateSetRequestNewPreferredEndpoints([]string{"cidr:10.0.0.0/24"}),
checkUpdateSetRequestPersistedSecrets(map[string]any{
"one": "two",
}),
},
wantCheckSetFuncs: []checkHostSetFunc{
checkVersion(3),
checkName("foo"),
checkPreferredEndpoints([]string{"cidr:10.0.0.0/24"}),
checkNeedSync(false),
checkVerifySetOplog(oplog.OpType_OP_TYPE_UPDATE),
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
assert := assert.New(t)
// Define a plugin "manager", basically just a map with a mock
// plugin in it. This also includes functionality to capture the
// state, and set an error and the returned secrets to nil. Note
// that the way that this is set up means that the tests cannot run
// in parallel, but there could be other factors affecting that as
// well.
var gotOnUpdateCallCount int
testPluginMap := map[string]plgpb.HostPluginServiceClient{
testPlugin.GetPublicId(): &loopback.WrappingPluginHostClient{
Server: &loopback.TestPluginHostServer{
NormalizeSetDataFn: func(_ context.Context, req *plgpb.NormalizeSetDataRequest) (*plgpb.NormalizeSetDataResponse, error) {
if req.Attributes == nil {
return new(plgpb.NormalizeSetDataResponse), nil
}
var attrs struct {
NormalizeToSlice string `mapstructure:"normalize_to_slice"`
}
require.NoError(mapstructure.Decode(req.Attributes.AsMap(), &attrs))
if attrs.NormalizeToSlice == "" {
return new(plgpb.NormalizeSetDataResponse), nil
}
retAttrs := proto.Clone(req.Attributes).(*structpb.Struct)
retAttrs.Fields[normalizeToSliceKey] = structpb.NewListValue(&structpb.ListValue{
Values: []*structpb.Value{structpb.NewStringValue(attrs.NormalizeToSlice)},
})
return &plgpb.NormalizeSetDataResponse{Attributes: retAttrs}, nil
},
OnUpdateSetFn: func(_ context.Context, req *plgpb.OnUpdateSetRequest) (*plgpb.OnUpdateSetResponse, error) {
gotOnUpdateCallCount++
for _, check := range tt.wantCheckPluginReqFuncs {
check(t, req)
}
return &plgpb.OnUpdateSetResponse{}, tt.withPluginError
},
},
},
}
origSet, wantedHosts := tt.startingSet(t, ctx)
pluginMap := testPluginMap
if tt.withEmptyPluginMap {
pluginMap = make(map[string]plgpb.HostPluginServiceClient)
}
repo, err := NewRepository(ctx, dbRW, dbRW, dbKmsCache, sched, pluginMap)
require.NoError(err)
require.NotNil(repo)
workingSet := origSet.clone()
for _, cf := range tt.changeFuncs {
workingSet = cf(workingSet)
}
projectId := testCatalog.ProjectId
if tt.withProjectId != nil {
projectId = *tt.withProjectId
}
version := origSet.Version
if tt.version != 0 {
version = tt.version
}
gotUpdatedSet, gotHosts, gotPlugin, gotNumUpdated, err := repo.UpdateSet(ctx, projectId, workingSet, version, tt.fieldMask)
t.Cleanup(func() { gotOnUpdateCallCount = 0 })
if tt.wantIsErr != 0 {
require.Equal(db.NoRowsAffected, gotNumUpdated)
require.Truef(errors.Match(errors.T(tt.wantIsErr), err), "want err: %q got: %q", tt.wantIsErr, err)
return
}
require.NoError(err)
require.Equal(1, gotOnUpdateCallCount)
assert.Equal(1, gotNumUpdated)
// Quick assertion that the set is not nil and that the plugin
// ID in the catalog referenced by the set matches the plugin
// ID in the returned plugin.
require.NotNil(gotUpdatedSet)
require.NotNil(gotPlugin)
assert.Equal(testCatalog.PublicId, gotUpdatedSet.CatalogId)
assert.Equal(testCatalog.PluginId, gotPlugin.PublicId)
// Also assert that the hosts returned by the request are the ones that belong to the set
wantHostMap := make(map[string]string, len(wantedHosts))
for _, h := range wantedHosts {
wantHostMap[h.PublicId] = h.ExternalId
}
gotHostMap := make(map[string]string, len(gotHosts))
for _, h := range gotHosts {
gotHostMap[h.PublicId] = h.ExternalId
}
assert.Equal(wantHostMap, gotHostMap)
// Perform checks
for _, check := range tt.wantCheckSetFuncs {
check(t, gotUpdatedSet)
}
gotLookupSet, _, err := repo.LookupSet(ctx, workingSet.GetPublicId())
assert.NoError(err)
assert.Empty(cmp.Diff(gotUpdatedSet, gotLookupSet, protocmp.Transform()))
})
}
t.Run("Unset Empty PreferredEndpoint", func(t *testing.T) {
var gotOnUpdateCallCount int
testPluginMap := map[string]plgpb.HostPluginServiceClient{
testPlugin.GetPublicId(): &loopback.WrappingPluginHostClient{
Server: &loopback.TestPluginHostServer{
OnUpdateSetFn: func(_ context.Context, req *plgpb.OnUpdateSetRequest) (*plgpb.OnUpdateSetResponse, error) {
gotOnUpdateCallCount++
for _, check := range []checkPluginReqFunc{
checkUpdateSetRequestCurrentPreferredEndpoints(nil),
checkUpdateSetRequestNewPreferredEndpointsNil(),
checkUpdateSetRequestPersistedSecrets(map[string]any{
"one": "two",
}),
} {
check(t, req)
}
return &plgpb.OnUpdateSetResponse{}, nil
},
},
},
}
origSet, _ := setupBareHostSet(t, ctx)
pluginMap := testPluginMap
repo, err := NewRepository(ctx, dbRW, dbRW, dbKmsCache, sched, pluginMap)
require.NoError(t, err)
require.NotNil(t, repo)
workingSet := origSet.clone()
workingSet = changePreferredEndpoints(nil)(workingSet)
gotUpdatedSet, _, _, gotNumUpdated, err := repo.UpdateSet(ctx, testCatalog.ProjectId, workingSet, origSet.Version, []string{"PreferredEndpoints"})
t.Cleanup(func() { gotOnUpdateCallCount = 0 })
require.NoError(t, err)
require.Equal(t, 1, gotOnUpdateCallCount)
assert.Equal(t, 0, gotNumUpdated)
assert.Empty(t, cmp.Diff(gotUpdatedSet, origSet, protocmp.Transform()))
})
}
func TestRepository_UpdateSet_UnsetEmptyPreferredEndpoint(t *testing.T) {
}
func TestRepository_LookupSet(t *testing.T) {
ctx := context.Background()
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)
sched := scheduler.TestScheduler(t, conn, wrapper)
_, prj := iam.TestScopes(t, iamRepo)
plg := plugin.TestPlugin(t, conn, "lookup")
plgm := map[string]plgpb.HostPluginServiceClient{
plg.GetPublicId(): loopback.NewWrappingPluginHostClient(&loopback.TestPluginServer{}),
}
catalog := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId())
hostSet := TestSet(t, conn, kms, sched, catalog, map[string]plgpb.HostPluginServiceClient{
plg.GetPublicId(): loopback.NewWrappingPluginHostClient(&loopback.TestPluginHostServer{
ListHostsFn: func(ctx context.Context, req *plgpb.ListHostsRequest) (*plgpb.ListHostsResponse, error) {
require.NotEmpty(t, req.GetSets())
require.NotNil(t, req.GetCatalog())
return &plgpb.ListHostsResponse{}, nil
},
}),
}, WithSyncIntervalSeconds(5))
hostSetId, err := newHostSetId(ctx)
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(ctx, rw, rw, kms, sched, plgm)
assert.NoError(err)
require.NotNil(repo)
got, _, err := repo.LookupSet(ctx, 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)
if tt.want != nil {
assert.Empty(cmp.Diff(got, tt.want, protocmp.Transform()), "LookupSet(%q) got response %q, wanted %q", tt.in, got, tt.want)
}
})
}
}
func TestRepository_Endpoints(t *testing.T) {
ctx := context.Background()
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)
sched := scheduler.TestScheduler(t, conn, wrapper)
_, prj := iam.TestScopes(t, iamRepo)
plg := plugin.TestPlugin(t, conn, "endpoints")
hostlessCatalog := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId())
plgm := map[string]plgpb.HostPluginServiceClient{
plg.GetPublicId(): loopback.NewWrappingPluginHostClient(&loopback.TestPluginServer{}),
}
catalog := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId())
hostSet10 := TestSet(t, conn, kms, sched, catalog, plgm, WithName("hostSet10"), WithPreferredEndpoints([]string{"cidr:10.0.0.1/24"}))
hostSet192 := TestSet(t, conn, kms, sched, catalog, plgm, WithName("hostSet192"), WithPreferredEndpoints([]string{"cidr:192.168.0.1/24"}))
hostSet100 := TestSet(t, conn, kms, sched, catalog, plgm, WithName("hostSet100"), WithPreferredEndpoints([]string{"cidr:100.100.100.100/24"}))
hostSetDNS := TestSet(t, conn, kms, sched, catalog, plgm, WithName("hostSetDNS"), WithPreferredEndpoints([]string{"dns:*"}))
hostlessSet := TestSet(t, conn, kms, sched, hostlessCatalog, plgm)
h1 := TestHost(t, conn, catalog.GetPublicId(), "test", withIpAddresses([]string{"10.0.0.5", "192.168.0.5"}), withDnsNames([]string{"example.com"}))
TestSetMembers(t, conn, hostSet10.GetPublicId(), []*Host{h1})
TestSetMembers(t, conn, hostSet192.GetPublicId(), []*Host{h1})
TestSetMembers(t, conn, hostSet100.GetPublicId(), []*Host{h1})
TestSetMembers(t, conn, hostSetDNS.GetPublicId(), []*Host{h1})
tests := []struct {
name string
setIds []string
want []*host.Endpoint
wantIsErr errors.Code
}{
{
name: "with-no-set-id",
wantIsErr: errors.InvalidParameter,
},
{
name: "with-set10",
setIds: []string{hostSet10.GetPublicId()},
want: []*host.Endpoint{
{
HostId: func() string {
s, err := newHostId(ctx, catalog.GetPublicId(), "test")
require.NoError(t, err)
return s
}(),
SetId: hostSet10.GetPublicId(),
Address: "10.0.0.5",
},
},
},
{
name: "with-different-set",
setIds: []string{hostSet192.GetPublicId()},
want: []*host.Endpoint{
{
HostId: func() string {
s, err := newHostId(ctx, catalog.GetPublicId(), "test")
require.NoError(t, err)
return s
}(),
SetId: hostSet192.GetPublicId(),
Address: "192.168.0.5",
},
},
},
{
name: "with-all-addresses-filtered-set",
setIds: []string{hostSet100.GetPublicId()},
want: nil,
},
{
name: "with-no-hosts-from-plugin",
setIds: []string{hostlessSet.GetPublicId()},
want: nil,
},
{
name: "with-dns-names",
setIds: []string{hostSetDNS.GetPublicId()},
want: []*host.Endpoint{
{
HostId: func() string {
s, err := newHostId(ctx, catalog.GetPublicId(), "test")
require.NoError(t, err)
return s
}(),
SetId: hostSetDNS.GetPublicId(),
Address: "example.com",
},
},
},
}
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(ctx, rw, rw, kms, sched, plgm)
assert.NoError(err)
require.NotNil(repo)
got, err := repo.Endpoints(ctx, tt.setIds)
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)
if tt.want == nil {
return
}
sort.Slice(tt.want, func(i, j int) bool {
return tt.want[i].HostId < tt.want[j].HostId
})
sort.Slice(got, func(i, j int) bool {
return got[i].HostId < got[j].HostId
})
assert.Empty(cmp.Diff(got, tt.want, protocmp.Transform()))
for _, ep := range got {
h := allocHost()
h.PublicId = ep.HostId
require.NoError(rw.LookupByPublicId(ctx, h))
assert.Equal(ep.HostId, h.PublicId)
// TODO: Uncomment when we have a better way to lookup host
// with it's address
// assert.Equal(ep.Address, h.Address)
assert.Equal(catalog.GetPublicId(), h.GetCatalogId())
}
})
}
}
func TestRepository_ListSets(t *testing.T) {
ctx := context.Background()
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)
sched := scheduler.TestScheduler(t, conn, wrapper)
_, prj := iam.TestScopes(t, iamRepo)
plg := plugin.TestPlugin(t, conn, "list")
plgm := map[string]plgpb.HostPluginServiceClient{
plg.GetPublicId(): loopback.NewWrappingPluginHostClient(&loopback.TestPluginServer{}),
}
catalogA := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId())
catalogB := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId())
hostSets := []*HostSet{
TestSet(t, conn, kms, sched, catalogA, plgm),
TestSet(t, conn, kms, sched, catalogA, plgm),
TestSet(t, conn, kms, sched, catalogA, plgm),
}
printoutTable(t, rw)
tests := []struct {
name string
in string
opts []host.Option
want []*HostSet
wantIsErr errors.Code
}{
{
name: "with-no-catalog-id",
wantIsErr: errors.InvalidParameter,
},
{
name: "Catalog-with-no-host-sets",
in: catalogB.PublicId,
want: nil,
},
{
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(ctx, rw, rw, kms, sched, plgm)
assert.NoError(err)
require.NotNil(repo)
got, gotPlg, err := repo.ListSets(ctx, 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...))
if got != nil {
assert.Empty(cmp.Diff(plg, gotPlg, protocmp.Transform()))
}
})
}
}
func TestRepository_ListSets_Limits(t *testing.T) {
ctx := context.Background()
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)
sched := scheduler.TestScheduler(t, conn, wrapper)
_, prj := iam.TestScopes(t, iamRepo)
plg := plugin.TestPlugin(t, conn, "listlimit")
plgm := map[string]plgpb.HostPluginServiceClient{
plg.GetPublicId(): loopback.NewWrappingPluginHostClient(&loopback.TestPluginServer{}),
}
catalog := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId())
count := 10
var hostSets []*HostSet
for i := 0; i < count; i++ {
hostSets = append(hostSets, TestSet(t, conn, kms, sched, catalog, plgm))
}
tests := []struct {
name string
repoOpts []host.Option
listOpts []host.Option
wantLen int
}{
{
name: "With no limits",
wantLen: count,
},
{
name: "With repo limit",
repoOpts: []host.Option{host.WithLimit(3)},
wantLen: 3,
},
{
name: "With negative repo limit",
repoOpts: []host.Option{host.WithLimit(-1)},
wantLen: count,
},
{
name: "With List limit",
listOpts: []host.Option{host.WithLimit(3)},
wantLen: 3,
},
{
name: "With negative List limit",
listOpts: []host.Option{host.WithLimit(-1)},
wantLen: count,
},
{
name: "With repo smaller than list limit",
repoOpts: []host.Option{host.WithLimit(2)},
listOpts: []host.Option{host.WithLimit(6)},
wantLen: 6,
},
{
name: "With repo larger than list limit",
repoOpts: []host.Option{host.WithLimit(6)},
listOpts: []host.Option{host.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(ctx, rw, rw, kms, sched, plgm, tt.repoOpts...)
assert.NoError(err)
require.NotNil(repo)
got, gotPlg, err := repo.ListSets(ctx, hostSets[0].CatalogId, tt.listOpts...)
require.NoError(err)
assert.Len(got, tt.wantLen)
assert.Empty(cmp.Diff(plg, gotPlg, protocmp.Transform()))
})
}
}
func TestRepository_DeleteSet(t *testing.T) {
ctx := context.Background()
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)
sched := scheduler.TestScheduler(t, conn, wrapper)
_, prj := iam.TestScopes(t, iamRepo)
plg := plugin.TestPlugin(t, conn, "create")
plgm := map[string]plgpb.HostPluginServiceClient{
plg.GetPublicId(): loopback.NewWrappingPluginHostClient(loopback.TestPluginHostServer{OnCreateSetFn: func(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) {
return &plgpb.OnCreateSetResponse{}, nil
}}),
}
c := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId())
hostSet := TestSet(t, conn, kms, sched, c, plgm)
hostSet2 := TestSet(t, conn, kms, sched, c, plgm)
newHostSetId, err := newHostSetId(ctx)
require.NoError(t, err)
tests := []struct {
name string
in string
pluginChecker func(*plgpb.OnDeleteSetRequest) error
want int
wantIsErr errors.Code
}{
{
name: "With no public id",
pluginChecker: func(req *plgpb.OnDeleteSetRequest) error {
assert.Fail(t, "The plugin shouldn't be called")
return nil
},
wantIsErr: errors.InvalidParameter,
},
{
name: "With non existing host set id",
in: newHostSetId,
pluginChecker: func(req *plgpb.OnDeleteSetRequest) error {
assert.Fail(t, "The plugin shouldn't be called")
return nil
},
want: 0,
},
{
name: "With existing host set id",
in: hostSet.PublicId,
pluginChecker: func(req *plgpb.OnDeleteSetRequest) error {
assert.Equal(t, c.GetPublicId(), req.GetCatalog().GetId())
assert.Equal(t, hostSet.GetPublicId(), req.GetSet().GetId())
return nil
},
want: 1,
},
{
name: "Ignores plugin errors",
in: hostSet2.PublicId,
pluginChecker: func(req *plgpb.OnDeleteSetRequest) error {
assert.Equal(t, c.GetPublicId(), req.GetCatalog().GetId())
assert.Equal(t, hostSet2.GetPublicId(), req.GetSet().GetId())
return fmt.Errorf("test error")
},
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(ctx, rw, rw, kms, sched, plgm)
assert.NoError(err)
require.NotNil(repo)
got, err := repo.DeleteSet(ctx, prj.PublicId, tt.in)
if tt.wantIsErr != 0 {
assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "want err: %q got: %q", tt.wantIsErr, err)
assert.Zero(got)
return
}
require.NoError(err)
assert.EqualValues(tt.want, got)
})
}
}
func printoutTable(t *testing.T, rw *db.Db) {
ctx := context.Background()
hsas := []*hostSetAgg{}
require.NoError(t, rw.SearchWhere(ctx, &hsas, "", nil))
for _, hs := range hsas {
t.Logf("%#v", hs)
}
}