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

feat(kuma-cp) TrafficPermission for ExternalServices #1957

Merged
merged 4 commits into from
May 11, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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
45 changes: 45 additions & 0 deletions pkg/core/permissions/matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,48 @@ func BuildTrafficPermissionMap(
}
return result, nil
}

func (m *TrafficPermissionsMatcher) MatchExternalServices(ctx context.Context, dataplane *mesh_core.DataplaneResource, externalServices *mesh_core.ExternalServiceResourceList) ([]*mesh_core.ExternalServiceResource, error) {
permissions := &mesh_core.TrafficPermissionResourceList{}
if err := m.ResourceManager.List(ctx, permissions, store.ListByMesh(dataplane.GetMeta().GetMesh())); err != nil {
return nil, errors.Wrap(err, "could not retrieve traffic permissions")
}

var matchedExternalServices []*mesh_core.ExternalServiceResource

externalServicePermissions := m.BuildExternalServicesPermissionsMap(externalServices, permissions.Items)
for _, externalService := range externalServices.Items {
permission := externalServicePermissions[externalService.GetMeta().GetName()]
if permission == nil {
continue
}
matched := false
for _, selector := range permission.Spec.Sources {
if dataplane.Spec.MatchTags(selector.Match) {
matched = true
}
}
if matched {
matchedExternalServices = append(matchedExternalServices, externalService)
}
}
return matchedExternalServices, nil
}

type ExternalServicePermissions map[string]*mesh_core.TrafficPermissionResource

func (m *TrafficPermissionsMatcher) BuildExternalServicesPermissionsMap(externalServices *mesh_core.ExternalServiceResourceList, trafficPermissions []*mesh_core.TrafficPermissionResource) ExternalServicePermissions {
policies := make([]policy.ConnectionPolicy, len(trafficPermissions))
for i, permission := range trafficPermissions {
policies[i] = permission
}

result := ExternalServicePermissions{}
for _, externalService := range externalServices.Items {
matchedPolicy := policy.SelectInboundConnectionPolicy(externalService.Spec.Tags, policies)
if matchedPolicy != nil {
result[externalService.GetMeta().GetName()] = matchedPolicy.(*mesh_core.TrafficPermissionResource)
}
}
return result
}
210 changes: 210 additions & 0 deletions pkg/core/permissions/matcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,214 @@ var _ = Describe("Match", func() {
},
}),
)

Context("MatchExternalServices", func() {
type testCase struct {
dataplane *core_mesh.DataplaneResource
policies []*core_mesh.TrafficPermissionResource
externalServices []*core_mesh.ExternalServiceResource
expected map[string]bool
}

DescribeTable("should find the policy",
func(given testCase) {
manager := core_manager.NewResourceManager(memory.NewStore())
matcher := permissions.TrafficPermissionsMatcher{ResourceManager: manager}

err := manager.Create(context.Background(), core_mesh.NewMeshResource(), store.CreateByKey(core_model.DefaultMesh, core_model.NoMesh))
Expect(err).ToNot(HaveOccurred())

for _, p := range given.policies {
err := manager.Create(context.Background(), p, store.CreateByKey(p.Meta.GetName(), "default"))
Expect(err).ToNot(HaveOccurred())
}

es := &core_mesh.ExternalServiceResourceList{
Items: given.externalServices,
}
matchedEs, err := matcher.MatchExternalServices(context.Background(), given.dataplane, es)
Expect(err).ToNot(HaveOccurred())

Expect(given.expected).To(HaveLen(len(matchedEs)))
for _, externalService := range matchedEs {
Expect(given.expected[externalService.GetMeta().GetName()]).To(BeTrue())
}
},
Entry("should match external services that matches traffic permission", testCase{
dataplane: &core_mesh.DataplaneResource{
Meta: &model.ResourceMeta{
Mesh: "default",
Name: "dp1",
},
Spec: &mesh_proto.Dataplane{
Networking: &mesh_proto.Dataplane_Networking{
Address: "192.168.0.1",
Inbound: []*mesh_proto.Dataplane_Networking_Inbound{
{
Port: 8080,
ServicePort: 8081,
Tags: map[string]string{
"kuma.io/service": "web",
},
},
},
Outbound: []*mesh_proto.Dataplane_Networking_Outbound{
{
Port: 8080,
Tags: map[string]string{
"kuma.io/service": "httpbin",
},
},
},
},
},
},
externalServices: []*core_mesh.ExternalServiceResource{
{
Meta: &model.ResourceMeta{
Mesh: "default",
Name: "httpbin",
},
Spec: &mesh_proto.ExternalService{
Tags: map[string]string{
"kuma.io/service": "httpbin",
},
Networking: &mesh_proto.ExternalService_Networking{
Address: "httpbin.org",
},
},
},
{ // this won't be matched since there is no traffic permission for it
Meta: &model.ResourceMeta{
Mesh: "default",
Name: "google",
},
Spec: &mesh_proto.ExternalService{
Tags: map[string]string{
"kuma.io/service": "google",
},
Networking: &mesh_proto.ExternalService_Networking{
Address: "google.com",
},
},
},
},
policies: []*core_mesh.TrafficPermissionResource{
{
Meta: &model.ResourceMeta{
Mesh: "default",
Name: "web-to-httpbin",
},
Spec: &mesh_proto.TrafficPermission{
Sources: []*mesh_proto.Selector{
{
Match: map[string]string{
"kuma.io/service": "web",
},
},
},
Destinations: []*mesh_proto.Selector{
{
Match: map[string]string{
"kuma.io/service": "httpbin",
},
},
},
},
},
},
expected: map[string]bool{
"httpbin": true,
},
}),
Entry("should match all external services because of the traffic permission that matches all", testCase{
dataplane: &core_mesh.DataplaneResource{
Meta: &model.ResourceMeta{
Mesh: "default",
Name: "dp1",
},
Spec: &mesh_proto.Dataplane{
Networking: &mesh_proto.Dataplane_Networking{
Address: "192.168.0.1",
Inbound: []*mesh_proto.Dataplane_Networking_Inbound{
{
Port: 8080,
ServicePort: 8081,
Tags: map[string]string{
"kuma.io/service": "web",
},
},
},
Outbound: []*mesh_proto.Dataplane_Networking_Outbound{
{
Port: 8080,
Tags: map[string]string{
"kuma.io/service": "httpbin",
},
},
},
},
},
},
externalServices: []*core_mesh.ExternalServiceResource{
{
Meta: &model.ResourceMeta{
Mesh: "default",
Name: "httpbin",
},
Spec: &mesh_proto.ExternalService{
Tags: map[string]string{
"kuma.io/service": "httpbin",
},
Networking: &mesh_proto.ExternalService_Networking{
Address: "httpbin.org",
},
},
},
{ // this won't be matched since there is no traffic permission for it
Meta: &model.ResourceMeta{
Mesh: "default",
Name: "google",
},
Spec: &mesh_proto.ExternalService{
Tags: map[string]string{
"kuma.io/service": "google",
},
Networking: &mesh_proto.ExternalService_Networking{
Address: "google.com",
},
},
},
},
policies: []*core_mesh.TrafficPermissionResource{
{
Meta: &model.ResourceMeta{
Mesh: "default",
Name: "all",
},
Spec: &mesh_proto.TrafficPermission{
Sources: []*mesh_proto.Selector{
{
Match: map[string]string{
"kuma.io/service": "*",
},
},
},
Destinations: []*mesh_proto.Selector{
{
Match: map[string]string{
"kuma.io/service": "*",
},
},
},
},
},
},
expected: map[string]bool{
"httpbin": true,
"google": true,
},
}),
)
})
})
44 changes: 24 additions & 20 deletions pkg/core/policy/connection_matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,26 +145,7 @@ func SelectInboundConnectionPolicies(dataplane *mesh_core.DataplaneResource, inb
sort.Stable(ConnectionPolicyByName(policies)) // sort to avoid flakiness
policiesMap := make(InboundConnectionPolicyMap)
for _, inbound := range inbounds {
var bestPolicy ConnectionPolicy
var bestRank mesh_proto.TagSelectorRank
sameRankCreatedLater := func(policy ConnectionPolicy, rank mesh_proto.TagSelectorRank) bool {
return rank.CompareTo(bestRank) == 0 && policy.GetMeta().GetCreationTime().After(bestPolicy.GetMeta().GetCreationTime())
}

for _, policy := range policies {
for _, selector := range policy.Destinations() {
tagSelector := mesh_proto.TagSelector(selector.Match)
if inbound.MatchTags(tagSelector) {
rank := tagSelector.Rank()
if rank.CompareTo(bestRank) > 0 || sameRankCreatedLater(policy, rank) {
bestRank = rank
bestPolicy = policy
}
}
}
}

if bestPolicy != nil {
if bestPolicy := SelectInboundConnectionPolicy(inbound.Tags, policies); bestPolicy != nil {
iface := dataplane.Spec.GetNetworking().ToInboundInterface(inbound)
policiesMap[iface] = bestPolicy
}
Expand All @@ -173,6 +154,29 @@ func SelectInboundConnectionPolicies(dataplane *mesh_core.DataplaneResource, inb
return policiesMap
}

// SelectInboundConnectionPolicy picks a single the most specific policy for given inbound tags.
func SelectInboundConnectionPolicy(inboundTags map[string]string, policies []ConnectionPolicy) ConnectionPolicy {
var bestPolicy ConnectionPolicy
var bestRank mesh_proto.TagSelectorRank
sameRankCreatedLater := func(policy ConnectionPolicy, rank mesh_proto.TagSelectorRank) bool {
return rank.CompareTo(bestRank) == 0 && policy.GetMeta().GetCreationTime().After(bestPolicy.GetMeta().GetCreationTime())
}

for _, policy := range policies {
for _, selector := range policy.Destinations() {
tagSelector := mesh_proto.TagSelector(selector.Match)
if tagSelector.Matches(inboundTags) {
rank := tagSelector.Rank()
if rank.CompareTo(bestRank) > 0 || sameRankCreatedLater(policy, rank) {
bestRank = rank
bestPolicy = policy
}
}
}
}
return bestPolicy
}

type ConnectionPolicyByName []ConnectionPolicy

func (a ConnectionPolicyByName) Len() int { return len(a) }
Expand Down
16 changes: 6 additions & 10 deletions pkg/xds/cache/cla/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (

"github.com/golang/protobuf/proto"

"github.com/kumahq/kuma/pkg/core/datasource"

"github.com/kumahq/kuma/pkg/core/resources/model"
"github.com/kumahq/kuma/pkg/metrics"

Expand All @@ -33,14 +31,12 @@ var (
type Cache struct {
cache *once.Cache
rm manager.ReadOnlyResourceManager
dsl datasource.Loader
ipFunc lookup.LookupIPFunc
zone string
}

func NewCache(
rm manager.ReadOnlyResourceManager,
dsl datasource.Loader,
zone string, expirationTime time.Duration,
ipFunc lookup.LookupIPFunc,
metrics metrics.Metrics,
Expand All @@ -52,7 +48,6 @@ func NewCache(
return &Cache{
cache: c,
rm: rm,
dsl: dsl,
zone: zone,
ipFunc: ipFunc,
}, nil
Expand All @@ -69,11 +64,12 @@ func (c *Cache) GetCLA(ctx context.Context, meshName, meshHash, service string,
if err := c.rm.Get(ctx, mesh, core_store.GetByKey(meshName, model.NoMesh)); err != nil {
return nil, err
}
externalServices := &core_mesh.ExternalServiceResourceList{}
if err := c.rm.List(ctx, externalServices, core_store.ListByMesh(meshName)); err != nil {
return nil, err
}
endpointMap := topology.BuildEndpointMap(mesh, c.zone, dataplanes.Items, externalServices.Items, c.dsl)
// External Services can be nil since GetCLA is used only for EDS clusters
//
// This also solves the problem that if the ExternalService is blocked by TrafficPermission
// OutboundProxyGenerate treats this as EDS cluster and tries to get endpoints via GetCLA
// Since GetCLA is consistent for a mesh, it would return an endpoint with address which is not valid for EDS.
endpointMap := topology.BuildEdsEndpointMap(mesh, c.zone, dataplanes.Items)
return envoy_endpoints.CreateClusterLoadAssignment(service, endpointMap[service], apiVersion)
}))
if err != nil {
Expand Down
Loading