Add mutate hook to `Write` endpoint (#16958)

This commit is contained in:
Semir Patel 2023-04-12 16:50:07 -05:00 committed by GitHub
parent ad5a4201d5
commit f9311318e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 55 additions and 4 deletions

View File

@ -73,6 +73,10 @@ func (s *Server) Write(ctx context.Context, req *pbresource.WriteRequest) (*pbre
return nil, status.Error(codes.InvalidArgument, err.Error()) return nil, status.Error(codes.InvalidArgument, err.Error())
} }
if err = reg.Mutate(req.Resource); err != nil {
return nil, status.Errorf(codes.Internal, "failed mutate hook: %v", err.Error())
}
// At the storage backend layer, all writes are CAS operations. // At the storage backend layer, all writes are CAS operations.
// //
// This makes it possible to *safely* do things like keeping the Uid stable // This makes it possible to *safely* do things like keeping the Uid stable

View File

@ -114,6 +114,31 @@ func TestWrite_ACLs(t *testing.T) {
} }
} }
func TestWrite_Mutate(t *testing.T) {
server := testServer(t)
client := testClient(t, server)
demo.Register(server.Registry)
artist, err := demo.GenerateV2Artist()
require.NoError(t, err)
artistData := &pbdemov2.Artist{}
artist.Data.UnmarshalTo(artistData)
require.NoError(t, err)
// mutate hook sets genre to disco when unspecified
artistData.Genre = pbdemov2.Genre_GENRE_UNSPECIFIED
artist.Data.MarshalFrom(artistData)
require.NoError(t, err)
rsp, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: artist})
require.NoError(t, err)
// verify mutate hook set genre to disco
require.NoError(t, rsp.Resource.Data.UnmarshalTo(artistData))
require.Equal(t, pbdemov2.Genre_GENRE_DISCO, artistData.Genre)
}
func TestWrite_ResourceCreation_Success(t *testing.T) { func TestWrite_ResourceCreation_Success(t *testing.T) {
server := testServer(t) server := testServer(t)
client := testClient(t, server) client := testClient(t, server)

View File

@ -108,6 +108,19 @@ func Register(r resource.Registry) {
return nil return nil
} }
mutateV2ArtistFn := func(res *pbresource.Resource) error {
// Not a realistic use for this hook, but set genre if not specified
artist := &pbdemov2.Artist{}
if err := anypb.UnmarshalTo(res.Data, artist, proto.UnmarshalOptions{}); err != nil {
return err
}
if artist.Genre == pbdemov2.Genre_GENRE_UNSPECIFIED {
artist.Genre = pbdemov2.Genre_GENRE_DISCO
return res.Data.MarshalFrom(artist)
}
return nil
}
r.Register(resource.Registration{ r.Register(resource.Registration{
Type: TypeV1Artist, Type: TypeV1Artist,
Proto: &pbdemov1.Artist{}, Proto: &pbdemov1.Artist{},
@ -138,6 +151,7 @@ func Register(r resource.Registry) {
List: makeListACL(TypeV2Artist), List: makeListACL(TypeV2Artist),
}, },
Validate: validateV2ArtistFn, Validate: validateV2ArtistFn,
Mutate: mutateV2ArtistFn,
}) })
r.Register(resource.Registration{ r.Register(resource.Registration{

View File

@ -35,6 +35,9 @@ type Registration struct {
// check for required fields). // check for required fields).
Validate func(*pbresource.Resource) error Validate func(*pbresource.Resource) error
// Mutate is called to fill out any autogenerated fields (e.g. UUIDs).
Mutate func(*pbresource.Resource) error
// In the future, we'll add hooks, the controller etc. here. // In the future, we'll add hooks, the controller etc. here.
// TODO: https://github.com/hashicorp/consul/pull/16622#discussion_r1134515909 // TODO: https://github.com/hashicorp/consul/pull/16622#discussion_r1134515909
} }
@ -109,6 +112,11 @@ func (r *TypeRegistry) Register(registration Registration) {
registration.Validate = func(resource *pbresource.Resource) error { return nil } registration.Validate = func(resource *pbresource.Resource) error { return nil }
} }
// default mutate to a no-op
if registration.Mutate == nil {
registration.Mutate = func(resource *pbresource.Resource) error { return nil }
}
r.registrations[key] = registration r.registrations[key] = registration
} }

View File

@ -61,10 +61,7 @@ func TestRegister(t *testing.T) {
func TestRegister_Defaults(t *testing.T) { func TestRegister_Defaults(t *testing.T) {
r := resource.NewRegistry() r := resource.NewRegistry()
r.Register(resource.Registration{ r.Register(resource.Registration{Type: demo.TypeV2Artist})
Type: demo.TypeV2Artist,
// intentionally don't provide ACLs so defaults kick in
})
artist, err := demo.GenerateV2Artist() artist, err := demo.GenerateV2Artist()
require.NoError(t, err) require.NoError(t, err)
@ -85,6 +82,9 @@ func TestRegister_Defaults(t *testing.T) {
// verify default validate is a no-op // verify default validate is a no-op
require.NoError(t, reg.Validate(nil)) require.NoError(t, reg.Validate(nil))
// verify default mutate is a no-op
require.NoError(t, reg.Mutate(nil))
} }
func assertRegisterPanics(t *testing.T, registerFn func(reg resource.Registration), registration resource.Registration, panicString string) { func assertRegisterPanics(t *testing.T, registerFn func(reg resource.Registration), registration resource.Registration, panicString string) {