* internal/host/plugin: Add UpdateCatalog This adds support for updating plugin-based host catalogs. The workflow is: * The existing catalog is fetched. * Any attribute changes are patched against it, along with other changes such as name and description. * Both the existing catalog, and the state of the catalog as it will exist post-update, along with any persisted secrets, are sent to the plugin via an OnUpdateCatalog request. Upon success, the following is done: * The record is updated if non-secret updates need to be made. * Secrets are updated if they still exist. * If an empty secret set was sent back, secrets are instead deleted. * If the secrets field was instead null, secret update is skipped altogether. There has also been a lot of helper code added to the tests to help make the update tests more readable and reduce boilerplate. While the create tests have not been updated to use these new helpers, it might be a good idea to do so for consistency. * move secret/persisted state closer to plugin call * Update internal/host/plugin/repository_host_catalog.go Co-authored-by: Todd <T.Alan.Knight@gmail.com> * Update internal/host/plugin/repository_host_catalog_test.go Co-authored-by: Todd <T.Alan.Knight@gmail.com> * move all test helpers for UpdateCatalog inline * WIP - transparent attributes struct/wire conversion * Revert "WIP - transparent attributes struct/wire conversion" This reverts commit 31af1b1b7e535119aa23bd0aa936da1a80eb5aa2. * deal with multiple attribute masks * Note TODO for upsertQuery for HostCatalogSecret * add version check guard * Update internal/host/plugin/host_catalog_secret.go Co-authored-by: Todd <T.Alan.Knight@gmail.com> * remove redundant JIRA reference * don't separate number of updated catalogs and secrets * increment version when secrets is the only thing being updated * add another test for updating both catalog and secrets at once * correct comment for mock plugin manager section * add first-part path matching for secrets * update plugin lookup error to return errors.Internal * only add name/description to plugin request if they are non-empty * update duplicate tests to use two project scopes * remove unused test checks * return plugin record along with host catalog in update * move away from using passed in catalog after patching newCatalog * use t.Cleanup() instead of defer Co-authored-by: Todd <T.Alan.Knight@gmail.com>
175 lines
5.1 KiB
Go
175 lines
5.1 KiB
Go
package plugin
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"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/plugin/host"
|
|
"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"
|
|
)
|
|
|
|
func TestHostCatalogSecret_New(t *testing.T) {
|
|
conn, _ := db.TestSetup(t, "postgres")
|
|
wrapper := db.TestWrapper(t)
|
|
kkms := kms.TestKms(t, conn, wrapper)
|
|
|
|
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
|
|
plg := host.TestPlugin(t, conn, "test")
|
|
cat := TestCatalog(t, conn, prj.GetPublicId(), plg.GetPublicId())
|
|
|
|
type args struct {
|
|
catalogId string
|
|
attrs *structpb.Struct
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want *HostCatalogSecret
|
|
wantErr bool
|
|
wantEncryptErr bool
|
|
}{
|
|
{
|
|
name: "blank-catalog-id",
|
|
args: args{
|
|
catalogId: "",
|
|
},
|
|
want: &HostCatalogSecret{
|
|
HostCatalogSecret: &store.HostCatalogSecret{},
|
|
},
|
|
wantEncryptErr: true,
|
|
},
|
|
{
|
|
name: "no-attributes",
|
|
args: args{
|
|
catalogId: cat.GetPublicId(),
|
|
},
|
|
want: &HostCatalogSecret{
|
|
HostCatalogSecret: &store.HostCatalogSecret{
|
|
CatalogId: cat.GetPublicId(),
|
|
},
|
|
},
|
|
wantEncryptErr: true,
|
|
},
|
|
{
|
|
name: "valid",
|
|
args: args{
|
|
catalogId: cat.GetPublicId(),
|
|
attrs: func() *structpb.Struct {
|
|
st, err := structpb.NewStruct(map[string]interface{}{"foo": "bar"})
|
|
require.NoError(t, err)
|
|
return st
|
|
}(),
|
|
},
|
|
want: &HostCatalogSecret{
|
|
HostCatalogSecret: &store.HostCatalogSecret{
|
|
CatalogId: cat.GetPublicId(),
|
|
Secret: func() []byte {
|
|
st, err := structpb.NewStruct(map[string]interface{}{"foo": "bar"})
|
|
require.NoError(t, err)
|
|
b, err := proto.Marshal(st)
|
|
require.NoError(t, err)
|
|
return b
|
|
}(),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
databaseWrapper, err := kkms.GetWrapper(ctx, prj.PublicId, kms.KeyPurposeDatabase)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, databaseWrapper)
|
|
|
|
got, err := newHostCatalogSecret(context.Background(), tt.args.catalogId, tt.args.attrs)
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
require.Nil(t, got)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.NotNil(t, got)
|
|
|
|
assert.Empty(t, got.CtSecret)
|
|
assert.Equal(t, tt.want, got)
|
|
|
|
err = got.encrypt(ctx, databaseWrapper)
|
|
if tt.wantEncryptErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.NoError(t, got.decrypt(ctx, databaseWrapper))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHostCatalogSecret_Custom_Queries(t *testing.T) {
|
|
ctx := context.Background()
|
|
conn, _ := db.TestSetup(t, "postgres")
|
|
rw := db.New(conn)
|
|
wrapper := db.TestWrapper(t)
|
|
kkms := kms.TestKms(t, conn, wrapper)
|
|
|
|
_, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper))
|
|
plg := host.TestPlugin(t, conn, "test")
|
|
cat := TestCatalog(t, conn, prj.GetPublicId(), plg.GetPublicId())
|
|
databaseWrapper, err := kkms.GetWrapper(ctx, prj.PublicId, kms.KeyPurposeDatabase)
|
|
require.NoError(t, err)
|
|
|
|
hcs, err := newHostCatalogSecret(ctx, cat.GetPublicId(),
|
|
&structpb.Struct{Fields: map[string]*structpb.Value{"foo": structpb.NewStringValue("bar")}})
|
|
require.NoError(t, err)
|
|
assert.NoError(t, hcs.encrypt(ctx, databaseWrapper))
|
|
q, v := hcs.upsertQuery()
|
|
_, err = rw.Exec(ctx, q, v)
|
|
assert.NoError(t, err)
|
|
|
|
found, err := newHostCatalogSecret(ctx, cat.GetPublicId(), nil)
|
|
require.NoError(t, err)
|
|
assert.NoError(t, rw.LookupWhere(ctx, found, "catalog_id=?", found.GetCatalogId()))
|
|
require.NoError(t, found.decrypt(ctx, databaseWrapper))
|
|
require.NoError(t, hcs.decrypt(ctx, databaseWrapper))
|
|
// update the created/updated time from the original
|
|
hcs.CreateTime, hcs.UpdateTime = found.CreateTime, found.UpdateTime
|
|
assert.Empty(t, cmp.Diff(hcs, found, protocmp.Transform()))
|
|
|
|
// Update the secret and see the value updated.
|
|
updated, err := newHostCatalogSecret(ctx, cat.GetPublicId(),
|
|
&structpb.Struct{Fields: map[string]*structpb.Value{"updated": structpb.NewStringValue("value")}})
|
|
require.NoError(t, err)
|
|
assert.NoError(t, updated.encrypt(ctx, databaseWrapper))
|
|
q, v = updated.upsertQuery()
|
|
_, err = rw.Exec(ctx, q, v)
|
|
assert.NoError(t, err)
|
|
|
|
assert.NoError(t, rw.LookupWhere(ctx, found, "catalog_id=?", found.GetCatalogId()))
|
|
require.NoError(t, found.decrypt(ctx, databaseWrapper))
|
|
require.NoError(t, updated.decrypt(ctx, databaseWrapper))
|
|
// set the created time to the first and update time from the newly found
|
|
updated.CreateTime, updated.UpdateTime = hcs.CreateTime, found.UpdateTime
|
|
assert.Empty(t, cmp.Diff(updated, found, protocmp.Transform()))
|
|
|
|
// Try to delete this secret.
|
|
q, v = updated.deleteQuery()
|
|
_, err = rw.Exec(ctx, q, v)
|
|
assert.NoError(t, err)
|
|
err = rw.LookupWhere(ctx, found, "catalog_id=?", found.GetCatalogId())
|
|
assert.Error(t, err)
|
|
assert.True(t, errors.IsNotFoundError(err))
|
|
}
|