Beispiel #1
0
 def test_eq(self):
     default_namespace = K8sNamespace('default')
     pod_a = Pod('A', default_namespace)
     ip1 = IpBlock("1.2.3.0/24")
     ip2 = IpBlock("1.2.3.0/32")
     ip3 = ip1 - ip2
     set1 = PeerSet({pod_a, ip1})
     set2 = PeerSet({pod_a, ip2, ip3})
     self.assertTrue(set1 == set2)
 def get_all_global_peers(self):
     """
     Return all global peers known in the system
     :return PeerSet: The required set of peers
     """
     res = PeerSet()
     for peer in self.peer_set:
         if peer.is_global_peer():
             res.add(peer)
     return res
Beispiel #3
0
 def test_subtract(self):
     self.assertTrue(True)
     default_namespace = K8sNamespace('default')
     pod_a = Pod('A', default_namespace)
     ip1 = IpBlock("1.2.3.0/24")
     ip2 = IpBlock("1.2.3.0/32")
     ip3 = ip1 - ip2
     set1 = PeerSet({pod_a, ip1})
     set2 = PeerSet({pod_a, ip2})
     set3 = PeerSet({ip3})
     self.assertTrue(set1-set2 == set3)
Beispiel #4
0
 def get_pods_with_service_account_name(self, sa_name, namespace_str):
     """
     Return all pods that are with a service account name in a given namespace
     :param sa_name: string  the service account name
     :param namespace_str:  string  the namespace str
     :rtype PeerSet
     """
     res = PeerSet()
     for peer in self.peer_set:
         if isinstance(peer, Pod) and peer.service_account_name == sa_name and peer.namespace.name == namespace_str:
             res.add(peer)
     return res
 def get_namespace_pods(self, namespace):
     """
     Return all pods that are in a given namespace
     :param K8sNamespace namespace: The target namespace
     :return PeerSet: All pods in the namespace
     """
     if namespace is None:
         return self.get_all_peers_group()
     res = PeerSet()
     for peer in self.peer_set:
         if peer.namespace == namespace:
             res.add(peer)
     return res
Beispiel #6
0
 def test_or(self):
     default_namespace = K8sNamespace('default')
     pod_a = Pod('A', default_namespace)
     pod_b = Pod('B', default_namespace)
     ip1 = IpBlock("1.2.3.0/24")
     ip2 = IpBlock("1.2.3.0/32")
     ip3 = ip1 - ip2
     set1 = PeerSet({pod_a, pod_b, ip1})
     set2 = PeerSet({pod_a, ip2})
     set3 = PeerSet({pod_b, ip3})
     self.assertTrue(set2 | set3 == set1)
     set2 |= set3
     self.assertTrue(set2 == set1)
 def get_namespace_pods_with_key(self, key, does_not_exist):
     """
     Return all pods in namespaces that have a given key
     :param str key: The relevant key
     :param bool does_not_exist: whether to check for the inexistence of this key
     :return PeerSet: All pods in namespace with (or without) the given key
     """
     res = PeerSet()
     for peer in self.peer_set:
         if peer.namespace is None:
             continue
         if (key in peer.namespace.labels) ^ does_not_exist:
             res.add(peer)
     return res
Beispiel #8
0
 def test_and(self):
     default_namespace = K8sNamespace('default')
     pod_a = Pod('A', default_namespace)
     pod_b = Pod('B', default_namespace)
     ip1 = IpBlock("1.2.3.0/24")
     ip2 = IpBlock("1.2.3.0/32")
     pod_set_1 = {pod_a, pod_b, ip2}
     pod_set_2 = {pod_a, ip1}
     a = PeerSet(pod_set_1)
     b = PeerSet(pod_set_2)
     res1 = a & b
     self.assertTrue(res1 == PeerSet({pod_a, ip2}))
     a &= b
     self.assertTrue(a == res1)
Beispiel #9
0
 def get_peers_with_key(self, namespace, key, does_not_exist):
     """
     Return all peers (possibly in a given namespace) that have a specific key in their labels
     :param K8sNamespace namespace: If not none - only include peers in this namespace
     :param str key: The relevant key
     :param bool does_not_exist: Whether to only include peers that do not have this key
     :return PeerSet: All peers that (do not) have the key
     """
     res = PeerSet()
     for peer in self.peer_set:
         if namespace is not None and peer.namespace != namespace:
             continue
         if (key in peer.labels or key in peer.extra_labels) ^ does_not_exist:
             res.add(peer)
     return res
Beispiel #10
0
    def parse_label_selector(self, label_selector):
        """
        Parse a LabelSelector element
        :param dict label_selector: The element to parse
        :return: A PeerSet containing all the pods captured by this selection
        :rtype: Peer.PeerSet
        """
        if label_selector is None:
            return PeerSet()  # A None value means the selector selects nothing
        if not label_selector:
            return self.peer_container.get_all_peers_group(
            )  # An empty value means the selector selects everything

        allowed_elements = {'matchLabels': [0, dict]}
        self.check_fields_validity(label_selector,
                                   'authorization policy WorkloadSelector',
                                   allowed_elements)

        res = self.peer_container.get_all_peers_group()
        match_labels = label_selector.get('matchLabels')
        if match_labels:
            for key, val in match_labels.items():
                res &= self.peer_container.get_peers_with_label(key, [val])
            self.allowed_labels.add(':'.join(match_labels.keys()))

        if not res:
            self.warning(
                'A podSelector selects no pods. Better use "podSelector: Null"',
                label_selector)

        return res
 def __init__(self,
              ns_resources=None,
              peer_resources=None,
              config_name='global'):
     """
     create a PeerContainer object
     :param list ns_resources: the list of namespace resources
     :param list peer_resources: the list of peer resources
     :param str config_name: the config name
     """
     self.peer_set = PeerSet()
     self.namespaces = {
     }  # mapping from namespace name to the actual K8sNamespace object
     self.representative_peers = {}
     if ns_resources:
         self._set_namespace_list(ns_resources)
     if peer_resources:
         self._set_peer_list(peer_resources, config_name)
    def parse_source(self, source_dict):
        """
        Parse a source peer inside a rule (an element of the 'from' array)
        :param dict source_dict: The object to parse
        :return: A PeerSet object containing the set of peers defined by the selectors/ipblocks
        :rtype: Peer.PeerSet
        """

        from_allowed_elements = {'source': [1, dict]}
        self.check_fields_validity(source_dict, 'authorization policy rule: from', from_allowed_elements)

        source_peer = source_dict.get('source')
        if source_peer is None:
            self.syntax_error('Authorization policy from.source cannot be null. ')

        # TODO: support source with multiple attributes ("fields in the source are ANDed together")
        # TODO: add support for allowed elements currently unsupported (principals, requestPrincipals, remoteIpBlocks)
        allowed_elements = {'namespaces': [0, list], 'notNamespaces': [0, list], 'ipBlocks': [0, list],
                            'notIpBlocks': [0, list], 'principals': [0, list], 'notPrincipals': [0, list],
                            'requestPrincipals': 2,
                            'notRequestPrincipals': 2, 'remoteIpBlocks': 2, 'notRemoteIpBlocks': 2}
        # TODO: though specified 'list' value_type, check_fields_validity doesn't fail since value is None (empty)...
        self.check_fields_validity(source_peer, 'authorization policy rule: source', allowed_elements)
        for key_elem in allowed_elements:
            self.validate_existing_key_is_not_null(source_peer, key_elem)
        self.validate_dict_elem_has_non_empty_array_value(source_peer, 'from.source')

        has_ns = 'namespaces' in source_peer or 'notNamespaces' in source_peer
        has_ip = 'ipBlocks' in source_peer or 'notIpBlocks' in source_peer
        has_principals = 'principals' in source_peer or 'notPrincipals' in source_peer

        # TODO: how to support a source peer with both namespace and ip-block properties?
        #  currently assuming ip-block is only outside the cluster
        if has_ip and (has_principals or has_ns):
            self.warning('currently not supporting source with both namespaces/principals and ip block')
            # TODO: should return empty peerSet if has requirements of both ns and ip-block ?
            return PeerSet()

        res = self.peer_container.get_all_peers_group(True)

        if has_principals:
            principals_list = source_peer.get('principals')
            not_principals_list = source_peer.get('notPrincipals')
            res &= self.parse_principals(principals_list, not_principals_list)

        if has_ns:
            ns_list = source_peer.get('namespaces')
            not_ns_list = source_peer.get('notNamespaces')
            res &= self.parse_namespaces(ns_list, not_ns_list)

        elif has_ip:
            ip_blocks = source_peer.get('ipBlocks')
            not_ip_blocks = source_peer.get('notIpBlocks')
            res &= self.parse_ip_block(ip_blocks, not_ip_blocks)

        return res
Beispiel #13
0
    def parse_ingress_rule(self, rule):
        """
        Parse a single ingress rule, producing a IstioPolicyRule.
        :param dict rule: The dict with the rule fields
        :return: A IstioPolicyRule with the proper PeerSet and ConnectionSet
        :rtype: IstioPolicyRule
        """
        if rule is None:
            self.syntax_error('Authorization policy rule cannot be null. ')

        allowed_elements = {
            'from': [0, list],
            'to': [0, list],
            'when': [0, list]
        }
        self.check_fields_validity(rule, 'authorization policy rule',
                                   allowed_elements)
        for key_elem in allowed_elements:
            self.validate_existing_key_is_not_null(rule, key_elem)

        # collect source peers into res_peers
        from_array = self.get_key_array_and_validate_not_empty(rule, 'from')
        if from_array is not None:
            res_peers = PeerSet()
            for source_dict in from_array:
                res_peers |= self.parse_source(source_dict)
        else:  # no 'from' in the rule => all source peers allowed
            res_peers = self.peer_container.get_all_peers_group(True)

        to_array = self.get_key_array_and_validate_not_empty(rule, 'to')
        # currently parsing only ports
        # TODO: extend operations parsing to include other attributes
        if to_array is not None:
            connections = ConnectionSet()
            for operation_dict in to_array:
                connections |= self.parse_operation(operation_dict)
        else:  # no 'to' in the rule => all connections allowed
            connections = ConnectionSet(True)

        # condition possible result value: source-ip (from) , source-namespace (from) [Peerset], destination.port (to) [ConnectionSet]
        # should update either res_pods or connections according to the condition
        condition_array = rule.get(
            'when')  # this array can be empty (unlike 'to' and 'from')
        # the combined condition ("AND" of all conditions) should be applied
        if condition_array is not None:
            for condition in condition_array:
                condition_res = self.parse_condition(condition)
                if isinstance(condition_res, PeerSet):
                    res_peers &= condition_res
                elif isinstance(condition_res, ConnectionSet):
                    connections &= condition_res

        if not res_peers:
            self.warning('Rule selects no pods', rule)

        return IstioPolicyRule(res_peers, connections)
 def __init__(self, name, namespace):
     self.name = name
     self.namespace = namespace
     self.selected_peers = PeerSet()  # The peers affected by this policy
     self.ingress_rules = []
     self.egress_rules = []
     self.affects_ingress = False  # whether the policy affects the ingress of the selected peers
     self.affects_egress = False  # whether the policy affects the egress of the selected peers
     self.findings = [
     ]  # accumulated findings which are relevant only to this policy (emptiness and redundancy)
Beispiel #15
0
 def referenced_ip_blocks(self):
     """
     :return: A set of all ipblocks referenced in one of the policy rules (one Peer object per one ip range)
     :rtype: Peer.PeerSet
     """
     res = PeerSet()
     for rule in self.ingress_rules:
         for peer in rule.peer_set:
             if isinstance(peer, IpBlock):
                 res |= peer.split()
     return res
Beispiel #16
0
 def get_pods_with_service_name_containing_given_string(self, name_substring):
     """
     Returns all pods that belong to services whose name contains the given substring
     :param str name_substring: the service name substring
     :return: PeerSet
     """
     res = PeerSet()
     for key, val in self.services.items():
         if name_substring in key:
             res |= val.target_pods
     return res
 def __init__(self, policy, peer_container, ingress_file_name=''):
     """
     :param dict policy: The ingress policy object as provided by the yaml parser
     :param PeerContainer peer_container: The ingress policy will be evaluated against this set of peers
     :param str ingress_file_name: The name of the ingress resource file
     """
     GenericYamlParser.__init__(self, ingress_file_name)
     self.policy = policy
     self.peer_container = peer_container
     self.namespace = None
     self.default_backend_peers = PeerSet()
     self.default_backend_ports = PortSet()
Beispiel #18
0
 def _set_services_and_populate_target_pods(self, service_list):
     """
     Populates services from the given service list,
     and for every service computes and populates its target pods.
     :param list service_list: list of service in K8sService format
     :return: None
     """
     for srv in service_list:
         # populate target ports
         if srv.selector:
             srv.target_pods = self.peer_set
         for key, val in srv.selector.items():
             srv.target_pods &= self.get_peers_with_label(key, [val], GenericYamlParser.FilterActionType.In,
                                                          srv.namespace)
         # remove target_pods that don't contain named ports referenced by target_ports
         for port in srv.ports.values():
             if not isinstance(port.target_port, str):
                 continue
             # check if all pods include this named port, and remove those that don't
             pods_to_remove = PeerSet()
             for pod in srv.target_pods:
                 pod_named_port = pod.named_ports.get(port.target_port)
                 if not pod_named_port:
                     print(f'Warning: The named port {port.target_port} referenced in Service {srv.name}'
                           f' is not defined in the pod {pod}. Ignoring the pod')
                     pods_to_remove.add(pod)
                 elif pod_named_port[1] != port.protocol:
                     print(f'Warning: The protocol {port.protocol} in the named port {port.target_port} '
                           f'referenced in Service {srv.name} does not match the protocol {pod_named_port[1]} '
                           f'defined in the pod {pod}. Ignoring the pod')
                     pods_to_remove.add(pod)
             srv.target_pods -= pods_to_remove
         if not srv.target_pods:
             print(f'Warning: The service {srv.name} does not reference any pod')
         self.services[srv.full_name()] = srv
    def _recursive_parse_label_selector(self, label_selector, origin_map,
                                        namespace, namespace_selector):
        """
        Recursive Parse a label selector expression appearing in selector/notSelector/namespaceSelector parts of the EntityRule
        :param str label_selector: The selector expression to parse
        :param dict origin_map: The EntityRule object (for reporting errors)
        :param K8sNamespace namespace: Restrict pods to the given namespace
        :param bool namespace_selector: True if this is a namespaceSelector
        :return: the set of peers selected by the selector expression
        :rtype: PeerSet
        """

        include_globals = not namespace_selector or 'global()' in label_selector
        label_selector = self._strip_selector(origin_map, label_selector)
        # We are handling the operators according to the "order of operation" - '!', '&&', '||'.
        # i.e. we will first try to spit the label by '||',
        # if the label does not contain '||', we will split by '&&',
        # if the label does not contain &&, we will look for the prefix '!',
        # and if there is no '!', we will evaluate the expression.
        # handling '||' :
        split_expr = self._split_selector(label_selector, '||')
        if len(split_expr) != 1:
            res = PeerSet()
            for expr in split_expr:
                res |= self._recursive_parse_label_selector(
                    expr, origin_map, namespace, namespace_selector)
            return res

        # there is no '||', handling '&&' :
        split_expr = self._split_selector(label_selector, '&&')
        if len(split_expr) != 1:
            res = self.peer_container.get_all_peers_group(
                include_globals=include_globals)
            for expr in split_expr:
                res &= self._recursive_parse_label_selector(
                    expr, origin_map, namespace, namespace_selector)
            return res

        # there is no '&&', handling '!' :
        if label_selector[0] == '!':
            if namespace_selector:
                all_peers = self.peer_container.get_all_peers_group(
                    include_globals=include_globals)
            else:
                all_peers = self.peer_container.get_namespace_pods(namespace)
            return all_peers - self._recursive_parse_label_selector(
                label_selector[1:], origin_map, namespace, namespace_selector)

        # there is no operator, parsing the expression:
        return self._parse_selector_expr(split_expr[0], origin_map, namespace,
                                         namespace_selector)
 def parse_principals(self, principals_list, not_principals_list):
     """
     Parse a principals element (within a source component of  a rule)
     :param list[str] principals_list: list of principals patterns/strings
     :param list[str] not_principals_list: negative list of principals patterns/strings
     :return: A PeerSet containing the relevant pods
     :rtype: Peer.PeerSet
     """
     res = PeerSet() if principals_list is not None else self.peer_container.get_all_peers_group()
     for principal in principals_list or []:
         res |= self._parse_principal_str(principal, principals_list)
     for principal in not_principals_list or []:
         res -= self._parse_principal_str(principal, not_principals_list)
     return res
 def _parse_ns_str(self, ns):
     """
     parse a namespace string from source component in rule
     :param str ns: the namespace string
     :return: PeerSet: All pods in the namespace ns
     """
     ns_str_values = self._parse_istio_regex_from_enumerated_domain(ns, 'namespaces')
     res = PeerSet()
     if not ns_str_values:
         self.warning(f"no match for namespace: {ns}")
     for ns_str in ns_str_values:
         ns_obj = self.peer_container.get_namespace(ns_str)
         res |= self.peer_container.get_namespace_pods(ns_obj)
     return res
 def _parse_principal_str(self, principal, principals_list):
     """
     parse a principal string from source component in rule
     :param str principal: the principal str, currently assuming in format: "cluster.local/ns/<ns-str>/sa/<sa-str>"
     :param list principals_list: The principals object (for reporting warnings)
     :return: PeerSet: All pods with the given ns + sa_name as extracted from principal str
     """
     principal_str_values = self._parse_istio_regex_from_enumerated_domain(principal, 'principals')
     res = PeerSet()
     if not principal_str_values:
         self.warning(f"no match for principal: {principal}", principals_list)
     for principal_str in principal_str_values:
         ns, sa_name = self._get_principal_str_components(principal_str)
         if ns and sa_name:
             res |= self.peer_container.get_pods_with_service_account_name(sa_name, ns)
     return res
    def _get_rule_peers(self, entity_rule):
        """
        Parse the peer-specifying parts of the source/destination parts of a rule
        :param dict entity_rule: The object to parse
        :return: The peers that are specified by nets/notNets/selector/notSelector/namespaceSelector
        :rtype: PeerSet
        """
        nets = entity_rule.get('nets')
        if nets:
            rule_ips = IpBlock(nets[0])
            for cidr in nets[1:]:
                rule_ips |= IpBlock(cidr)
        else:
            rule_ips = IpBlock.get_all_ips_block()

        not_nets = entity_rule.get('notNets', [])
        for cidr in not_nets:
            rule_ips -= IpBlock(cidr)

        ns_selector = self._get_value_as_str(entity_rule, 'namespaceSelector')
        pod_selector = self._get_value_as_str(entity_rule, 'selector')
        not_pod_selector = self._get_value_as_str(entity_rule, 'notSelector')
        if ns_selector:
            rule_peers = self._parse_label_selector(ns_selector,
                                                    entity_rule,
                                                    namespace_selector=True)
        elif pod_selector:
            rule_peers = self.peer_container.get_namespace_pods(self.namespace)
        elif nets or not_nets:
            rule_peers = PeerSet()
            rule_peers.add(rule_ips)
        else:
            rule_peers = self.peer_container.get_all_peers_group(True)

        ns_to_use = self.namespace if not ns_selector else None
        if pod_selector is not None:
            selected_pods = self._parse_label_selector(pod_selector,
                                                       entity_rule, ns_to_use)
            if pod_selector.strip() != 'all()' and selected_pods == rule_peers:
                self.warning(
                    'selector has no effect - better delete or use "all()"',
                    entity_rule)
            rule_peers &= selected_pods
        if not_pod_selector:
            rule_peers -= self._parse_label_selector(not_pod_selector,
                                                     entity_rule, ns_to_use)

        if (nets or not_nets) and (ns_selector or pod_selector):
            rule_peers = PeerSet()
            self.warning(
                'Mixing ip-based selection with label-based selection is likely a mistake',
                entity_rule)

        return rule_peers
 def parse_namespaces(self, ns_list, not_ns_list):
     """
     Parse a namespaces element (within a source component of  a rule)
     :param list[str] ns_list: list of namespaces patterns/strings
     :param list[str] not_ns_list: negative list of namespaces patterns/strings
     :return: A PeerSet containing the relevant pods
     :rtype: Peer.PeerSet
     """
     # If 'namespaces' not set, any namespace is allowed.
     ns_list = self.peer_container.get_all_namespaces_str_list() if ns_list is None else ns_list
     not_ns_list = [] if not_ns_list is None else not_ns_list
     res = PeerSet()
     for ns in ns_list:
         res |= self._parse_ns_str(ns)
     for ns in not_ns_list:
         res -= self._parse_ns_str(ns)
     return res
 def _make_rules_from_conns(self, tcp_conns):
     """
     Make IngressPolicyRules from the given connections
     :param TcpLikeProperties tcp_conns: the given connections
     :return: the list of IngressPolicyRules
     """
     peers_to_conns = dict()
     res = []
     # extract peers dimension from cubes
     for cube in tcp_conns:
         ports = None
         paths = None
         hosts = None
         peer_set = None
         for i, dim in enumerate(tcp_conns.active_dimensions):
             if dim == "dst_ports":
                 ports = cube[i]
             elif dim == "paths":
                 paths = cube[i]
             elif dim == "hosts":
                 hosts = cube[i]
             elif dim == "peers":
                 peer_set = PeerSet(
                     set(
                         tcp_conns.base_peer_set.get_peer_list_by_indices(
                             cube[i])))
             else:
                 assert False
         if not peer_set:
             peer_set = self.peer_container.peer_set.copy()
         port_set = PortSet()
         port_set.port_set = ports
         port_set.named_ports = tcp_conns.named_ports
         port_set.excluded_named_ports = tcp_conns.excluded_named_ports
         new_conns = self._get_connection_set_from_properties(
             port_set, paths_dfa=paths, hosts_dfa=hosts)
         if peers_to_conns.get(peer_set):
             peers_to_conns[
                 peer_set] |= new_conns  # optimize conns for the same peers
         else:
             peers_to_conns[peer_set] = new_conns
     for peer_set, conns in peers_to_conns.items():
         res.append(IngressPolicyRule(peer_set, conns))
     return res
Beispiel #26
0
 def get_all_peers_group(self, add_external_ips=False, include_globals=True):
     """
     Return all peers known in the system
     :param bool add_external_ips: Whether to also add the full range of ips
     :param bool include_globals: Whether to include global peers
     :return PeerSet: The required set of peers
     """
     res = PeerSet()
     for peer in self.peer_set:
         if include_globals or not peer.is_global_peer():
             res.add(peer)
     if add_external_ips:
         res.add(IpBlock.get_all_ips_block())
     return res
 def get_profile_pods(self, profile_name, first_profile_only):
     """
     Return all the pods that have a specific profile assigned
     :param str profile_name: The name of the target profile
     :param bool first_profile_only: whether to only consider the first profile of each pod
     :return PeerSet: The set of pods with the given profile
     """
     res = PeerSet()
     for peer in self.peer_set:
         if peer.has_profiles():
             if first_profile_only:
                 if peer.get_first_profile_name() == profile_name:
                     res.add(peer)
             else:
                 if profile_name in peer.profiles:
                     res.add(peer)
     return res
Beispiel #28
0
 def test_get_peer_set(self):
     ip1 = IpBlock("1.2.3.0/24")
     ip1_set = PeerSet({ip1})
     self.assertTrue(ip1_set == ip1.get_peer_set())
     self.assertTrue(PeerSet() == (ip1-ip1).get_peer_set())
 def get_namespace_pods_with_label(self,
                                   key,
                                   values,
                                   action=FilterActionType.In):
     """
     Return all pods in namespace with a given key-value label
     :param str key: The relevant key
     :param list[str] values: possible values for the key
     :param FilterActionType action: how to filter the values
     :return PeerSet: All pods in namespaces that have (or not) the given key-value label
     """
     res = PeerSet()
     for peer in self.peer_set:
         if peer.namespace is None:
             continue
         # Note: It seems as if the semantics of NotIn is "either key does not exist, or its value is not in values"
         # Reference: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
         if action == self.FilterActionType.In:
             if peer.namespace.labels.get(key, '') in values:
                 res.add(peer)
         elif action == self.FilterActionType.NotIn:
             if peer.namespace.labels.get(key, '') not in values:
                 res.add(peer)
         elif action == self.FilterActionType.Contain:
             if values[0] in peer.namespace.labels.get(key, ''):
                 res.add(peer)
         elif action == self.FilterActionType.StartWith:
             if peer.namespace.labels.get(key, '').startswith(values[0]):
                 res.add(peer)
         elif action == self.FilterActionType.EndWith:
             if peer.namespace.labels.get(key, '').endswith(values[0]):
                 res.add(peer)
     return res
 def get_peers_with_label(self,
                          key,
                          values,
                          action=FilterActionType.In,
                          namespace=None):
     """
     Return all peers that have a specific key-value label (in a specific namespace)
     :param str key: The relevant key
     :param list[str] values: A list of possible values to match
     :param FilterActionType action: how to filter the values
     :param K8sNamespace namespace: If not None, only consider peers in this namespace
     :return PeerSet: All peers that (do not) have the key-value as their label
     """
     res = PeerSet()
     for peer in self.peer_set:
         # Note: It seems as if the semantics of NotIn is "either key does not exist, or its value is not in values"
         # Reference: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
         if namespace is not None and peer.namespace != namespace:
             continue
         if action == self.FilterActionType.In:
             if peer.labels.get(key) in values or peer.extra_labels.get(
                     key) in values:
                 res.add(peer)
         elif action == self.FilterActionType.NotIn:
             if peer.labels.get(
                     key) not in values and peer.extra_labels.get(
                         key) not in values:
                 res.add(peer)
         elif action == self.FilterActionType.Contain:
             if values[0] in peer.labels.get(
                     key, '') or values[0] in peer.extra_labels.get(
                         key, ''):
                 res.add(peer)
         elif action == self.FilterActionType.StartWith:
             if peer.labels.get(key, '').startswith(
                     values[0]) or peer.extra_labels.get(
                         key, '').startswith(values[0]):
                 res.add(peer)
         elif action == self.FilterActionType.EndWith:
             if peer.labels.get(
                     key, '').endswith(values[0]) or peer.extra_labels.get(
                         key, '').endswith(values[0]):
                 res.add(peer)
     return res