diff --git a/agent/grpc-external/services/resource/list.go b/agent/grpc-external/services/resource/list.go new file mode 100644 index 0000000000..d43645c050 --- /dev/null +++ b/agent/grpc-external/services/resource/list.go @@ -0,0 +1,28 @@ +package resource + +import ( + "context" + + "github.com/hashicorp/consul/internal/storage" + "github.com/hashicorp/consul/proto-public/pbresource" +) + +func (s *Server) List(ctx context.Context, req *pbresource.ListRequest) (*pbresource.ListResponse, error) { + if _, err := s.resolveType(req.Type); err != nil { + return nil, err + } + + resources, err := s.backend.List(ctx, readConsistencyFrom(ctx), storage.UnversionedTypeFrom(req.Type), req.Tenancy, req.NamePrefix) + if err != nil { + return nil, err + } + + // filter out non-matching GroupVersion + result := make([]*pbresource.Resource, 0) + for _, resource := range resources { + if resource.Id.Type.GroupVersion == req.Type.GroupVersion { + result = append(result, resource) + } + } + return &pbresource.ListResponse{Resources: result}, nil +} diff --git a/agent/grpc-external/services/resource/list_test.go b/agent/grpc-external/services/resource/list_test.go new file mode 100644 index 0000000000..9eb28d23d5 --- /dev/null +++ b/agent/grpc-external/services/resource/list_test.go @@ -0,0 +1,145 @@ +package resource + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/consul/internal/resource" + "github.com/hashicorp/consul/internal/storage" + "github.com/hashicorp/consul/proto-public/pbresource" + "github.com/hashicorp/consul/proto/private/prototest" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +func TestList_TypeNotFound(t *testing.T) { + server := testServer(t) + client := testClient(t, server) + + _, err := client.List(context.Background(), &pbresource.ListRequest{ + Type: typev1, + Tenancy: tenancy, + NamePrefix: "", + }) + require.Error(t, err) + require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) + require.Contains(t, err.Error(), "resource type mesh/v1/service not registered") +} + +func TestList_Empty(t *testing.T) { + for desc, tc := range listTestCases() { + t.Run(desc, func(t *testing.T) { + server := testServer(t) + client := testClient(t, server) + server.registry.Register(resource.Registration{Type: typev1}) + + rsp, err := client.List(tc.ctx, &pbresource.ListRequest{ + Type: typev1, + Tenancy: tenancy, + NamePrefix: "", + }) + require.NoError(t, err) + require.Empty(t, rsp.Resources) + }) + } +} + +func TestList_Many(t *testing.T) { + for desc, tc := range listTestCases() { + t.Run(desc, func(t *testing.T) { + server := testServer(t) + client := testClient(t, server) + server.registry.Register(resource.Registration{Type: typev1}) + + resources := make([]*pbresource.Resource, 10) + for i := 0; i < len(resources); i++ { + r := &pbresource.Resource{ + Id: &pbresource.ID{ + Uid: fmt.Sprintf("uid%d", i), + Name: fmt.Sprintf("name%d", i), + Type: typev1, + Tenancy: tenancy, + }, + Version: "", + } + server.backend.WriteCAS(tc.ctx, r) + } + + rsp, err := client.List(tc.ctx, &pbresource.ListRequest{ + Type: typev1, + Tenancy: tenancy, + NamePrefix: "", + }) + require.NoError(t, err) + prototest.AssertElementsMatch(t, resources, rsp.Resources) + }) + } +} + +func TestList_GroupVersionMismatch(t *testing.T) { + for desc, tc := range listTestCases() { + t.Run(desc, func(t *testing.T) { + server := testServer(t) + client := testClient(t, server) + server.registry.Register(resource.Registration{Type: typev1}) + server.backend.WriteCAS(tc.ctx, &pbresource.Resource{Id: id2}) + + rsp, err := client.List(tc.ctx, &pbresource.ListRequest{ + Type: typev1, + Tenancy: tenancy, + NamePrefix: "", + }) + require.NoError(t, err) + require.Empty(t, rsp.Resources) + }) + } +} + +func TestList_VerifyReadConsistencyArg(t *testing.T) { + // Uses a mockBackend instead of the inmem Backend to verify the ReadConsistency argument is set correctly. + for desc, tc := range listTestCases() { + t.Run(desc, func(t *testing.T) { + mockBackend := NewMockBackend(t) + server := NewServer(Config{ + registry: resource.NewRegistry(), + backend: mockBackend, + }) + server.registry.Register(resource.Registration{Type: typev1}) + resource1 := &pbresource.Resource{Id: id1, Version: "1"} + mockBackend.On("List", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return([]*pbresource.Resource{resource1}, nil) + client := testClient(t, server) + + rsp, err := client.List(tc.ctx, &pbresource.ListRequest{Type: typev1, Tenancy: tenancy, NamePrefix: ""}) + require.NoError(t, err) + prototest.AssertDeepEqual(t, resource1, rsp.Resources[0]) + mockBackend.AssertCalled(t, "List", mock.Anything, tc.consistency, mock.Anything, mock.Anything, mock.Anything) + }) + } +} + +type listTestCase struct { + consistency storage.ReadConsistency + ctx context.Context +} + +func listTestCases() map[string]listTestCase { + return map[string]listTestCase{ + "eventually consistent read": { + consistency: storage.EventualConsistency, + ctx: context.Background(), + }, + "strongly consistent read": { + consistency: storage.StrongConsistency, + ctx: metadata.NewOutgoingContext( + context.Background(), + metadata.New(map[string]string{"x-consul-consistency-mode": "consistent"}), + ), + }, + } +} diff --git a/agent/grpc-external/services/resource/read.go b/agent/grpc-external/services/resource/read.go index ad84ae2b24..1c924bff2e 100644 --- a/agent/grpc-external/services/resource/read.go +++ b/agent/grpc-external/services/resource/read.go @@ -3,30 +3,21 @@ package resource import ( "context" "errors" - "fmt" "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" - "github.com/hashicorp/consul/internal/resource" "github.com/hashicorp/consul/internal/storage" "github.com/hashicorp/consul/proto-public/pbresource" ) func (s *Server) Read(ctx context.Context, req *pbresource.ReadRequest) (*pbresource.ReadResponse, error) { // check type exists - _, ok := s.registry.Resolve(req.Id.Type) - if !ok { - return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("resource type %s not registered", resource.ToGVK(req.Id.Type))) + if _, err := s.resolveType(req.Id.Type); err != nil { + return nil, err } - consistency := storage.EventualConsistency - if isConsistentRead(ctx) { - consistency = storage.StrongConsistency - } - - resource, err := s.backend.Read(ctx, consistency, req.Id) + resource, err := s.backend.Read(ctx, readConsistencyFrom(ctx), req.Id) if err != nil { if errors.Is(err, storage.ErrNotFound) { return nil, status.Error(codes.NotFound, err.Error()) @@ -38,17 +29,3 @@ func (s *Server) Read(ctx context.Context, req *pbresource.ReadRequest) (*pbreso } return &pbresource.ReadResponse{Resource: resource}, nil } - -func isConsistentRead(ctx context.Context) bool { - md, ok := metadata.FromIncomingContext(ctx) - if !ok { - return false - } - - vals := md.Get("x-consul-consistency-mode") - if len(vals) == 0 { - return false - } - - return vals[0] == "consistent" -} diff --git a/agent/grpc-external/services/resource/server.go b/agent/grpc-external/services/resource/server.go index ab4d107629..105d87ff5e 100644 --- a/agent/grpc-external/services/resource/server.go +++ b/agent/grpc-external/services/resource/server.go @@ -5,6 +5,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "github.com/hashicorp/consul/internal/resource" @@ -51,16 +52,12 @@ func (s *Server) WriteStatus(ctx context.Context, req *pbresource.WriteStatusReq return &pbresource.WriteStatusResponse{}, nil } -func (s *Server) List(ctx context.Context, req *pbresource.ListRequest) (*pbresource.ListResponse, error) { - // TODO - return &pbresource.ListResponse{}, nil -} - func (s *Server) Delete(ctx context.Context, req *pbresource.DeleteRequest) (*pbresource.DeleteResponse, error) { // TODO return &pbresource.DeleteResponse{}, nil } +//nolint:unparam func (s *Server) resolveType(typ *pbresource.Type) (*resource.Registration, error) { v, ok := s.registry.Resolve(typ) if ok { @@ -71,3 +68,20 @@ func (s *Server) resolveType(typ *pbresource.Type) (*resource.Registration, erro "resource type %s not registered", resource.ToGVK(typ), ) } + +func readConsistencyFrom(ctx context.Context) storage.ReadConsistency { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return storage.EventualConsistency + } + + vals := md.Get("x-consul-consistency-mode") + if len(vals) == 0 { + return storage.EventualConsistency + } + + if vals[0] == "consistent" { + return storage.StrongConsistency + } + return storage.EventualConsistency +} diff --git a/agent/grpc-external/services/resource/server_test.go b/agent/grpc-external/services/resource/server_test.go index 277a01c7b2..6c7cd478fc 100644 --- a/agent/grpc-external/services/resource/server_test.go +++ b/agent/grpc-external/services/resource/server_test.go @@ -29,14 +29,6 @@ func TestWriteStatus_TODO(t *testing.T) { require.NotNil(t, resp) } -func TestList_TODO(t *testing.T) { - server := NewServer(Config{}) - client := testClient(t, server) - resp, err := client.List(context.Background(), &pbresource.ListRequest{}) - require.NoError(t, err) - require.NotNil(t, resp) -} - func TestDelete_TODO(t *testing.T) { server := NewServer(Config{}) client := testClient(t, server)