...

Text file src/github.com/emissary-ingress/emissary/v3/python/ambassador/ir/irutils.py

Documentation: github.com/emissary-ingress/emissary/v3/python/ambassador/ir

     1# Copyright 2021 Datawire. All rights reserved.
     2#
     3# Licensed under the Apache License, Version 2.0 (the "License");
     4# you may not use this file except in compliance with the License.
     5# You may obtain a copy of the License at
     6#
     7#     http://www.apache.org/licenses/LICENSE-2.0
     8#
     9# Unless required by applicable law or agreed to in writing, software
    10# distributed under the License is distributed on an "AS IS" BASIS,
    11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12# See the License for the specific language governing permissions and
    13# limitations under the License
    14
    15import logging
    16import os
    17from typing import Any, Dict
    18
    19from ambassador.utils import parse_bool
    20
    21######
    22# Utilities for hostglob_matches
    23#
    24# hostglob_matches_start has g1 starting with '*' and g2 not ending with '*';
    25# it's OK for g2 to start with a wilcard too.
    26
    27
    28def hostglob_matches_start(g1: str, g2: str, g2start: bool) -> bool:
    29    # Leading "*" cannot match an empty string, so unless we have a wildcard
    30    # for g2, we have to have g1 longer than g2.
    31
    32    g1match = g1[1:]
    33    g2match = g2[1:] if g2start else g2
    34
    35    if len(g1) > len(g2match):
    36        if not g2start:
    37            # logging.debug("  match start: %s is too short => False", g1)
    38            return False
    39
    40        # Wildcards for both, so make sure we do the substring match against
    41        # the longer one.
    42        g1match = g2[1:]
    43        g2match = g1[1:]
    44
    45    match = g2match.endswith(g1match)
    46    # logging.debug("  match start: %s ~ %s => %s", g1match, g2match, match)
    47
    48    return match
    49
    50
    51# hostglob_matches_end has g1 ending with '*' and g2 not starting with '*';
    52# it's OK for g2 to end with a wilcard too.
    53
    54
    55def hostglob_matches_end(g1: str, g2: str, g2end: bool) -> bool:
    56    # Leading "*" cannot match an empty string, so unless we have a wildcard
    57    # for g2, we have to have g1 longer than g2.
    58    g1match = g1[:-1]
    59    g2match = g2[:-1] if g2end else g2
    60
    61    if len(g1) > len(g2match):
    62        if not g2end:
    63            # logging.debug("  match end: %s is too short => False", g1)
    64            return False
    65
    66        # Wildcards for both, so make sure we do the substring match against
    67        # the longer one.
    68        g1match = g2[:-1]
    69        g2match = g1[:-1]
    70
    71    match = g2match.startswith(g1match)
    72    # logging.debug("  match end: %s ~ %s => %s", g1match, g2match, match)
    73
    74    return match
    75
    76
    77################
    78
    79
    80def hostglob_matches(g1: str, g2: str) -> bool:
    81    """
    82    hostglob_matches determines whether or not two given DNS globs are
    83    compatible with each other, i.e. whether or not there can be a hostname
    84    that matches both globs.
    85
    86    Note that it does not actually find such a hostname: a return of True
    87    just means that such a hostname could exist.
    88    """
    89
    90    # logging.debug("hostglob_matches: %s ~ %s", g1, g2)
    91
    92    # Short-circuit: if g1 & g2 are equal, we're done here.
    93    if g1 == g2:
    94        # logging.debug("  equal => True")
    95        return True
    96
    97    # Next special case: if either glob is "*", then it matches everything.
    98    if (g1 == "*") or (g2 == "*"):
    99        # logging.debug("  \"*\" present => True")
   100        return True
   101
   102    # Final special case: if either starts with a bare ".", that's not OK.
   103    # (Ending with a bare "." is different because DNS.)
   104    if g1[0] == "." or g2[0] == ".":
   105        # logging.debug("  exact match starts with bare \".\" => False")
   106        return False
   107
   108    # OK, we don't have the simple-"*" case, so any wildcards must be at
   109    # the start or end, and they must be a component alone.
   110    g1start = g1[0] == "*"
   111    g1end = g1[-1] == "*"
   112    g2start = g2[0] == "*"
   113    g2end = g2[-1] == "*"
   114
   115    # logging.debug("  g1start=%s g1end=%s g2start=%s g2end=%s", g1start, g1end, g2start, g2end)
   116
   117    if (g1start and g1end) or (g2start and g2end):
   118        # Not a valid DNS glob: you can't have a "*" at both ends. (If you do,
   119        # Envoy will decide that the one at the start is the allowed wildcard, and
   120        # treat the one at the end as a literal "*", which will match nothing.)
   121        return g1 == g2
   122
   123    if not (g1start or g1end or g2start or g2end):
   124        # No valid wildcards. and we already know that they're not equal,
   125        # so this is not a match.
   126        # logging.debug("  not equal => False")
   127        return False
   128
   129    # OK, if we're here, we have a wildcard to check. There are a few cases
   130    # here, so we'll start with the easy one: one value starts with "*" and
   131    # the other ends with "*", because those can always overlap as long as
   132    # the overlap between isn't empty -- and in this method, we only need to
   133    # concern ourselves with being sure that there is a possibility of a match
   134    # to both.
   135    if (g1start and g2end) or (g2start and g1end):
   136        # logging.debug("  start/end pair => True")
   137        return True
   138
   139    # OK, now we have to actually do some work. Again, we really only have to
   140    # be convinced that it's possible for something to match, so e.g.
   141    #
   142    # *example.com, example.com
   143    #
   144    # is not a valid pair, because that "*" must never match an empty string.
   145    # However,
   146    #
   147    # *example.com, *.example.com
   148    #
   149    # is fine, because e.g. "foo.example.com" matches both.
   150
   151    if g1start:
   152        return hostglob_matches_start(g1, g2, g2start)
   153
   154    if g2start:
   155        return hostglob_matches_start(g2, g1, g1start)
   156
   157    if g1end:
   158        return hostglob_matches_end(g1, g2, g2end)
   159
   160    if g2end:
   161        return hostglob_matches_end(g2, g1, g1end)
   162
   163    # This is "impossible"
   164    return False
   165
   166
   167################
   168## disable_strict_selectors is a utility function to control the behaviour of label selectors for Host/Mapping association
   169## and serves to provide a single place where the default value can be updated.
   170##
   171## Ambassador (2.0-2.3) & (3.0-3.1) consider a match on a single label as a "good enough" match.
   172## In versions 2.5+ and 3.2+ _ALL_ labels in a selector must be present for it to be considered a match.
   173## DISABLE_STRICT_LABEL_SELECTORS provides a way to restore the old unintended loose matching behaviour
   174## in the event that it is desired. The ability to disable strict label matching will be removed in a future version
   175
   176
   177def disable_strict_selectors() -> bool:
   178    return parse_bool(os.environ.get("DISABLE_STRICT_LABEL_SELECTORS", "false"))
   179
   180
   181################
   182## selector_matches is a utility for doing K8s label selector matching.
   183
   184
   185def selector_matches(
   186    logger: logging.Logger, selector: Dict[str, Any], labels: Dict[str, str]
   187) -> bool:
   188    match: Dict[str, str] = selector.get("matchLabels") or {}
   189
   190    if not match:
   191        # If there's no matchLabels to match, return True.
   192        logger.debug("      no matchLabels in selector => True")
   193        return True
   194
   195    # If we have stuff to match on, but no labels to actually match them, we
   196    # can short-circuit (and skip a weirder conditional down in the loop).
   197    if not labels:
   198        logger.debug("      no incoming labels => False")
   199        return False
   200
   201    if disable_strict_selectors():
   202        for k, v in match.items():
   203            if labels.get(k) == v:
   204                logger.debug("    selector match for %s=%s => True", k, v)
   205                return True
   206
   207            logger.debug("      selector miss on %s=%s", k, v)
   208
   209        logger.debug("      all selectors miss => False")
   210        return False
   211    else:
   212        # For every label in mappingSelector, there must be a label with same value in the Mapping itself.
   213        for k, v in match.items():
   214            if labels.get(k) == v:
   215                logger.debug("      selector match for %s=%s => True", k, v)
   216            else:
   217                logger.debug("      selector miss for %s=%s => False", k, v)
   218                return False
   219
   220        logger.debug("      all selectors match => True")
   221        return True

View as plain text