Add mechanism for plugin to normalize attributes (#2376)

* Add mechanism for plugin to normalize attributes

This introduces a new normalization call to the plugin RPC service. If
implemented, the plugin will be given the attributes prior to saving
them in a create/update call, and can perform any normalization needed,
e.g. ensuring that a parameter passed in is represented in a canonical
way. As an example, the CLI allows a user of the AWS plugin to pass in a
string, but canonically it is a string slice, and should be returned
that way as well. Not by chance, this is the exact behavior of the
testing implementation of this function.

This does not update any individual plugin, but rather sets the
controller-side logic up for plugins to support this functionality.
This commit is contained in:
Jeff Mitchell authored and GitHub committed 2022-08-18 15:39:30 -04:00
1 parent d4fce6fbe1
commit 7364d84835
9 files changed
+1013 -331

No files matched your search

+8
View File
@@ -19,6 +19,10 @@ func NewWrappingPluginClient(s plgpb.HostPluginServiceServer) *WrappingPluginCli
return &WrappingPluginClient{Server: s}
}
func (tpc *WrappingPluginClient) NormalizeCatalogData(ctx context.Context, req *plgpb.NormalizeCatalogDataRequest, opts ...grpc.CallOption) (*plgpb.NormalizeCatalogDataResponse, error) {
return tpc.Server.NormalizeCatalogData(ctx, req)
}
func (tpc *WrappingPluginClient) OnCreateCatalog(ctx context.Context, req *plgpb.OnCreateCatalogRequest, opts ...grpc.CallOption) (*plgpb.OnCreateCatalogResponse, error) {
return tpc.Server.OnCreateCatalog(ctx, req)
}
@@ -31,6 +35,10 @@ func (tpc *WrappingPluginClient) OnDeleteCatalog(ctx context.Context, req *plgpb
return tpc.Server.OnDeleteCatalog(ctx, req)
}
func (tpc *WrappingPluginClient) NormalizeSetData(ctx context.Context, req *plgpb.NormalizeSetDataRequest, opts ...grpc.CallOption) (*plgpb.NormalizeSetDataResponse, error) {
return tpc.Server.NormalizeSetData(ctx, req)
}
func (tpc *WrappingPluginClient) OnCreateSet(ctx context.Context, req *plgpb.OnCreateSetRequest, opts ...grpc.CallOption) (*plgpb.OnCreateSetResponse, error) {
return tpc.Server.OnCreateSet(ctx, req)
}
@@ -14,6 +14,7 @@ import (
"github.com/hashicorp/boundary/internal/oplog"
hostplugin "github.com/hashicorp/boundary/internal/plugin/host"
"github.com/hashicorp/boundary/internal/scheduler"
"github.com/hashicorp/boundary/internal/util"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
pbset "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin"
@@ -25,6 +26,38 @@ import (
"google.golang.org/protobuf/types/known/wrapperspb"
)
// normalizeCatalogAttributes allows a plugin to normalize attributes before
// they are saved
func normalizeCatalogAttributes(ctx context.Context, plgClient plgpb.HostPluginServiceClient, plgHc *pb.HostCatalog) error {
const op = "plugin.(Repository).normalizeCatalogAttributes"
switch {
case util.IsNil(plgClient):
return errors.New(ctx, errors.InvalidParameter, op, "plugin client is nil")
case plgHc == nil:
return errors.New(ctx, errors.InvalidParameter, op, "host catalog is nil")
case plgHc.GetAttributes() == nil:
return nil
}
ret, err := plgClient.NormalizeCatalogData(ctx, &plgpb.NormalizeCatalogDataRequest{
Attributes: plgHc.GetAttributes(),
})
switch {
case err == nil:
if ret.Attributes != nil {
plgHc.Attrs = &pb.HostCatalog_Attributes{
Attributes: ret.Attributes,
}
}
case status.Code(err) == codes.Unimplemented:
// Do nothing
default:
return errors.Wrap(ctx, err, op, errors.WithMsg("error asking plugin to normalize catalog data"))
}
return nil
}
// CreateCatalog inserts c into the repository and returns a new
// HostCatalog containing the catalog's PublicId. c must contain a valid
// ScopeID and PluginID. c must not contain a PublicId. The PublicId is
@@ -90,6 +123,12 @@ func (r *Repository) CreateCatalog(ctx context.Context, c *HostCatalog, _ ...Opt
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("plugin %q not available", c.GetPluginId()))
}
if plgHc.GetAttributes() != nil {
if err := normalizeCatalogAttributes(ctx, plgClient, plgHc); err != nil {
return nil, nil, errors.Wrap(ctx, err, op)
}
}
oplogWrapper, err := r.kms.GetWrapper(ctx, c.ScopeId, kms.KeyPurposeOplog)
if err != nil {
return nil, nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
@@ -321,6 +360,17 @@ func (r *Repository) UpdateCatalog(ctx context.Context, c *HostCatalog, version
return nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
if updateAttributes {
if newPlgHc.GetAttributes() != nil {
if err := normalizeCatalogAttributes(ctx, plgClient, newPlgHc); err != nil {
return nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
if newCatalog.Attributes, err = proto.Marshal(newPlgHc.GetAttributes()); err != nil {
return nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
}
}
dbWrapper, err := r.kms.GetWrapper(ctx, newCatalog.ScopeId, kms.KeyPurposeDatabase)
if err != nil {
return nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get db wrapper"))
@@ -14,12 +14,14 @@ import (
"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"
hostplg "github.com/hashicorp/boundary/internal/plugin/host"
hostplugin "github.com/hashicorp/boundary/internal/plugin/host"
"github.com/hashicorp/boundary/internal/scheduler"
"github.com/hashicorp/boundary/internal/scheduler/job"
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"
@@ -38,6 +40,8 @@ func TestRepository_CreateCatalog(t *testing.T) {
plg := hostplg.TestPlugin(t, conn, "test")
unimplementedPlugin := hostplg.TestPlugin(t, conn, "unimplemented")
const normalizeToSliceKey = "normalize_to_slice"
tests := []struct {
name string
in *HostCatalog
@@ -181,8 +185,9 @@ func TestRepository_CreateCatalog(t *testing.T) {
PluginId: plg.GetPublicId(),
Attributes: func() []byte {
st, err := structpb.NewStruct(map[string]interface{}{
"k1": "foo",
"nilkey": nil,
"k1": "foo",
"nilkey": nil,
normalizeToSliceKey: "normalizeme",
})
require.NoError(t, err)
b, err := proto.Marshal(st)
@@ -249,16 +254,38 @@ func TestRepository_CreateCatalog(t *testing.T) {
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert := assert.New(t)
require, assert := require.New(t), assert.New(t)
kmsCache := kms.TestKms(t, conn, wrapper)
// gotPluginAttrs tracks which attributes a plugin has received through a closure and can be compared in the
// test against the expected values sent to the plugin.
var gotPluginAttrs *structpb.Struct
var origPluginAttrs, gotPluginAttrs *structpb.Struct
if tt.in != nil && tt.in.HostCatalog != 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(): &WrappingPluginClient{
Server: &TestPluginServer{
NormalizeCatalogDataFn: func(_ context.Context, req *plgpb.NormalizeCatalogDataRequest) (*plgpb.NormalizeCatalogDataResponse, error) {
if req.Attributes == nil {
return new(plgpb.NormalizeCatalogDataResponse), nil
}
var attrs struct {
NormalizeToSlice string `mapstructure:"normalize_to_slice"`
}
require.NoError(mapstructure.Decode(req.Attributes.AsMap(), &attrs))
if attrs.NormalizeToSlice == "" {
return new(plgpb.NormalizeCatalogDataResponse), nil
}
retAttrs := proto.Clone(req.Attributes).(*structpb.Struct)
retAttrs.Fields[normalizeToSliceKey] = structpb.NewListValue(&structpb.ListValue{
Values: []*structpb.Value{structpb.NewStringValue(attrs.NormalizeToSlice)},
})
return &plgpb.NormalizeCatalogDataResponse{Attributes: retAttrs}, nil
},
OnCreateCatalogFn: func(_ context.Context, req *plgpb.OnCreateCatalogRequest) (*plgpb.OnCreateCatalogResponse, error) {
pluginCalled = true
gotPluginAttrs = req.GetCatalog().GetAttributes()
@@ -284,7 +311,7 @@ func TestRepository_CreateCatalog(t *testing.T) {
assert.Nil(got)
return
}
require.NoError(t, err)
require.NoError(err)
assert.Empty(tt.in.PublicId)
assert.NotNil(got)
assertPluginBasedPublicId(t, HostCatalogPrefix, got.PublicId)
@@ -293,11 +320,25 @@ func TestRepository_CreateCatalog(t *testing.T) {
assert.Equal(tt.want.Description, got.Description)
assert.Equal(got.CreateTime, got.UpdateTime)
// wantedPluginAttributes := &structpb.Struct{}
// require.NoError(t, proto.Unmarshal(tt.want.GetAttributes(), wantedPluginAttributes))
gotB, err := proto.Marshal(gotPluginAttrs)
require.NoError(t, err)
assert.Equal(tt.want.GetAttributes(), gotB)
if origPluginAttrs != nil {
if normalizeVal := origPluginAttrs.Fields[normalizeToSliceKey]; normalizeVal != nil {
gotVal := gotPluginAttrs.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.GetAttributes(), wantedPluginAttributes))
assert.Empty(cmp.Diff(wantedPluginAttributes, gotPluginAttrs, protocmp.Transform()))
assert.NoError(db.TestVerifyOplog(t, rw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)))
@@ -305,20 +346,20 @@ func TestRepository_CreateCatalog(t *testing.T) {
err = rw.LookupWhere(ctx, &cSecret, "catalog_id=?", []interface{}{got.GetPublicId()})
if tt.wantSecret == nil {
assert.Nil(got.Secrets)
require.Error(t, err)
require.True(t, errors.IsNotFoundError(err))
require.Error(err)
require.True(errors.IsNotFoundError(err))
return
}
require.NoError(t, err)
require.Empty(t, cSecret.Secret)
require.NotEmpty(t, cSecret.CtSecret)
require.NoError(err)
require.Empty(cSecret.Secret)
require.NotEmpty(cSecret.CtSecret)
dbWrapper, err := kmsCache.GetWrapper(ctx, got.GetScopeId(), kms.KeyPurposeDatabase)
require.NoError(t, err)
require.NoError(t, cSecret.decrypt(ctx, dbWrapper))
require.NoError(err)
require.NoError(cSecret.decrypt(ctx, dbWrapper))
st := &structpb.Struct{}
require.NoError(t, proto.Unmarshal(cSecret.Secret, st))
require.NoError(proto.Unmarshal(cSecret.Secret, st))
assert.Empty(cmp.Diff(tt.wantSecret, st, protocmp.Transform()))
})
}
@@ -431,6 +472,15 @@ func TestRepository_UpdateCatalog(t *testing.T) {
_, projectScope := iam.TestScopes(t, iam.TestRepo(t, dbConn, dbWrapper))
_, projectScopeAlt := iam.TestScopes(t, iam.TestRepo(t, dbConn, dbWrapper))
// Set up two existing catalogs for duplicate tests, one in the
// same scope that we are working in mostly, and another in a
// different one. Both are project scopes.
const (
testDuplicateCatalogNameProjectScope = "duplicate-catalog-name-project-scope"
testDuplicateCatalogNameAltScope = "duplicate-catalog-name-alt-scope"
normalizeToSliceKey = "normalize_to_slice"
)
// 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
@@ -444,6 +494,23 @@ func TestRepository_UpdateCatalog(t *testing.T) {
testPluginMap := map[string]plgpb.HostPluginServiceClient{
testPlugin.GetPublicId(): &WrappingPluginClient{
Server: &TestPluginServer{
NormalizeCatalogDataFn: func(_ context.Context, req *plgpb.NormalizeCatalogDataRequest) (*plgpb.NormalizeCatalogDataResponse, error) {
if req.Attributes == nil {
return new(plgpb.NormalizeCatalogDataResponse), nil
}
var attrs struct {
NormalizeToSlice string `mapstructure:"normalize_to_slice"`
}
require.NoError(t, mapstructure.Decode(req.Attributes.AsMap(), &attrs))
if attrs.NormalizeToSlice == "" {
return new(plgpb.NormalizeCatalogDataResponse), nil
}
retAttrs := proto.Clone(req.Attributes).(*structpb.Struct)
retAttrs.Fields[normalizeToSliceKey] = structpb.NewListValue(&structpb.ListValue{
Values: []*structpb.Value{structpb.NewStringValue(attrs.NormalizeToSlice)},
})
return &plgpb.NormalizeCatalogDataResponse{Attributes: retAttrs}, nil
},
OnUpdateCatalogFn: func(_ context.Context, req *plgpb.OnUpdateCatalogRequest) (*plgpb.OnUpdateCatalogResponse, error) {
gotOnUpdateCatalogRequest = req
respSecrets := req.GetNewCatalog().GetSecrets()
@@ -458,14 +525,6 @@ func TestRepository_UpdateCatalog(t *testing.T) {
},
}
// Set up two existing catalogs for duplicate tests, one in the
// same scope that we are working in mostly, and another in a
// different one. Both are project scopes.
const (
testDuplicateCatalogNameProjectScope = "duplicate-catalog-name-project-scope"
testDuplicateCatalogNameAltScope = "duplicate-catalog-name-alt-scope"
)
// Project scope
existingProjectScopeCatalog := TestCatalog(t, dbConn, projectScope.PublicId, testPlugin.GetPublicId())
existingProjectScopeCatalog.Name = testDuplicateCatalogNameProjectScope
@@ -597,6 +656,7 @@ func TestRepository_UpdateCatalog(t *testing.T) {
require := require.New(t)
st := &structpb.Struct{}
require.NoError(proto.Unmarshal(gotCatalog.Attributes, st))
assert.Equal(want, st.AsMap())
assert.Empty(cmp.Diff(mustStruct(want), st, protocmp.Transform()))
}
}
@@ -932,7 +992,8 @@ func TestRepository_UpdateCatalog(t *testing.T) {
{
name: "update attributes (overwrite)",
changeFuncs: []changeHostCatalogFunc{changeAttributes(map[string]interface{}{
"foo": "baz",
"foo": "baz",
normalizeToSliceKey: "normalizeme",
})},
version: 2,
fieldMask: []string{"attributes"},
@@ -943,10 +1004,12 @@ func TestRepository_UpdateCatalog(t *testing.T) {
"foo": "bar",
}),
checkUpdateCatalogRequestNewAttributes(map[string]interface{}{
"foo": "baz",
"foo": "baz",
normalizeToSliceKey: []any{"normalizeme"},
}),
checkAttributes(map[string]interface{}{
"foo": "baz",
"foo": "baz",
normalizeToSliceKey: []any{"normalizeme"},
}),
checkSecrets(map[string]interface{}{
"one": "two",
@@ -16,6 +16,7 @@ import (
"github.com/hashicorp/boundary/internal/oplog"
hostplugin "github.com/hashicorp/boundary/internal/plugin/host"
"github.com/hashicorp/boundary/internal/scheduler"
"github.com/hashicorp/boundary/internal/util"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"google.golang.org/grpc/codes"
@@ -25,6 +26,38 @@ import (
"google.golang.org/protobuf/types/known/wrapperspb"
)
// normalizeCatalogAttributes allows a plugin to normalize set attributes before
// they are saved
func normalizeSetAttributes(ctx context.Context, plgClient plgpb.HostPluginServiceClient, plgHs *pb.HostSet) error {
const op = "plugin.(Repository).normalizeSetAttributes"
switch {
case util.IsNil(plgClient):
return errors.New(ctx, errors.InvalidParameter, op, "plugin client is nil")
case plgHs == nil:
return errors.New(ctx, errors.InvalidParameter, op, "host set is nil")
case plgHs.GetAttributes() == nil:
return nil
}
ret, err := plgClient.NormalizeSetData(ctx, &plgpb.NormalizeSetDataRequest{
Attributes: plgHs.GetAttributes(),
})
switch {
case err == nil:
if ret.Attributes != nil {
plgHs.Attrs = &pb.HostSet_Attributes{
Attributes: ret.Attributes,
}
}
case status.Code(err) == codes.Unimplemented:
// Do nothing
default:
return errors.Wrap(ctx, err, op, errors.WithMsg("error asking plugin to normalize set data"))
}
return nil
}
// CreateSet inserts s into the repository and returns a new HostSet
// containing the host set's PublicId. s is not changed. s must contain a
// valid CatalogId. s must not contain a PublicId. The PublicId is
@@ -92,6 +125,12 @@ func (r *Repository) CreateSet(ctx context.Context, scopeId string, s *HostSet,
return nil, nil, errors.Wrap(ctx, err, op)
}
if plgHs.GetAttributes() != nil {
if err := normalizeSetAttributes(ctx, plgClient, plgHs); err != nil {
return nil, nil, errors.Wrap(ctx, err, op)
}
}
var preferredEndpoints []interface{}
if s.PreferredEndpoints != nil {
preferredEndpoints = make([]interface{}, 0, len(s.PreferredEndpoints))
@@ -334,6 +373,17 @@ func (r *Repository) UpdateSet(ctx context.Context, scopeId string, s *HostSet,
return nil, nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
if updateAttributes {
if newPlgSet.GetAttributes() != nil {
if err := normalizeSetAttributes(ctx, plgClient, newPlgSet); err != nil {
return nil, nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
if newSet.Attributes, err = proto.Marshal(newPlgSet.GetAttributes()); err != nil {
return nil, nil, nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
}
}
// Get the preferred endpoints to write out.
var preferredEndpoints []interface{}
if endpointOp == endpointOpUpdate {
@@ -17,10 +17,12 @@ import (
"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"
hostplg "github.com/hashicorp/boundary/internal/plugin/host"
"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"
@@ -44,6 +46,8 @@ func TestRepository_CreateSet(t *testing.T) {
unimplementedPluginCatalog := TestCatalog(t, conn, prj.PublicId, plg.GetPublicId())
attrs := []byte{}
const normalizeToSliceKey = "normalize_to_slice"
tests := []struct {
name string
in *HostSet
@@ -233,8 +237,9 @@ func TestRepository_CreateSet(t *testing.T) {
Description: ("test-description-repo"),
Attributes: func() []byte {
st, err := structpb.NewStruct(map[string]interface{}{
"k1": "foo",
"removed": nil,
"k1": "foo",
"removed": nil,
normalizeToSliceKey: "normalizeme",
})
require.NoError(t, err)
b, err := proto.Marshal(st)
@@ -262,14 +267,37 @@ func TestRepository_CreateSet(t *testing.T) {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
var pluginReceivedAttrs *structpb.Struct
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(): NewWrappingPluginClient(TestPluginServer{OnCreateSetFn: func(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) {
pluginCalled = true
pluginReceivedAttrs = req.GetSet().GetAttributes()
return &plgpb.OnCreateSetResponse{}, nil
}}),
plg.GetPublicId(): NewWrappingPluginClient(TestPluginServer{
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(): NewWrappingPluginClient(TestPluginServer{OnCreateSetFn: func(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) {
pluginCalled = true
pluginReceivedAttrs = req.GetSet().GetAttributes()
@@ -294,6 +322,23 @@ func TestRepository_CreateSet(t *testing.T) {
assert.Equal(tt.want.Name, got.GetName())
assert.Equal(tt.want.Description, got.GetDescription())
assert.Equal(got.GetCreateTime(), got.GetUpdateTime())
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()))
@@ -427,6 +472,8 @@ func TestRepository_UpdateSet(t *testing.T) {
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.
@@ -1007,7 +1054,8 @@ func TestRepository_UpdateSet(t *testing.T) {
name: "update attributes (overwrite)",
startingSet: setupHostSet,
changeFuncs: []changeHostSetFunc{changeAttributes(map[string]interface{}{
"foo": "baz",
"foo": "baz",
normalizeToSliceKey: "normalizeme",
})},
fieldMask: []string{"attributes"},
wantCheckPluginReqFuncs: []checkPluginReqFunc{
@@ -1015,7 +1063,8 @@ func TestRepository_UpdateSet(t *testing.T) {
"foo": "bar",
}),
checkUpdateSetRequestNewAttributes(map[string]interface{}{
"foo": "baz",
"foo": "baz",
normalizeToSliceKey: []any{"normalizeme"},
}),
checkUpdateSetRequestPersistedSecrets(map[string]interface{}{
"one": "two",
@@ -1024,7 +1073,8 @@ func TestRepository_UpdateSet(t *testing.T) {
wantCheckSetFuncs: []checkHostSetFunc{
checkVersion(3),
checkAttributes(map[string]interface{}{
"foo": "baz",
"foo": "baz",
normalizeToSliceKey: []any{"normalizeme"},
}),
checkNeedSync(true),
checkVerifySetOplog(oplog.OpType_OP_TYPE_UPDATE),
@@ -1147,6 +1197,23 @@ func TestRepository_UpdateSet(t *testing.T) {
testPluginMap := map[string]plgpb.HostPluginServiceClient{
testPlugin.GetPublicId(): &WrappingPluginClient{
Server: &TestPluginServer{
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 {
+23 -7
View File
@@ -233,16 +233,25 @@ var _ plgpb.HostPluginServiceServer = (*TestPluginServer)(nil)
// TestPluginServer provides a host plugin service server where each method can be overwritten for tests.
type TestPluginServer struct {
OnCreateCatalogFn func(context.Context, *plgpb.OnCreateCatalogRequest) (*plgpb.OnCreateCatalogResponse, error)
OnUpdateCatalogFn func(context.Context, *plgpb.OnUpdateCatalogRequest) (*plgpb.OnUpdateCatalogResponse, error)
OnDeleteCatalogFn func(context.Context, *plgpb.OnDeleteCatalogRequest) (*plgpb.OnDeleteCatalogResponse, error)
OnCreateSetFn func(context.Context, *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error)
OnUpdateSetFn func(context.Context, *plgpb.OnUpdateSetRequest) (*plgpb.OnUpdateSetResponse, error)
OnDeleteSetFn func(context.Context, *plgpb.OnDeleteSetRequest) (*plgpb.OnDeleteSetResponse, error)
ListHostsFn func(context.Context, *plgpb.ListHostsRequest) (*plgpb.ListHostsResponse, error)
NormalizeCatalogDataFn func(context.Context, *plgpb.NormalizeCatalogDataRequest) (*plgpb.NormalizeCatalogDataResponse, error)
OnCreateCatalogFn func(context.Context, *plgpb.OnCreateCatalogRequest) (*plgpb.OnCreateCatalogResponse, error)
OnUpdateCatalogFn func(context.Context, *plgpb.OnUpdateCatalogRequest) (*plgpb.OnUpdateCatalogResponse, error)
OnDeleteCatalogFn func(context.Context, *plgpb.OnDeleteCatalogRequest) (*plgpb.OnDeleteCatalogResponse, error)
NormalizeSetDataFn func(context.Context, *plgpb.NormalizeSetDataRequest) (*plgpb.NormalizeSetDataResponse, error)
OnCreateSetFn func(context.Context, *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error)
OnUpdateSetFn func(context.Context, *plgpb.OnUpdateSetRequest) (*plgpb.OnUpdateSetResponse, error)
OnDeleteSetFn func(context.Context, *plgpb.OnDeleteSetRequest) (*plgpb.OnDeleteSetResponse, error)
ListHostsFn func(context.Context, *plgpb.ListHostsRequest) (*plgpb.ListHostsResponse, error)
plgpb.UnimplementedHostPluginServiceServer
}
func (t TestPluginServer) NormalizeCatalogData(ctx context.Context, req *plgpb.NormalizeCatalogDataRequest) (*plgpb.NormalizeCatalogDataResponse, error) {
if t.NormalizeCatalogDataFn == nil {
return t.UnimplementedHostPluginServiceServer.NormalizeCatalogData(ctx, req)
}
return t.NormalizeCatalogDataFn(ctx, req)
}
func (t TestPluginServer) OnCreateCatalog(ctx context.Context, req *plgpb.OnCreateCatalogRequest) (*plgpb.OnCreateCatalogResponse, error) {
if t.OnCreateCatalogFn == nil {
return t.UnimplementedHostPluginServiceServer.OnCreateCatalog(ctx, req)
@@ -264,6 +273,13 @@ func (t TestPluginServer) OnDeleteCatalog(ctx context.Context, req *plgpb.OnDele
return t.OnDeleteCatalogFn(ctx, req)
}
func (t TestPluginServer) NormalizeSetData(ctx context.Context, req *plgpb.NormalizeSetDataRequest) (*plgpb.NormalizeSetDataResponse, error) {
if t.NormalizeSetDataFn == nil {
return t.UnimplementedHostPluginServiceServer.NormalizeSetData(ctx, req)
}
return t.NormalizeSetDataFn(ctx, req)
}
func (t TestPluginServer) OnCreateSet(ctx context.Context, req *plgpb.OnCreateSetRequest) (*plgpb.OnCreateSetResponse, error) {
if t.OnCreateSetFn == nil {
return t.UnimplementedHostPluginServiceServer.OnCreateSet(ctx, req)
@@ -10,8 +10,17 @@ option go_package = "github.com/hashicorp/boundary/sdk/pbs/plugin;plugin";
// HostPluginService describes the service for host plugins.
service HostPluginService {
// OnCreateCatalog is a hook that runs when a host catalog is
// created.
// NormalizeCatalogData is a hook that passes attributes to the plugin and
// allows those values to be normalized prior to creating or updating those
// values in the host catalog data.
//
// NormalizeCatalogData is called before:
// * OnCreateCatalog
// * OnUpdateCatalog
rpc NormalizeCatalogData(NormalizeCatalogDataRequest) returns (NormalizeCatalogDataResponse);
// OnCreateCatalog is a hook that runs when a
// host catalog is created.
rpc OnCreateCatalog(OnCreateCatalogRequest) returns (OnCreateCatalogResponse);
// OnUpdateCatalog is a hook that runs when a host catalog is
@@ -22,6 +31,15 @@ service HostPluginService {
// deleted.
rpc OnDeleteCatalog(OnDeleteCatalogRequest) returns (OnDeleteCatalogResponse);
// NormalizeSetData is a hook that passes attributes to the plugin and
// allows those values to be normalized prior to creating or updating those
// values in the host set data.
//
// NormalizeSetData is called before:
// * OnCreateSet
// * OnUpdateSet
rpc NormalizeSetData(NormalizeSetDataRequest) returns (NormalizeSetDataResponse);
// OnCreateSet is a hook that runs when a host set is created.
rpc OnCreateSet(OnCreateSetRequest) returns (OnCreateSetResponse);
@@ -35,6 +53,17 @@ service HostPluginService {
rpc ListHosts(ListHostsRequest) returns (ListHostsResponse);
}
message NormalizeCatalogDataRequest {
// The incoming attributes in the create or update request.
google.protobuf.Struct attributes = 100;
}
message NormalizeCatalogDataResponse {
// Outgoing attributes. If nil, no changes will be recorded. If non-nil, the
// values here will be used in place of the original set of attributes.
google.protobuf.Struct attributes = 100;
}
message OnCreateCatalogRequest {
// The host catalog to create. The request may contain optional
// secret data to help authenticate the request against a cloud
@@ -84,6 +113,17 @@ message OnDeleteCatalogRequest {
message OnDeleteCatalogResponse {}
message NormalizeSetDataRequest {
// The incoming attributes in the create or update request.
google.protobuf.Struct attributes = 100;
}
message NormalizeSetDataResponse {
// Outgoing attributes. If nil, no changes will be recorded. If non-nil, the
// values here will be used in place of the original set of attributes.
google.protobuf.Struct attributes = 100;
}
message OnCreateSetRequest {
// The host catalog that the set belongs to.
controller.api.resources.hostcatalogs.v1.HostCatalog catalog = 10;
File diff suppressed because it is too large. Load diff
+104 -4
View File
@@ -18,8 +18,16 @@ const _ = grpc.SupportPackageIsVersion7
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type HostPluginServiceClient interface {
// OnCreateCatalog is a hook that runs when a host catalog is
// created.
// NormalizeCatalogData is a hook that passes attributes to the plugin and
// allows those values to be normalized prior to creating or updating those
// values in the host catalog data.
//
// NormalizeCatalogData is called before:
// * OnCreateCatalog
// * OnUpdateCatalog
NormalizeCatalogData(ctx context.Context, in *NormalizeCatalogDataRequest, opts ...grpc.CallOption) (*NormalizeCatalogDataResponse, error)
// OnCreateCatalog is a hook that runs when a
// host catalog is created.
OnCreateCatalog(ctx context.Context, in *OnCreateCatalogRequest, opts ...grpc.CallOption) (*OnCreateCatalogResponse, error)
// OnUpdateCatalog is a hook that runs when a host catalog is
// updated.
@@ -27,6 +35,14 @@ type HostPluginServiceClient interface {
// OnDeleteCatalog is a hook that runs when a host catalog is
// deleted.
OnDeleteCatalog(ctx context.Context, in *OnDeleteCatalogRequest, opts ...grpc.CallOption) (*OnDeleteCatalogResponse, error)
// NormalizeSetData is a hook that passes attributes to the plugin and
// allows those values to be normalized prior to creating or updating those
// values in the host set data.
//
// NormalizeSetData is called before:
// * OnCreateSet
// * OnUpdateSet
NormalizeSetData(ctx context.Context, in *NormalizeSetDataRequest, opts ...grpc.CallOption) (*NormalizeSetDataResponse, error)
// OnCreateSet is a hook that runs when a host set is created.
OnCreateSet(ctx context.Context, in *OnCreateSetRequest, opts ...grpc.CallOption) (*OnCreateSetResponse, error)
// OnUpdateSet is a hook that runs when a host set is updated.
@@ -45,6 +61,15 @@ func NewHostPluginServiceClient(cc grpc.ClientConnInterface) HostPluginServiceCl
return &hostPluginServiceClient{cc}
}
func (c *hostPluginServiceClient) NormalizeCatalogData(ctx context.Context, in *NormalizeCatalogDataRequest, opts ...grpc.CallOption) (*NormalizeCatalogDataResponse, error) {
out := new(NormalizeCatalogDataResponse)
err := c.cc.Invoke(ctx, "/plugin.v1.HostPluginService/NormalizeCatalogData", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *hostPluginServiceClient) OnCreateCatalog(ctx context.Context, in *OnCreateCatalogRequest, opts ...grpc.CallOption) (*OnCreateCatalogResponse, error) {
out := new(OnCreateCatalogResponse)
err := c.cc.Invoke(ctx, "/plugin.v1.HostPluginService/OnCreateCatalog", in, out, opts...)
@@ -72,6 +97,15 @@ func (c *hostPluginServiceClient) OnDeleteCatalog(ctx context.Context, in *OnDel
return out, nil
}
func (c *hostPluginServiceClient) NormalizeSetData(ctx context.Context, in *NormalizeSetDataRequest, opts ...grpc.CallOption) (*NormalizeSetDataResponse, error) {
out := new(NormalizeSetDataResponse)
err := c.cc.Invoke(ctx, "/plugin.v1.HostPluginService/NormalizeSetData", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *hostPluginServiceClient) OnCreateSet(ctx context.Context, in *OnCreateSetRequest, opts ...grpc.CallOption) (*OnCreateSetResponse, error) {
out := new(OnCreateSetResponse)
err := c.cc.Invoke(ctx, "/plugin.v1.HostPluginService/OnCreateSet", in, out, opts...)
@@ -112,8 +146,16 @@ func (c *hostPluginServiceClient) ListHosts(ctx context.Context, in *ListHostsRe
// All implementations must embed UnimplementedHostPluginServiceServer
// for forward compatibility
type HostPluginServiceServer interface {
// OnCreateCatalog is a hook that runs when a host catalog is
// created.
// NormalizeCatalogData is a hook that passes attributes to the plugin and
// allows those values to be normalized prior to creating or updating those
// values in the host catalog data.
//
// NormalizeCatalogData is called before:
// * OnCreateCatalog
// * OnUpdateCatalog
NormalizeCatalogData(context.Context, *NormalizeCatalogDataRequest) (*NormalizeCatalogDataResponse, error)
// OnCreateCatalog is a hook that runs when a
// host catalog is created.
OnCreateCatalog(context.Context, *OnCreateCatalogRequest) (*OnCreateCatalogResponse, error)
// OnUpdateCatalog is a hook that runs when a host catalog is
// updated.
@@ -121,6 +163,14 @@ type HostPluginServiceServer interface {
// OnDeleteCatalog is a hook that runs when a host catalog is
// deleted.
OnDeleteCatalog(context.Context, *OnDeleteCatalogRequest) (*OnDeleteCatalogResponse, error)
// NormalizeSetData is a hook that passes attributes to the plugin and
// allows those values to be normalized prior to creating or updating those
// values in the host set data.
//
// NormalizeSetData is called before:
// * OnCreateSet
// * OnUpdateSet
NormalizeSetData(context.Context, *NormalizeSetDataRequest) (*NormalizeSetDataResponse, error)
// OnCreateSet is a hook that runs when a host set is created.
OnCreateSet(context.Context, *OnCreateSetRequest) (*OnCreateSetResponse, error)
// OnUpdateSet is a hook that runs when a host set is updated.
@@ -136,6 +186,9 @@ type HostPluginServiceServer interface {
type UnimplementedHostPluginServiceServer struct {
}
func (UnimplementedHostPluginServiceServer) NormalizeCatalogData(context.Context, *NormalizeCatalogDataRequest) (*NormalizeCatalogDataResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method NormalizeCatalogData not implemented")
}
func (UnimplementedHostPluginServiceServer) OnCreateCatalog(context.Context, *OnCreateCatalogRequest) (*OnCreateCatalogResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method OnCreateCatalog not implemented")
}
@@ -145,6 +198,9 @@ func (UnimplementedHostPluginServiceServer) OnUpdateCatalog(context.Context, *On
func (UnimplementedHostPluginServiceServer) OnDeleteCatalog(context.Context, *OnDeleteCatalogRequest) (*OnDeleteCatalogResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method OnDeleteCatalog not implemented")
}
func (UnimplementedHostPluginServiceServer) NormalizeSetData(context.Context, *NormalizeSetDataRequest) (*NormalizeSetDataResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method NormalizeSetData not implemented")
}
func (UnimplementedHostPluginServiceServer) OnCreateSet(context.Context, *OnCreateSetRequest) (*OnCreateSetResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method OnCreateSet not implemented")
}
@@ -170,6 +226,24 @@ func RegisterHostPluginServiceServer(s grpc.ServiceRegistrar, srv HostPluginServ
s.RegisterService(&HostPluginService_ServiceDesc, srv)
}
func _HostPluginService_NormalizeCatalogData_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(NormalizeCatalogDataRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HostPluginServiceServer).NormalizeCatalogData(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/plugin.v1.HostPluginService/NormalizeCatalogData",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HostPluginServiceServer).NormalizeCatalogData(ctx, req.(*NormalizeCatalogDataRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HostPluginService_OnCreateCatalog_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(OnCreateCatalogRequest)
if err := dec(in); err != nil {
@@ -224,6 +298,24 @@ func _HostPluginService_OnDeleteCatalog_Handler(srv interface{}, ctx context.Con
return interceptor(ctx, in, info, handler)
}
func _HostPluginService_NormalizeSetData_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(NormalizeSetDataRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HostPluginServiceServer).NormalizeSetData(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/plugin.v1.HostPluginService/NormalizeSetData",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HostPluginServiceServer).NormalizeSetData(ctx, req.(*NormalizeSetDataRequest))
}
return interceptor(ctx, in, info, handler)
}
func _HostPluginService_OnCreateSet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(OnCreateSetRequest)
if err := dec(in); err != nil {
@@ -303,6 +395,10 @@ var HostPluginService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "plugin.v1.HostPluginService",
HandlerType: (*HostPluginServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "NormalizeCatalogData",
Handler: _HostPluginService_NormalizeCatalogData_Handler,
},
{
MethodName: "OnCreateCatalog",
Handler: _HostPluginService_OnCreateCatalog_Handler,
@@ -315,6 +411,10 @@ var HostPluginService_ServiceDesc = grpc.ServiceDesc{
MethodName: "OnDeleteCatalog",
Handler: _HostPluginService_OnDeleteCatalog_Handler,
},
{
MethodName: "NormalizeSetData",
Handler: _HostPluginService_NormalizeSetData_Handler,
},
{
MethodName: "OnCreateSet",
Handler: _HostPluginService_OnCreateSet_Handler,