diff --git a/agent/grpc-external/services/resource/write.go b/agent/grpc-external/services/resource/write.go index a836b3e39..57380f8f6 100644 --- a/agent/grpc-external/services/resource/write.go +++ b/agent/grpc-external/services/resource/write.go @@ -73,6 +73,10 @@ func (s *Server) Write(ctx context.Context, req *pbresource.WriteRequest) (*pbre 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. // // This makes it possible to *safely* do things like keeping the Uid stable diff --git a/agent/grpc-external/services/resource/write_test.go b/agent/grpc-external/services/resource/write_test.go index 4e83cb8bb..fa576da5d 100644 --- a/agent/grpc-external/services/resource/write_test.go +++ b/agent/grpc-external/services/resource/write_test.go @@ -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) { server := testServer(t) client := testClient(t, server) diff --git a/internal/resource/demo/demo.go b/internal/resource/demo/demo.go index b9f8ab68d..2c9fe4b43 100644 --- a/internal/resource/demo/demo.go +++ b/internal/resource/demo/demo.go @@ -108,6 +108,19 @@ func Register(r resource.Registry) { 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{ Type: TypeV1Artist, Proto: &pbdemov1.Artist{}, @@ -138,6 +151,7 @@ func Register(r resource.Registry) { List: makeListACL(TypeV2Artist), }, Validate: validateV2ArtistFn, + Mutate: mutateV2ArtistFn, }) r.Register(resource.Registration{ diff --git a/internal/resource/registry.go b/internal/resource/registry.go index 6b49b2313..dac144ab8 100644 --- a/internal/resource/registry.go +++ b/internal/resource/registry.go @@ -35,6 +35,9 @@ type Registration struct { // check for required fields). 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. // 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 } } + // default mutate to a no-op + if registration.Mutate == nil { + registration.Mutate = func(resource *pbresource.Resource) error { return nil } + } + r.registrations[key] = registration } diff --git a/internal/resource/registry_test.go b/internal/resource/registry_test.go index 7360e48d1..a2e24badc 100644 --- a/internal/resource/registry_test.go +++ b/internal/resource/registry_test.go @@ -61,10 +61,7 @@ func TestRegister(t *testing.T) { func TestRegister_Defaults(t *testing.T) { r := resource.NewRegistry() - r.Register(resource.Registration{ - Type: demo.TypeV2Artist, - // intentionally don't provide ACLs so defaults kick in - }) + r.Register(resource.Registration{Type: demo.TypeV2Artist}) artist, err := demo.GenerateV2Artist() require.NoError(t, err) @@ -85,6 +82,9 @@ func TestRegister_Defaults(t *testing.T) { // verify default validate is a no-op 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) {