Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resource: Allow nil tenancy #18618

Merged
merged 1 commit into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 28 additions & 20 deletions agent/grpc-external/services/resource/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (
"github.com/hashicorp/consul/acl/resolver"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/resource/demo"
"github.com/hashicorp/consul/internal/storage"
"github.com/hashicorp/consul/proto-public/pbresource"
)

Expand All @@ -34,10 +33,6 @@ func TestDelete_InputValidation(t *testing.T) {
artistId.Type = nil
return artistId
},
"no tenancy": func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Tenancy = nil
return artistId
},
"no name": func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Name = ""
return artistId
Expand Down Expand Up @@ -102,21 +97,23 @@ func TestDelete_ACLs(t *testing.T) {
t.Run(desc, func(t *testing.T) {
server := testServer(t)
client := testClient(t, server)

mockACLResolver := &MockACLResolver{}
mockACLResolver.On("ResolveTokenAndDefaultMeta", mock.Anything, mock.Anything, mock.Anything).
Return(tc.authz, nil)
server.ACLResolver = mockACLResolver
demo.RegisterTypes(server.Registry)

artist, err := demo.GenerateV2Artist()
require.NoError(t, err)

artist, err = server.Backend.WriteCAS(context.Background(), artist)
// Write test resource to delete.
rsp, err := client.Write(context.Background(), &pbresource.WriteRequest{Resource: artist})
require.NoError(t, err)

// exercise ACL
_, err = client.Delete(testContext(t), &pbresource.DeleteRequest{Id: artist.Id})
// Mock is put in place after the above "write" since the "write" must also pass the ACL check.
mockACLResolver := &MockACLResolver{}
mockACLResolver.On("ResolveTokenAndDefaultMeta", mock.Anything, mock.Anything, mock.Anything).
Return(tc.authz, nil)
server.ACLResolver = mockACLResolver

// Exercise ACL.
_, err = client.Delete(testContext(t), &pbresource.DeleteRequest{Id: rsp.Resource.Id})
tc.assertErrFn(err)
})
}
Expand All @@ -134,15 +131,20 @@ func TestDelete_Success(t *testing.T) {

recordLabel, err := demo.GenerateV1RecordLabel("LoonyTunes")
require.NoError(t, err)
recordLabel, err = server.Backend.WriteCAS(ctx, recordLabel)
writeRsp, err := client.Write(ctx, &pbresource.WriteRequest{Resource: recordLabel})
require.NoError(t, err)
recordLabel = writeRsp.Resource
originalRecordLabelId := clone(recordLabel.Id)

artist, err := demo.GenerateV2Artist()
require.NoError(t, err)
artist, err = server.Backend.WriteCAS(ctx, artist)
writeRsp, err = client.Write(ctx, &pbresource.WriteRequest{Resource: artist})
require.NoError(t, err)
artist = writeRsp.Resource
originalArtistId := clone(artist.Id)

// Pick the resource to be deleted based on type's scope
// Pick the resource to be deleted based on type's scope and mod tenancy
// based on the tenancy test case.
deleteId := modFn(artist.Id, recordLabel.Id)
deleteReq := tc.deleteReqFn(recordLabel)
if proto.Equal(deleteId.Type, demo.TypeV2Artist) {
Expand All @@ -154,19 +156,25 @@ func TestDelete_Success(t *testing.T) {
require.NoError(t, err)

// Verify deleted
_, err = server.Backend.Read(ctx, storage.StrongConsistency, deleteId)
_, err = client.Read(ctx, &pbresource.ReadRequest{Id: deleteId})
require.Error(t, err)
require.ErrorIs(t, err, storage.ErrNotFound)
require.Equal(t, codes.NotFound.String(), status.Code(err).String())

// Derive tombstone name from resource that was deleted.
tname := tombstoneName(originalRecordLabelId)
if proto.Equal(deleteId.Type, demo.TypeV2Artist) {
tname = tombstoneName(originalArtistId)
}

// Verify tombstone created
_, err = client.Read(ctx, &pbresource.ReadRequest{
Id: &pbresource.ID{
Name: tombstoneName(deleteReq.Id),
Name: tname,
Type: resource.TypeV1Tombstone,
Tenancy: deleteReq.Id.Tenancy,
},
})
require.NoError(t, err, "expected tombstome to be found")
require.NoError(t, err, "expected tombstone to be found")
})
}
})
Expand Down
4 changes: 0 additions & 4 deletions agent/grpc-external/services/resource/list_by_owner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ func TestListByOwner_InputValidation(t *testing.T) {
artistId.Type = nil
return artistId
},
"no tenancy": func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Tenancy = nil
return artistId
},
"no name": func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Name = ""
return artistId
Expand Down
4 changes: 0 additions & 4 deletions agent/grpc-external/services/resource/read_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@ func TestRead_InputValidation(t *testing.T) {
artistId.Type = nil
return artistId
},
"no tenancy": func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Tenancy = nil
return artistId
},
"no name": func(artistId, _ *pbresource.ID) *pbresource.ID {
artistId.Name = ""
return artistId
Expand Down
14 changes: 12 additions & 2 deletions agent/grpc-external/services/resource/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,15 +133,25 @@ func validateId(id *pbresource.ID, errorPrefix string) error {
switch {
case id.Type == nil:
field = "type"
case id.Tenancy == nil:
field = "tenancy"
case id.Name == "":
field = "name"
}

if field != "" {
return status.Errorf(codes.InvalidArgument, "%s.%s is required", errorPrefix, field)
}

// Better UX: Allow callers to pass in nil tenancy. Defaulting and inheritance of tenancy
// from the request token will take place further down in the call flow.
if id.Tenancy == nil {
id.Tenancy = &pbresource.Tenancy{
Partition: "",
Namespace: "",
// TODO(spatel): Remove when peerTenancy introduced.
PeerName: "local",
}
}

resource.Normalize(id.Tenancy)

return nil
Expand Down
10 changes: 10 additions & 0 deletions agent/grpc-external/services/resource/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,11 @@ func tenancyCases() map[string]func(artistId, recordlabelId *pbresource.ID) *pbr
id.Tenancy.Namespace = ""
return id
},
"namespaced resource inherits tokens partition and namespace when tenacy nil": func(artistId, _ *pbresource.ID) *pbresource.ID {
id := clone(artistId)
id.Tenancy = nil
return id
},
"partitioned resource provides nonempty partition": func(_, recordLabelId *pbresource.ID) *pbresource.ID {
return recordLabelId
},
Expand All @@ -259,6 +264,11 @@ func tenancyCases() map[string]func(artistId, recordlabelId *pbresource.ID) *pbr
id.Tenancy.Partition = ""
return id
},
"partitioned resource inherits tokens partition when tenancy nil": func(_, recordLabelId *pbresource.ID) *pbresource.ID {
id := clone(recordLabelId)
id.Tenancy = nil
return id
},
}
return tenancyCases
}
13 changes: 11 additions & 2 deletions agent/grpc-external/services/resource/write_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,6 @@ func (s *Server) validateWriteStatusRequest(req *pbresource.WriteStatusRequest)
field = "id"
case req.Id.Type == nil:
field = "id.type"
case req.Id.Tenancy == nil:
field = "id.tenancy"
case req.Id.Name == "":
field = "id.name"
case req.Id.Uid == "":
Expand Down Expand Up @@ -169,6 +167,17 @@ func (s *Server) validateWriteStatusRequest(req *pbresource.WriteStatusRequest)
return nil, status.Error(codes.InvalidArgument, "status.observed_generation is not valid")
}

// Better UX: Allow callers to pass in nil tenancy. Defaulting and inheritance of tenancy
// from the request token will take place further down in the call flow.
if req.Id.Tenancy == nil {
req.Id.Tenancy = &pbresource.Tenancy{
Partition: "",
Namespace: "",
// TODO(spatel): Remove when peerTenancy introduced.
PeerName: "local",
}
}

// Lowercase
resource.Normalize(req.Id.Tenancy)

Expand Down
8 changes: 4 additions & 4 deletions agent/grpc-external/services/resource/write_status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,6 @@ func TestWriteStatus_InputValidation(t *testing.T) {
typ: demo.TypeV2Artist,
modFn: func(req *pbresource.WriteStatusRequest) { req.Id.Type = nil },
},
"no tenancy": {
typ: demo.TypeV2Artist,
modFn: func(req *pbresource.WriteStatusRequest) { req.Id.Tenancy = nil },
},
"no name": {
typ: demo.TypeV2Artist,
modFn: func(req *pbresource.WriteStatusRequest) { req.Id.Name = "" },
Expand Down Expand Up @@ -236,6 +232,10 @@ func TestWriteStatus_Tenancy_Defaults(t *testing.T) {
req.Id.Tenancy.Namespace = ""
},
},
"namespaced resource inherits tokens partition and namespace when tenancy nil": {
scope: resource.ScopeNamespace,
modFn: func(req *pbresource.WriteStatusRequest) { req.Id.Tenancy = nil },
},
"partitioned resource provides nonempty partition": {
scope: resource.ScopePartition,
modFn: func(req *pbresource.WriteStatusRequest) {},
Expand Down
20 changes: 15 additions & 5 deletions agent/grpc-external/services/resource/write_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,6 @@ func TestWrite_InputValidation(t *testing.T) {
artist.Id.Type = nil
return artist
},
"no tenancy": func(artist, _ *pbresource.Resource) *pbresource.Resource {
artist.Id.Tenancy = nil
return artist
},
"no name": func(artist, _ *pbresource.Resource) *pbresource.Resource {
artist.Id.Name = ""
return artist
Expand Down Expand Up @@ -106,7 +102,7 @@ func TestWrite_OwnerValidation(t *testing.T) {
},
"no owner tenancy": {
modReqFn: func(req *pbresource.WriteRequest) { req.Resource.Owner.Tenancy = nil },
errorContains: "resource.owner.tenancy",
errorContains: "resource.owner",
},
"no owner name": {
modReqFn: func(req *pbresource.WriteRequest) { req.Resource.Owner.Name = "" },
Expand Down Expand Up @@ -253,6 +249,13 @@ func TestWrite_Create_Success(t *testing.T) {
},
expectedTenancy: resource.DefaultNamespacedTenancy(),
},
"namespaced resource inherits tokens partition and namespace when tenancy nil": {
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource {
artist.Id.Tenancy = nil
return artist
},
expectedTenancy: resource.DefaultNamespacedTenancy(),
},
"partitioned resource provides nonempty partition": {
modFn: func(_, recordLabel *pbresource.Resource) *pbresource.Resource {
return recordLabel
Expand All @@ -273,6 +276,13 @@ func TestWrite_Create_Success(t *testing.T) {
},
expectedTenancy: resource.DefaultPartitionedTenancy(),
},
"partitioned resource inherits tokens partition when tenancy nil": {
modFn: func(_, recordLabel *pbresource.Resource) *pbresource.Resource {
recordLabel.Id.Tenancy = nil
return recordLabel
},
expectedTenancy: resource.DefaultPartitionedTenancy(),
},
// TODO(spatel): Add cluster scope tests when we have an actual cluster scoped resource (e.g. partition)
}
for desc, tc := range testCases {
Expand Down