Skip to content

Commit

Permalink
Merge branch 'main' into loshz/server-metadata-docs
Browse files Browse the repository at this point in the history
  • Loading branch information
loshz authored Aug 15, 2023
2 parents b4843d7 + 217107f commit d650577
Show file tree
Hide file tree
Showing 32 changed files with 3,285 additions and 119 deletions.
64 changes: 48 additions & 16 deletions agent/grpc-external/services/resource/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,37 @@ import (
"google.golang.org/grpc/status"

"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/internal/resource"
"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 := validateListRequest(req); err != nil {
return nil, err
}

// check type
reg, err := s.resolveType(req.Type)
reg, err := s.validateListRequest(req)
if err != nil {
return nil, err
}

// TODO(spatel): Refactor _ and entMeta in NET-4915
authz, authzContext, err := s.getAuthorizer(tokenFromContext(ctx), acl.DefaultEnterpriseMeta())
// v1 ACL subsystem is "wildcard" aware so just pass on through.
entMeta := v2TenancyToV1EntMeta(req.Tenancy)
token := tokenFromContext(ctx)
authz, authzContext, err := s.getAuthorizer(token, entMeta)
if err != nil {
return nil, err
}

// check acls
err = reg.ACLs.List(authz, req.Tenancy)
// Check ACLs.
err = reg.ACLs.List(authz, authzContext)
switch {
case acl.IsErrPermissionDenied(err):
return nil, status.Error(codes.PermissionDenied, err.Error())
case err != nil:
return nil, status.Errorf(codes.Internal, "failed list acl: %v", err)
}

// Ensure we're defaulting correctly when request tenancy units are empty.
v1EntMetaToV2Tenancy(reg, entMeta, req.Tenancy)

resources, err := s.Backend.List(
ctx,
readConsistencyFrom(ctx),
Expand All @@ -53,12 +54,21 @@ func (s *Server) List(ctx context.Context, req *pbresource.ListRequest) (*pbreso

result := make([]*pbresource.Resource, 0)
for _, resource := range resources {
// filter out non-matching GroupVersion
// Filter out non-matching GroupVersion.
if resource.Id.Type.GroupVersion != req.Type.GroupVersion {
continue
}

// filter out items that don't pass read ACLs
// Need to rebuild authorizer per resource since wildcard inputs may
// result in different tenancies. Consider caching per tenancy if this
// is deemed expensive.
entMeta = v2TenancyToV1EntMeta(resource.Id.Tenancy)
authz, authzContext, err = s.getAuthorizer(token, entMeta)
if err != nil {
return nil, err
}

// Filter out items that don't pass read ACLs.
err = reg.ACLs.Read(authz, authzContext, resource.Id)
switch {
case acl.IsErrPermissionDenied(err):
Expand All @@ -71,15 +81,37 @@ func (s *Server) List(ctx context.Context, req *pbresource.ListRequest) (*pbreso
return &pbresource.ListResponse{Resources: result}, nil
}

func validateListRequest(req *pbresource.ListRequest) error {
func (s *Server) validateListRequest(req *pbresource.ListRequest) (*resource.Registration, error) {
var field string
switch {
case req.Type == nil:
field = "type"
case req.Tenancy == nil:
field = "tenancy"
default:
return nil
}
return status.Errorf(codes.InvalidArgument, "%s is required", field)

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

// Check type exists.
reg, err := s.resolveType(req.Type)
if err != nil {
return nil, err
}

// Lowercase
resource.Normalize(req.Tenancy)

// Error when partition scoped and namespace not empty.
if reg.Scope == resource.ScopePartition && req.Tenancy.Namespace != "" {
return nil, status.Errorf(
codes.InvalidArgument,
"partition scoped type %s cannot have a namespace. got: %s",
resource.ToGVK(req.Type),
req.Tenancy.Namespace,
)
}

return reg, nil
}
127 changes: 123 additions & 4 deletions agent/grpc-external/services/resource/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/grpc-external/testutils"
"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 @@ -31,12 +32,16 @@ func TestList_InputValidation(t *testing.T) {
testCases := map[string]func(*pbresource.ListRequest){
"no type": func(req *pbresource.ListRequest) { req.Type = nil },
"no tenancy": func(req *pbresource.ListRequest) { req.Tenancy = nil },
"partitioned resource provides non-empty namespace": func(req *pbresource.ListRequest) {
req.Type = demo.TypeV1RecordLabel
req.Tenancy.Namespace = "bad"
},
}
for desc, modFn := range testCases {
t.Run(desc, func(t *testing.T) {
req := &pbresource.ListRequest{
Type: demo.TypeV2Album,
Tenancy: demo.TenancyDefault,
Tenancy: resource.DefaultNamespacedTenancy(),
}
modFn(req)

Expand All @@ -53,7 +58,7 @@ func TestList_TypeNotFound(t *testing.T) {

_, err := client.List(context.Background(), &pbresource.ListRequest{
Type: demo.TypeV2Artist,
Tenancy: demo.TenancyDefault,
Tenancy: resource.DefaultNamespacedTenancy(),
NamePrefix: "",
})
require.Error(t, err)
Expand All @@ -70,7 +75,7 @@ func TestList_Empty(t *testing.T) {

rsp, err := client.List(tc.ctx, &pbresource.ListRequest{
Type: demo.TypeV1Artist,
Tenancy: demo.TenancyDefault,
Tenancy: resource.DefaultNamespacedTenancy(),
NamePrefix: "",
})
require.NoError(t, err)
Expand Down Expand Up @@ -102,7 +107,7 @@ func TestList_Many(t *testing.T) {

rsp, err := client.List(tc.ctx, &pbresource.ListRequest{
Type: demo.TypeV2Artist,
Tenancy: demo.TenancyDefault,
Tenancy: resource.DefaultNamespacedTenancy(),
NamePrefix: "",
})
require.NoError(t, err)
Expand All @@ -111,6 +116,120 @@ func TestList_Many(t *testing.T) {
}
}

func TestList_Tenancy_Defaults_And_Normalization(t *testing.T) {
// Test units of tenancy get defaulted correctly when empty.
ctx := context.Background()
testCases := map[string]struct {
typ *pbresource.Type
tenancy *pbresource.Tenancy
}{
"namespaced type with empty partition": {
typ: demo.TypeV2Artist,
tenancy: &pbresource.Tenancy{
Partition: "",
Namespace: resource.DefaultNamespaceName,
PeerName: "local",
},
},
"namespaced type with empty namespace": {
typ: demo.TypeV2Artist,
tenancy: &pbresource.Tenancy{
Partition: resource.DefaultPartitionName,
Namespace: "",
PeerName: "local",
},
},
"namespaced type with empty partition and namespace": {
typ: demo.TypeV2Artist,
tenancy: &pbresource.Tenancy{
Partition: "",
Namespace: "",
PeerName: "local",
},
},
"namespaced type with uppercase partition and namespace": {
typ: demo.TypeV2Artist,
tenancy: &pbresource.Tenancy{
Partition: "DEFAULT",
Namespace: "DEFAULT",
PeerName: "local",
},
},
"namespaced type with wildcard partition and empty namespace": {
typ: demo.TypeV2Artist,
tenancy: &pbresource.Tenancy{
Partition: "*",
Namespace: "",
PeerName: "local",
},
},
"namespaced type with empty partition and wildcard namespace": {
typ: demo.TypeV2Artist,
tenancy: &pbresource.Tenancy{
Partition: "",
Namespace: "*",
PeerName: "local",
},
},
"partitioned type with empty partition": {
typ: demo.TypeV1RecordLabel,
tenancy: &pbresource.Tenancy{
Partition: "",
Namespace: "",
PeerName: "local",
},
},
"partitioned type with uppercase partition": {
typ: demo.TypeV1RecordLabel,
tenancy: &pbresource.Tenancy{
Partition: "DEFAULT",
Namespace: "",
PeerName: "local",
},
},
"partitioned type with wildcard partition": {
typ: demo.TypeV1RecordLabel,
tenancy: &pbresource.Tenancy{
Partition: "*",
PeerName: "local",
},
},
}
for desc, tc := range testCases {
t.Run(desc, func(t *testing.T) {
server := testServer(t)
demo.RegisterTypes(server.Registry)
client := testClient(t, server)

// Write partition scoped record label
recordLabel, err := demo.GenerateV1RecordLabel("LooneyTunes")
require.NoError(t, err)
recordLabelRsp, err := client.Write(ctx, &pbresource.WriteRequest{Resource: recordLabel})
require.NoError(t, err)

// Write namespace scoped artist
artist, err := demo.GenerateV2Artist()
require.NoError(t, err)
artistRsp, err := client.Write(ctx, &pbresource.WriteRequest{Resource: artist})
require.NoError(t, err)

// List and verify correct resource returned for empty tenancy units.
listRsp, err := client.List(ctx, &pbresource.ListRequest{
Type: tc.typ,
Tenancy: tc.tenancy,
})
require.NoError(t, err)
require.Len(t, listRsp.Resources, 1)
if tc.typ == demo.TypeV1RecordLabel {
prototest.AssertDeepEqual(t, recordLabelRsp.Resource, listRsp.Resources[0])
} else {
prototest.AssertDeepEqual(t, artistRsp.Resource, listRsp.Resources[0])
}
})

}
}

func TestList_GroupVersionMismatch(t *testing.T) {
for desc, tc := range listTestCases() {
t.Run(desc, func(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion agent/grpc-external/services/resource/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func (s *Server) WatchList(req *pbresource.WatchListRequest, stream pbresource.R
}

// check acls
err = reg.ACLs.List(authz, req.Tenancy)
err = reg.ACLs.List(authz, authzContext)
switch {
case acl.IsErrPermissionDenied(err):
return status.Error(codes.PermissionDenied, err.Error())
Expand Down
22 changes: 12 additions & 10 deletions agent/xds/clusters.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
envoy_upstreams_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3"
envoy_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"github.com/hashicorp/consul/agent/xds/config"
"github.com/hashicorp/consul/agent/xds/naming"

"github.com/hashicorp/go-hclog"
"google.golang.org/protobuf/encoding/protojson"
Expand Down Expand Up @@ -366,7 +368,7 @@ func makePassthroughClusters(cfgSnap *proxycfg.ConfigSnapshot) ([]proto.Message,
!meshConf.TransparentProxy.MeshDestinationsOnly {

clusters = append(clusters, &envoy_cluster_v3.Cluster{
Name: OriginalDestinationClusterName,
Name: naming.OriginalDestinationClusterName,
ClusterDiscoveryType: &envoy_cluster_v3.Cluster_Type{
Type: envoy_cluster_v3.Cluster_ORIGINAL_DST,
},
Expand Down Expand Up @@ -1041,7 +1043,7 @@ func (s *ResourceGenerator) configIngressUpstreamCluster(c *envoy_cluster_v3.Clu
if svc != nil {
override = svc.PassiveHealthCheck
}
outlierDetection := ToOutlierDetection(cfgSnap.IngressGateway.Defaults.PassiveHealthCheck, override, false)
outlierDetection := config.ToOutlierDetection(cfgSnap.IngressGateway.Defaults.PassiveHealthCheck, override, false)

c.OutlierDetection = outlierDetection
}
Expand All @@ -1050,7 +1052,7 @@ func (s *ResourceGenerator) makeAppCluster(cfgSnap *proxycfg.ConfigSnapshot, nam
var c *envoy_cluster_v3.Cluster
var err error

cfg, err := ParseProxyConfig(cfgSnap.Proxy.Config)
cfg, err := config.ParseProxyConfig(cfgSnap.Proxy.Config)
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
Expand Down Expand Up @@ -1144,7 +1146,7 @@ func (s *ResourceGenerator) makeUpstreamClusterForPeerService(

clusterName := generatePeeredClusterName(uid, tbs)

outlierDetection := ToOutlierDetection(upstreamConfig.PassiveHealthCheck, nil, true)
outlierDetection := config.ToOutlierDetection(upstreamConfig.PassiveHealthCheck, nil, true)
// We can't rely on health checks for services on cluster peers because they
// don't take into account service resolvers, splitters and routers. Setting
// MaxEjectionPercent too 100% gives outlier detection the power to eject the
Expand Down Expand Up @@ -1279,7 +1281,7 @@ func (s *ResourceGenerator) makeUpstreamClusterForPreparedQuery(upstream structs
CircuitBreakers: &envoy_cluster_v3.CircuitBreakers{
Thresholds: makeThresholdsIfNeeded(cfg.Limits),
},
OutlierDetection: ToOutlierDetection(cfg.PassiveHealthCheck, nil, true),
OutlierDetection: config.ToOutlierDetection(cfg.PassiveHealthCheck, nil, true),
}
if cfg.Protocol == "http2" || cfg.Protocol == "grpc" {
if err := s.setHttp2ProtocolOptions(c); err != nil {
Expand Down Expand Up @@ -1499,7 +1501,7 @@ func (s *ResourceGenerator) makeUpstreamClustersForDiscoveryChain(
CircuitBreakers: &envoy_cluster_v3.CircuitBreakers{
Thresholds: makeThresholdsIfNeeded(upstreamConfig.Limits),
},
OutlierDetection: ToOutlierDetection(upstreamConfig.PassiveHealthCheck, nil, true),
OutlierDetection: config.ToOutlierDetection(upstreamConfig.PassiveHealthCheck, nil, true),
}

var lb *structs.LoadBalancer
Expand Down Expand Up @@ -1676,7 +1678,7 @@ type clusterOpts struct {

// makeGatewayCluster creates an Envoy cluster for a mesh or terminating gateway
func (s *ResourceGenerator) makeGatewayCluster(snap *proxycfg.ConfigSnapshot, opts clusterOpts) *envoy_cluster_v3.Cluster {
cfg, err := ParseGatewayConfig(snap.Proxy.Config)
cfg, err := config.ParseGatewayConfig(snap.Proxy.Config)
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
Expand Down Expand Up @@ -1819,7 +1821,7 @@ func configureClusterWithHostnames(
// makeExternalIPCluster creates an Envoy cluster for routing to IP addresses outside of Consul
// This is used by terminating gateways for Destinations
func (s *ResourceGenerator) makeExternalIPCluster(snap *proxycfg.ConfigSnapshot, opts clusterOpts) *envoy_cluster_v3.Cluster {
cfg, err := ParseGatewayConfig(snap.Proxy.Config)
cfg, err := config.ParseGatewayConfig(snap.Proxy.Config)
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
Expand Down Expand Up @@ -1858,7 +1860,7 @@ func (s *ResourceGenerator) makeExternalIPCluster(snap *proxycfg.ConfigSnapshot,
// makeExternalHostnameCluster creates an Envoy cluster for hostname endpoints that will be resolved with DNS
// This is used by both terminating gateways for Destinations, and Mesh Gateways for peering control plane traffice
func (s *ResourceGenerator) makeExternalHostnameCluster(snap *proxycfg.ConfigSnapshot, opts clusterOpts) *envoy_cluster_v3.Cluster {
cfg, err := ParseGatewayConfig(snap.Proxy.Config)
cfg, err := config.ParseGatewayConfig(snap.Proxy.Config)
if err != nil {
// Don't hard fail on a config typo, just warn. The parse func returns
// default config if there is an error so it's safe to continue.
Expand Down Expand Up @@ -2044,7 +2046,7 @@ func (s *ResourceGenerator) getTargetClusterName(upstreamsSnapshot *proxycfg.Con

clusterName = generatePeeredClusterName(targetUID, tbs)
}
clusterName = CustomizeClusterName(clusterName, chain)
clusterName = naming.CustomizeClusterName(clusterName, chain)
if forMeshGateway {
clusterName = meshGatewayExportedClusterNamePrefix + clusterName
}
Expand Down
Loading

0 comments on commit d650577

Please sign in to comment.