Skip to content

Commit

Permalink
Merge pull request #644 from caseydavenport/nsselector
Browse files Browse the repository at this point in the history
Add NamespaceSelector to NetworkPolicy and GNP
  • Loading branch information
caseydavenport authored Nov 3, 2017
2 parents f21046f + 372a8eb commit 88a4db1
Show file tree
Hide file tree
Showing 10 changed files with 397 additions and 109 deletions.
19 changes: 18 additions & 1 deletion lib/apis/v2/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ type EntityRule struct {
// Nets is an optional field that restricts the rule to only apply to traffic that
// originates from (or terminates at) IP addresses in any of the given subnets.
Nets []string `json:"nets,omitempty" validate:"omitempty,dive,cidr"`

// Selector is an optional field that contains a selector expression (see Policy for
// sample syntax). Only traffic that originates from (terminates at) endpoints matching
// the selector will be matched.
Expand All @@ -96,18 +97,34 @@ type EntityRule struct {
// The effect is that the latter will accept packets from non-Calico sources whereas the
// former is limited to packets from Calico-controlled endpoints.
Selector string `json:"selector,omitempty" validate:"omitempty,selector"`

// NamespaceSelector is an optional field that contains a selector expression. Only traffic
// that originates from (or terminates at) endpoints within the selected namespaces will be
// matched. When both NamespaceSelector and Selector are defined on the same rule, then only
// workload endpoints that are matched by both selectors will be selected by the rule.
//
// For NetworkPolicy, an empty NamespaceSelector implies that the Selector is limited to selecting
// only workload endpoints in the same namespace as the NetworkPolicy.
//
// For GlobalNetworkPolicy, an empty NamespaceSelector implies the Selector applies to workload
// endpoints across all namespaces.
NamespaceSelector string `json:"namespaceSelector,omitempty" validate:"omitempty,selector"`

// Ports is an optional field that restricts the rule to only apply to traffic that has a
// source (destination) port that matches one of these ranges/values. This value is a
// list of integers or strings that represent ranges of ports.
//
// Since only some protocols have ports, if any ports are specified it requires the
// Protocol match in the Rule to be set to "tcp" or "udp".
Ports []numorstring.Port `json:"ports,omitempty" validate:"omitempty,dive"`
// NotTag is the negated version of the Tag field.

// NotNets is the negated version of the Nets field.
NotNets []string `json:"notNets,omitempty" validate:"omitempty,dive,cidr"`

// NotSelector is the negated version of the Selector field. See Selector field for
// subtleties with negated selectors.
NotSelector string `json:"notSelector,omitempty" validate:"omitempty,selector"`

// NotPorts is the negated version of the Ports field.
// Since only some protocols have ports, if any ports are specified it requires the
// Protocol match in the Rule to be set to "tcp" or "udp".
Expand Down
45 changes: 22 additions & 23 deletions lib/backend/k8s/conversion/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,14 +319,10 @@ func (c Converter) K8sNetworkPolicyToCalico(np *extensions.NetworkPolicy) (*mode
// k8sSelectorToCalico takes a namespaced k8s label selector and returns the Calico
// equivalent.
func (c Converter) k8sSelectorToCalico(s *metav1.LabelSelector, selectorType selectorType) string {
// All selectors should be limited in scope to Kubernetes pods.
selectors := []string{fmt.Sprintf("%s == 'k8s'", apiv2.LabelOrchestrator)}

// If this is a namespace selector, the labels need to be prefixed with the profile namespace
// prefix.
var prefix string
if selectorType == SelectorNamespace {
prefix = NamespaceLabelPrefix
// Only prefix pod selectors - this won't work for namespace selectors.
selectors := []string{}
if selectorType == SelectorPod {
selectors = append(selectors, fmt.Sprintf("%s == 'k8s'", apiv2.LabelOrchestrator))
}

// matchLabels is a map key => value, it means match if (label[key] ==
Expand All @@ -338,7 +334,7 @@ func (c Converter) k8sSelectorToCalico(s *metav1.LabelSelector, selectorType sel
sort.Strings(keys)
for _, k := range keys {
v := s.MatchLabels[k]
selectors = append(selectors, fmt.Sprintf("%s%s == '%s'", prefix, k, v))
selectors = append(selectors, fmt.Sprintf("%s == '%s'", k, v))
}

// matchExpressions is a list of in/notin/exists/doesnotexist tests.
Expand All @@ -348,13 +344,13 @@ func (c Converter) k8sSelectorToCalico(s *metav1.LabelSelector, selectorType sel
// Each selector is formatted differently based on the operator.
switch e.Operator {
case metav1.LabelSelectorOpIn:
selectors = append(selectors, fmt.Sprintf("%s%s in { '%s' }", prefix, e.Key, valueList))
selectors = append(selectors, fmt.Sprintf("%s in { '%s' }", e.Key, valueList))
case metav1.LabelSelectorOpNotIn:
selectors = append(selectors, fmt.Sprintf("%s%s not in { '%s' }", prefix, e.Key, valueList))
selectors = append(selectors, fmt.Sprintf("%s not in { '%s' }", e.Key, valueList))
case metav1.LabelSelectorOpExists:
selectors = append(selectors, fmt.Sprintf("has(%s%s)", prefix, e.Key))
selectors = append(selectors, fmt.Sprintf("has(%s)", e.Key))
case metav1.LabelSelectorOpDoesNotExist:
selectors = append(selectors, fmt.Sprintf("! has(%s%s)", prefix, e.Key))
selectors = append(selectors, fmt.Sprintf("! has(%s%s)", e.Key))
}
}

Expand Down Expand Up @@ -410,16 +406,17 @@ func (c Converter) k8sRuleToCalico(rPeers []extensions.NetworkPolicyPeer, rPorts
for _, port := range ports {
for _, peer := range peers {
protocol, calicoPorts := c.k8sPortToCalicoFields(port)
selector, nets, notNets := c.k8sPeerToCalicoFields(peer, ns)
selector, nsSelector, nets, notNets := c.k8sPeerToCalicoFields(peer, ns)
if ingress {
// Build inbound rule and append to list.
rules = append(rules, apiv2.Rule{
Action: "allow",
Protocol: protocol,
Source: apiv2.EntityRule{
Selector: selector,
Nets: nets,
NotNets: notNets,
Selector: selector,
NamespaceSelector: nsSelector,
Nets: nets,
NotNets: notNets,
},
Destination: apiv2.EntityRule{
Ports: calicoPorts,
Expand All @@ -431,10 +428,11 @@ func (c Converter) k8sRuleToCalico(rPeers []extensions.NetworkPolicyPeer, rPorts
Action: "allow",
Protocol: protocol,
Destination: apiv2.EntityRule{
Ports: calicoPorts,
Selector: selector,
Nets: nets,
NotNets: notNets,
Ports: calicoPorts,
Selector: selector,
NamespaceSelector: nsSelector,
Nets: nets,
NotNets: notNets,
},
})
}
Expand Down Expand Up @@ -462,7 +460,7 @@ func (c Converter) k8sProtocolToCalico(protocol *kapiv1.Protocol) *numorstring.P
return nil
}

func (c Converter) k8sPeerToCalicoFields(peer *extensions.NetworkPolicyPeer, ns string) (selector string, nets []string, notNets []string) {
func (c Converter) k8sPeerToCalicoFields(peer *extensions.NetworkPolicyPeer, ns string) (selector, nsSelector string, nets []string, notNets []string) {
// If no peer, return zero values for all fields (selector, nets and !nets).
if peer == nil {
return
Expand All @@ -475,7 +473,8 @@ func (c Converter) k8sPeerToCalicoFields(peer *extensions.NetworkPolicyPeer, ns
return
}
if peer.NamespaceSelector != nil {
selector = c.k8sSelectorToCalico(peer.NamespaceSelector, SelectorNamespace)
nsSelector = c.k8sSelectorToCalico(peer.NamespaceSelector, SelectorNamespace)
selector = fmt.Sprintf("%s == 'k8s'", apiv2.LabelOrchestrator)
return
}
if peer.IPBlock != nil {
Expand Down
50 changes: 50 additions & 0 deletions lib/backend/k8s/conversion/conversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,56 @@ var _ = Describe("Test NetworkPolicy conversion", func() {
Expect(pol.Value.(*apiv2.NetworkPolicy).Spec.Types[0]).To(Equal(apiv2.PolicyTypeIngress))
})

It("should parse a NetworkPolicy with a namespaceSelector", func() {
np := extensions.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "test.policy",
Namespace: "default",
},
Spec: extensions.NetworkPolicySpec{
PodSelector: metav1.LabelSelector{
MatchLabels: map[string]string{"label": "value"},
},
Ingress: []extensions.NetworkPolicyIngressRule{
extensions.NetworkPolicyIngressRule{
From: []extensions.NetworkPolicyPeer{
extensions.NetworkPolicyPeer{
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"namespaceRole": "dev",
"namespaceFoo": "bar",
},
},
},
},
},
},
PolicyTypes: []extensions.PolicyType{extensions.PolicyTypeIngress},
},
}

// Parse the policy.
pol, err := c.K8sNetworkPolicyToCalico(&np)
Expect(err).NotTo(HaveOccurred())

// Assert key fields are correct.
Expect(pol.Key.(model.ResourceKey).Name).To(Equal("knp.default.test.policy"))

// Assert value fields are correct.
Expect(int(*pol.Value.(*apiv2.NetworkPolicy).Spec.Order)).To(Equal(1000))
Expect(pol.Value.(*apiv2.NetworkPolicy).Spec.Selector).To(Equal("projectcalico.org/orchestrator == 'k8s' && label == 'value'"))
Expect(len(pol.Value.(*apiv2.NetworkPolicy).Spec.IngressRules)).To(Equal(1))
Expect(pol.Value.(*apiv2.NetworkPolicy).Spec.IngressRules[0].Source.Selector).To(Equal("projectcalico.org/orchestrator == 'k8s'"))
Expect(pol.Value.(*apiv2.NetworkPolicy).Spec.IngressRules[0].Source.NamespaceSelector).To(Equal("namespaceFoo == 'bar' && namespaceRole == 'dev'"))

// There should be no EgressRules
Expect(len(pol.Value.(*apiv2.NetworkPolicy).Spec.EgressRules)).To(Equal(0))

// Check that Types field exists and has only 'ingress'
Expect(len(pol.Value.(*apiv2.NetworkPolicy).Spec.Types)).To(Equal(1))
Expect(pol.Value.(*apiv2.NetworkPolicy).Spec.Types[0]).To(Equal(apiv2.PolicyTypeIngress))
})

It("should parse a NetworkPolicy with an empty namespaceSelector", func() {
np := extensions.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{
Expand Down
97 changes: 64 additions & 33 deletions lib/backend/syncersv1/updateprocessors/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/projectcalico/libcalico-go/lib/backend/k8s/conversion"
"github.com/projectcalico/libcalico-go/lib/backend/model"
cnet "github.com/projectcalico/libcalico-go/lib/net"
"github.com/projectcalico/libcalico-go/lib/selector/parser"
)

func RulesAPIV2ToBackend(ars []apiv2.Rule, ns string) []model.Rule {
Expand Down Expand Up @@ -51,45 +52,61 @@ func RuleAPIV2ToBackend(ar apiv2.Rule, ns string) model.Rule {
notICMPType = ar.NotICMP.Type
}

// If we have any selector specified, then we may need to add the namespace selector.
// We do this if this policy is namespaced AND if the Selector does not have any other
// k8s namespace (profile label) selector in it.
// TODO this is TEMPORARY CODE: We currently do a simple regex to search for pcns. in the
// selector to see if we are performing k8s namespace queries.
nsSelector := fmt.Sprintf("%s == '%s'", apiv2.LabelNamespace, ns)
if ns != "" && (ar.Source.Selector != "" || ar.Source.NotSelector != "") {
// Determine which namespaces are impacted by this rule.
var sourceNSSelector string
if ar.Source.NamespaceSelector != "" {
// A namespace selector was given - the rule applies to all namespaces
// which match this selector.
sourceNSSelector = parseNamespaceSelector(ar.Source.NamespaceSelector)
} else if ns != "" {
// No namespace selector was given and this is a namespaced network policy,
// so the rule applies only to its own namespace.
sourceNSSelector = fmt.Sprintf("%s == '%s'", apiv2.LabelNamespace, ns)
}

var destNSSelector string
if ar.Destination.NamespaceSelector != "" {
// A namespace selector was given - the rule applies to all namespaces
// which match this selector.
destNSSelector = parseNamespaceSelector(ar.Destination.NamespaceSelector)
} else if ns != "" {
// No namespace selector was given and this is a namespaced network policy,
// so the rule applies only to its own namespace.
destNSSelector = fmt.Sprintf("%s == '%s'", apiv2.LabelNamespace, ns)
}

var srcSelector, dstSelector string
if sourceNSSelector != "" {
// We need to namespace the rule's selector when converting to a v1 object.
logCxt := log.WithFields(log.Fields{
"Namespace": ns,
"Selector": ar.Source.Selector,
"NotSelector": ar.Source.NotSelector,
"Namespace": ns,
"Selector": ar.Source.Selector,
"NamespaceSelector": ar.Source.NamespaceSelector,
"NotSelector": ar.Source.NotSelector,
})
logCxt.Debug("Maybe update source Selector to include namespace")
if !strings.Contains(ar.Source.Selector, conversion.NamespaceLabelPrefix) {
logCxt.Debug("Updating source selector")
if ar.Source.Selector == "" {
ar.Source.Selector = nsSelector
} else {
ar.Source.Selector = fmt.Sprintf("(%s) && %s", ar.Source.Selector, nsSelector)
}
logCxt.Debug("Update source Selector to include namespace")
if ar.Source.Selector != "" {
srcSelector = fmt.Sprintf("(%s) && (%s)", sourceNSSelector, ar.Source.Selector)
} else {
srcSelector = sourceNSSelector
}
}
if ns != "" && (ar.Destination.Selector != "" || ar.Destination.NotSelector != "") {

if destNSSelector != "" {
// We need to namespace the rule's selector when converting to a v1 object.
logCxt := log.WithFields(log.Fields{
"Namespace": ns,
"Selector": ar.Destination.Selector,
"NotSelector": ar.Destination.NotSelector,
"Namespace": ns,
"Selector": ar.Destination.Selector,
"NamespaceSelector": ar.Destination.NamespaceSelector,
"NotSelector": ar.Destination.NotSelector,
})
logCxt.Debug("Maybe update Destination Selector to include namespace")
if !strings.Contains(ar.Destination.Selector, conversion.NamespaceLabelPrefix) {
logCxt.Debug("Updating Destination selector")
if ar.Destination.Selector == "" {
ar.Destination.Selector = nsSelector
} else {
ar.Destination.Selector = fmt.Sprintf("(%s) && %s", ar.Destination.Selector, nsSelector)
}
logCxt.Debug("Update Destination Selector to include namespace")
if ar.Destination.Selector != "" {
dstSelector = fmt.Sprintf("(%s) && (%s)", destNSSelector, ar.Destination.Selector)
} else {
dstSelector = destNSSelector
}
}

return model.Rule{
Action: ruleActionAPIV2ToBackend(ar.Action),
IPVersion: ar.IPVersion,
Expand All @@ -101,10 +118,10 @@ func RuleAPIV2ToBackend(ar apiv2.Rule, ns string) model.Rule {
NotICMPType: notICMPType,

SrcNets: convertStringsToNets(ar.Source.Nets),
SrcSelector: ar.Source.Selector,
SrcSelector: srcSelector,
SrcPorts: ar.Source.Ports,
DstNets: normalizeIPNets(ar.Destination.Nets),
DstSelector: ar.Destination.Selector,
DstSelector: dstSelector,
DstPorts: ar.Destination.Ports,

NotSrcNets: convertStringsToNets(ar.Source.NotNets),
Expand All @@ -116,6 +133,20 @@ func RuleAPIV2ToBackend(ar apiv2.Rule, ns string) model.Rule {
}
}

// parseNamespaceSelector takes a v2 namespace selector and returns the appropriate v1 representation
// by prefixing the keys with the `pcns.` prefix. For example, `k == 'v'` becomes `pcns.k == 'v'`.
func parseNamespaceSelector(s string) string {
parsedSelector, err := parser.Parse(s)
if err != nil {
log.WithError(err).Errorf("Failed to parse namespace selector: %s", s)
return ""
}
parsedSelector.AcceptVisitor(parser.PrefixVisitor{Prefix: conversion.NamespaceLabelPrefix})
updated := parsedSelector.String()
log.WithFields(log.Fields{"original": s, "updated": updated}).Debug("Updated namespace selector")
return updated
}

// normalizeIPNet converts an IPNet to a network by ensuring the IP address is correctly masked.
func normalizeIPNet(n string) *cnet.IPNet {
if n == "" {
Expand Down
Loading

0 comments on commit 88a4db1

Please sign in to comment.