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