envoy.code.check 0.5.7__tar.gz → 0.5.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/PKG-INFO +1 -1
  2. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/abstract/checker.py +19 -0
  3. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/abstract/extensions.py +184 -1
  4. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/abstract/yamllint.py +2 -25
  5. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/checker.py +1 -1
  6. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/interface.py +5 -0
  7. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy.code.check.egg-info/PKG-INFO +1 -1
  8. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy.code.check.egg-info/requires.txt +1 -1
  9. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/setup.py +2 -2
  10. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/MANIFEST.in +0 -0
  11. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/backend_shim.py +0 -0
  12. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/__init__.py +0 -0
  13. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/abstract/__init__.py +1 -1
  14. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/abstract/base.py +0 -0
  15. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/abstract/changelog.py +0 -0
  16. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/abstract/flake8.py +0 -0
  17. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/abstract/glint.py +0 -0
  18. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/abstract/gofmt.py +0 -0
  19. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/abstract/rst.py +0 -0
  20. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/abstract/runtime_guards.py +0 -0
  21. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/abstract/shellcheck.py +0 -0
  22. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/abstract/yapf.py +0 -0
  23. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/cmd.py +0 -0
  24. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/exceptions.py +0 -0
  25. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/py.typed +0 -0
  26. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy/code/check/typing.py +0 -0
  27. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy.code.check.egg-info/SOURCES.txt +0 -0
  28. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy.code.check.egg-info/dependency_links.txt +0 -0
  29. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy.code.check.egg-info/entry_points.txt +0 -0
  30. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy.code.check.egg-info/namespace_packages.txt +0 -0
  31. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/envoy.code.check.egg-info/top_level.txt +0 -0
  32. {envoy.code.check-0.5.7 → envoy.code.check-0.5.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: envoy.code.check
3
- Version: 0.5.7
3
+ Version: 0.5.9
4
4
  Summary: "Code checker used in Envoy proxy's CI"
5
5
  Home-page: https://github.com/envoyproxy/toolshed/tree/main/envoy.code.check
6
6
  Author: Ryan Northey
@@ -58,6 +58,7 @@ class ACodeChecker(
58
58
  "changelog",
59
59
  "extensions_fuzzed",
60
60
  "extensions_metadata",
61
+ "extensions_owners",
61
62
  "extensions_registered",
62
63
  "glint",
63
64
  "gofmt",
@@ -135,6 +136,8 @@ class ACodeChecker(
135
136
  """Extensions checker."""
136
137
  return self.extensions_class(
137
138
  self.directory,
139
+ owners=self.args.owners,
140
+ codeowners=self.args.codeowners,
138
141
  extensions_build_config=self.args.extensions_build_config,
139
142
  extensions_fuzzed_count=self.args.extensions_fuzzed_count,
140
143
  **self.check_kwargs)
@@ -260,6 +263,8 @@ class ACodeChecker(
260
263
  parser.add_argument("-x", "--excluding", action="append")
261
264
  parser.add_argument("-b", "--binary", action="append")
262
265
  parser.add_argument("-s", "--since")
266
+ parser.add_argument("--codeowners")
267
+ parser.add_argument("--owners")
263
268
  parser.add_argument("--extensions_build_config")
264
269
  parser.add_argument("--extensions_fuzzed_count")
265
270
 
@@ -294,6 +299,15 @@ class ACodeChecker(
294
299
  else:
295
300
  self.succeed("extensions_metadata", [f"{extension}"])
296
301
 
302
+ async def check_extensions_owners(self) -> None:
303
+ """Check for glint issues."""
304
+ checks = await self.extensions.owners_errors
305
+ for extension, errors in sorted(checks.items()):
306
+ if errors:
307
+ self.error("extensions_owners", errors)
308
+ else:
309
+ self.succeed("extensions_owners", [f"{extension}"])
310
+
297
311
  async def check_extensions_registered(self) -> None:
298
312
  """Check for glint issues."""
299
313
  if errors := await self.extensions.registration_errors:
@@ -362,6 +376,11 @@ class ACodeChecker(
362
376
  self.extensions.extensions_schema
363
377
  self.log.debug(f"Preloaded extensions ({len(metadata)})")
364
378
 
379
+ @checker.preload(when=["extensions_owners"])
380
+ async def preload_extensions_owners(self) -> None:
381
+ await self.extensions.owners_errors
382
+ self.log.debug("Preloaded extensions owners")
383
+
365
384
  @checker.preload(
366
385
  when=["python_flake8"],
367
386
  catches=[subprocess.exceptions.OSCommandError])
@@ -4,10 +4,11 @@ import itertools
4
4
  import json
5
5
  import logging
6
6
  import pathlib
7
+ import re
7
8
  from functools import cached_property
8
9
  from typing import (
9
10
  Any, cast, Dict, List,
10
- Optional, Set, Tuple, Type, Union)
11
+ Optional, Pattern, Set, Tuple, Type, Union)
11
12
 
12
13
  import yaml as _yaml
13
14
 
@@ -21,6 +22,10 @@ from envoy.code.check import abstract, exceptions, interface, typing
21
22
 
22
23
  logger = logging.getLogger(__name__)
23
24
 
25
+ CODEOWNER_RE = r"@\S+"
26
+ CODEOWNERS_CONTRIB_RE = r"(/contrib/[^@]*\s+)(@.*)"
27
+ CODEOWNERS_EXTENSIONS_RE = r".*(extensions[^@]*\s+)(@.*)"
28
+
24
29
  FUZZ_TEST_PATH = (
25
30
  "test/extensions/filters/network/common/fuzz/config.bzl")
26
31
  METADATA_PATH = "source/extensions/extensions_metadata.yaml"
@@ -29,6 +34,12 @@ METADATA_ONLY_EXTENSIONS = (
29
34
  CONTRIB_METADATA_PATH = "contrib/extensions_metadata.yaml"
30
35
  EXTENSIONS_SCHEMA = "tools/extensions/extensions_schema.yaml"
31
36
 
37
+ MAINTAINERS_RE = r".*github.com.(.*)\)\)"
38
+ OWNERS_MIN_DEFAULT = 2
39
+ TRACKED_OWNERSHIP_RE = (
40
+ r"^source/extensions/[^/]+/[^/]+/.*"
41
+ r"|^contrib/[^/]+/[^/]+/.*")
42
+
32
43
  # TODO(phlax): remove this workaround if/when per-category status is added
33
44
  UPSTREAM_EXTENSION_CATEGORY = "envoy.filters.http.upstream"
34
45
 
@@ -36,11 +47,14 @@ UPSTREAM_EXTENSION_CATEGORY = "envoy.filters.http.upstream"
36
47
  @abstracts.implementer(interface.IExtensionsCheck)
37
48
  class AExtensionsCheck(abstract.ACodeCheck, metaclass=abstracts.Abstraction):
38
49
  """Extensions check."""
50
+ _owners_min_default: int = OWNERS_MIN_DEFAULT
39
51
 
40
52
  def __init__(
41
53
  self,
42
54
  *args,
43
55
  **kwargs) -> None:
56
+ self._owners = kwargs.pop("owners")
57
+ self._codeowners = kwargs.pop("codeowners")
44
58
  self.extensions_build_config = kwargs.pop("extensions_build_config")
45
59
  self._fuzzed_count = kwargs.pop("extensions_fuzzed_count", None)
46
60
  super().__init__(*args, **kwargs)
@@ -64,6 +78,22 @@ class AExtensionsCheck(abstract.ACodeCheck, metaclass=abstracts.Abstraction):
64
78
  def builtin_extensions(self) -> typing.ExtensionsSchemaBuiltinList:
65
79
  return self.extensions_schema["builtin"]
66
80
 
81
+ @cached_property
82
+ def codeowner_re(self) -> Pattern[str]:
83
+ return re.compile(CODEOWNER_RE)
84
+
85
+ @cached_property
86
+ def codeowners_contrib_re(self) -> Pattern[str]:
87
+ return re.compile(CODEOWNERS_CONTRIB_RE)
88
+
89
+ @cached_property
90
+ def codeowners_extensions_re(self) -> Pattern[str]:
91
+ return re.compile(CODEOWNERS_EXTENSIONS_RE)
92
+
93
+ @cached_property
94
+ def codeowners_path(self) -> pathlib.Path:
95
+ return pathlib.Path(self._codeowners)
96
+
67
97
  @cached_property
68
98
  def configured_extensions(self) -> typing.ConfiguredExtensionsDict:
69
99
  return cast(
@@ -120,6 +150,21 @@ class AExtensionsCheck(abstract.ACodeCheck, metaclass=abstracts.Abstraction):
120
150
  if self._fuzzed_count is not None
121
151
  else None)
122
152
 
153
+ @cached_property
154
+ def maintainers(self) -> set[str]:
155
+ maintainers = {"@UNOWNED"}
156
+ with self.owners_path.open() as f:
157
+ for line in f:
158
+ try:
159
+ maintainers |= self._maintainer_line_parse(line)
160
+ except StopIteration:
161
+ break
162
+ return maintainers
163
+
164
+ @cached_property
165
+ def maintainers_re(self) -> Pattern[str]:
166
+ return re.compile(MAINTAINERS_RE)
167
+
123
168
  @async_property(cache=True)
124
169
  async def metadata(self) -> typing.ExtensionsMetadataDict:
125
170
  return dict(**await self.metadata_core, **await self.metadata_contrib)
@@ -164,6 +209,39 @@ class AExtensionsCheck(abstract.ACodeCheck, metaclass=abstracts.Abstraction):
164
209
  def metadata_only_extensions(self) -> Set[str]:
165
210
  return set(METADATA_ONLY_EXTENSIONS)
166
211
 
212
+ @cached_property
213
+ def owned(self):
214
+ owned = dict(contrib={}, core={})
215
+ with self.codeowners_path.open() as f:
216
+ for line in f:
217
+ owned["core"].update(
218
+ self._owners_extension_match_line(line))
219
+ owned["contrib"].update(
220
+ self._owners_extension_match_line(
221
+ line,
222
+ matcher=self.codeowners_contrib_re))
223
+ return owned
224
+
225
+ @async_property(cache=True)
226
+ async def owners_errors(self) -> Dict[str, Tuple[str, ...]]:
227
+ return (
228
+ await self._owners_tracked
229
+ | await self._owners_found)
230
+
231
+ @cached_property
232
+ def owners_path(self) -> pathlib.Path:
233
+ return pathlib.Path(self._owners)
234
+
235
+ @cached_property
236
+ def ownership_exceptions(self) -> dict[str, dict[str, int]]:
237
+ # TODO(phlax): Put this to config
238
+ return {
239
+ "extensions/filters/http/composite": dict(owners=1),
240
+ "contrib/config/source": dict(owners=0),
241
+ "contrib/config/test": dict(owners=0),
242
+ "contrib/common/sqlutils/": dict(owners=1),
243
+ "contrib/language/": dict(owners=1)}
244
+
167
245
  @async_property
168
246
  async def registration_errors(self) -> List[str]:
169
247
  return [
@@ -185,6 +263,24 @@ class AExtensionsCheck(abstract.ACodeCheck, metaclass=abstracts.Abstraction):
185
263
  and (data["security_posture"]
186
264
  == "robust_to_untrusted_downstream"))])
187
265
 
266
+ @async_property
267
+ async def tracked_directories(self) -> set[str]:
268
+ return set(
269
+ str(pathlib.Path(path).parent)
270
+ for path
271
+ in await self.directory.files
272
+ if self.tracked_ownership_re.match(path))
273
+
274
+ @cached_property
275
+ def tracked_ownership(self) -> tuple[str, ...]:
276
+ return (
277
+ tuple(f"source/{p}" for p in self.owned['core'].keys())
278
+ + tuple(self.owned["contrib"].keys()))
279
+
280
+ @cached_property
281
+ def tracked_ownership_re(self) -> Pattern[str]:
282
+ return re.compile(TRACKED_OWNERSHIP_RE)
283
+
188
284
  async def check_metadata(self, extension: str) -> Tuple[str, ...]:
189
285
  return tuple(
190
286
  itertools.chain.from_iterable(
@@ -194,6 +290,32 @@ class AExtensionsCheck(abstract.ACodeCheck, metaclass=abstracts.Abstraction):
194
290
  self._check_metadata_status(extension),
195
291
  self._check_metadata_status_upstream(extension))))
196
292
 
293
+ @async_property(cache=True)
294
+ async def _owners_found(self) -> dict[str, tuple[str, ...]]:
295
+ return {
296
+ directory: self._owners_error_matches(directory)
297
+ for directory
298
+ in await self.tracked_directories}
299
+
300
+ def _owners_less_than_min(self, extension, data) -> int:
301
+ min_owners = self.ownership_exceptions.get(
302
+ extension, {}).get("owners", self._owners_min_default)
303
+ return (
304
+ min_owners
305
+ if (len(data["owners"]) < min_owners
306
+ and data["owners"] != {"@UNOWNED"})
307
+ else 0)
308
+
309
+ @async_property(cache=True)
310
+ async def _owners_tracked(self) -> dict[str, tuple[str, ...]]:
311
+ return {
312
+ extension: self._owners_error_tracked(
313
+ extension,
314
+ extension_type,
315
+ data)
316
+ for extension_type, extensions in self.owned.items()
317
+ for extension, data in extensions.items()}
318
+
197
319
  async def _check_metadata_categories(
198
320
  self, extension: str) -> Tuple[str, ...]:
199
321
  categories = (await self.metadata)[extension].get("categories", ())
@@ -279,6 +401,14 @@ class AExtensionsCheck(abstract.ACodeCheck, metaclass=abstracts.Abstraction):
279
401
  logger.warning(warn_message.format(path=path, e=e))
280
402
  return e.value
281
403
 
404
+ def _maintainer_line_parse(self, line: str) -> set[str]:
405
+ if "Senior extension maintainers" in line:
406
+ raise StopIteration()
407
+ return (
408
+ {f"@{m.group(1).lower()}"}
409
+ if (m := self.maintainers_re.search(line)) is not None
410
+ else set())
411
+
282
412
  async def _metadata(self, path) -> typing.ExtensionsMetadataDict:
283
413
  errors = (functional.exceptions.TypeCastingError, FileNotFoundError)
284
414
  try:
@@ -290,3 +420,56 @@ class AExtensionsCheck(abstract.ACodeCheck, metaclass=abstracts.Abstraction):
290
420
  raise exceptions.ExtensionsConfigurationError(
291
421
  "Failed to parse extensions metadata "
292
422
  f"({path}): {e}")
423
+
424
+ def _owners_error_matches(self, path) -> tuple[str, ...]:
425
+ _skip = (
426
+ not self._owners_expected(path)
427
+ or path.startswith(self.tracked_ownership))
428
+ if _skip:
429
+ return ()
430
+ for tracked in self.tracked_ownership:
431
+ if tracked.startswith(path):
432
+ return ()
433
+ return (f"Directory ({path}) has no owners in CODEOWNERS", )
434
+
435
+ def _owners_error_tracked(
436
+ self,
437
+ extension: str,
438
+ extension_type: str,
439
+ data: dict[str, set]) -> tuple[str, ...]:
440
+ errors: tuple[str, ...] = ()
441
+ if min_owners := self._owners_less_than_min(extension, data):
442
+ errors += (
443
+ f"Extension ({extension}) has less than minimum "
444
+ f"of {min_owners} owners ({len(data['owners'])}) "
445
+ "in CODEOWNERS", )
446
+ if extension_type == "core" and len(data["maintainers"]) < 1:
447
+ errors += (
448
+ f"Extension ({extension}) has less than minimum "
449
+ f"of 1 maintainer ({len(data['maintainers'])}) "
450
+ "in CODEOWNERS", )
451
+ return errors
452
+
453
+ def _owners_expected(self, path: str, default: int = 1) -> int:
454
+ return self.ownership_exceptions.get(
455
+ path, {}).get("owners", default)
456
+
457
+ def _owners_extension_match_line(
458
+ self,
459
+ line: str,
460
+ matcher: Optional[
461
+ Pattern[str]] = None) -> dict[str, dict[str, set]]:
462
+ if line.startswith('#'):
463
+ return {}
464
+ m = (matcher or self.codeowners_extensions_re).search(line)
465
+ if m is None:
466
+ return {}
467
+ path = m.group(1).strip().lstrip("/")
468
+ owners = set(
469
+ self.codeowner_re.findall(m.group(2).strip()))
470
+ return {
471
+ path: dict(
472
+ owners=owners,
473
+ maintainers=(
474
+ owners
475
+ & self.maintainers))}
@@ -1,11 +1,10 @@
1
1
 
2
2
  import io
3
3
  import pathlib
4
- import re
5
4
  from functools import cached_property, partial
6
5
  from typing import (
7
6
  AsyncIterator, Dict, Generator, Iterator, List, Optional,
8
- Pattern, Set, Tuple)
7
+ Set, Tuple)
9
8
 
10
9
  from yamllint import linter # type:ignore
11
10
  from yamllint.config import YamlLintConfig # type:ignore
@@ -23,12 +22,6 @@ from envoy.code.check import abstract, typing
23
22
 
24
23
 
25
24
  YAMLLINT_CONFIG = '.yamllint'
26
- YAMLLINT_MATCH_RE = (
27
- r"[\w/\.]*\.yml$",
28
- r"[\w/\.]*\.yaml$", )
29
- YAMLLINT_NOMATCH_RE = (
30
- r"[\w/\.]*\.template\.yaml$",
31
- r"[\w/\.]*/server_xds\.cds\.with_unknown_field\.*\.yaml$")
32
25
 
33
26
 
34
27
  @abstracts.implementer(directory.IDirectoryContext)
@@ -103,13 +96,7 @@ class AYamllintCheck(abstract.AFileCodeCheck, metaclass=abstracts.Abstraction):
103
96
 
104
97
  @async_property
105
98
  async def checker_files(self) -> Set[str]:
106
- """Files with a `.yaml` suffix, but that are not excluded."""
107
- return set(
108
- path
109
- for path
110
- in await self.directory.files
111
- if (self.path_match_re.match(path)
112
- and not self.path_match_exclude_re.match(path)))
99
+ return await self.directory.files
113
100
 
114
101
  @cached_property
115
102
  def config(self) -> YamlLintConfig:
@@ -119,16 +106,6 @@ class AYamllintCheck(abstract.AFileCodeCheck, metaclass=abstracts.Abstraction):
119
106
  def config_path(self) -> pathlib.Path:
120
107
  return self.directory.path.joinpath(YAMLLINT_CONFIG)
121
108
 
122
- @cached_property
123
- def path_match_re(self) -> Pattern[str]:
124
- """Regex to match files to check."""
125
- return re.compile("|".join(YAMLLINT_MATCH_RE))
126
-
127
- @cached_property
128
- def path_match_exclude_re(self) -> Pattern[str]:
129
- """Regex to match files not to check."""
130
- return re.compile("|".join(YAMLLINT_NOMATCH_RE))
131
-
132
109
  @async_property(cache=True)
133
110
  async def problem_files(self) -> "typing.ProblemDict":
134
111
  return dict(await AwaitableGenerator(self._problem_files))
@@ -42,7 +42,7 @@ class YapfCheck(abstract.AYapfCheck):
42
42
 
43
43
 
44
44
  @abstracts.implementer(abstract.AYamllintCheck)
45
- class YamllintCheck:
45
+ class YamllintCheck(abstract.AYamllintCheck):
46
46
  pass
47
47
 
48
48
 
@@ -80,6 +80,11 @@ class IExtensionsCheck(metaclass=abstracts.Interface):
80
80
  async def metadata_errors(self) -> Dict[str, Tuple[str, ...]]:
81
81
  raise NotImplementedError
82
82
 
83
+ @property # type:ignore
84
+ @abstracts.interfacemethod
85
+ async def owners_errors(self) -> Dict[str, Tuple[str, ...]]:
86
+ raise NotImplementedError
87
+
83
88
  @property # type:ignore
84
89
  @abstracts.interfacemethod
85
90
  async def registration_errors(self) -> List[str]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: envoy.code.check
3
- Version: 0.5.7
3
+ Version: 0.5.9
4
4
  Summary: "Code checker used in Envoy proxy's CI"
5
5
  Home-page: https://github.com/envoyproxy/toolshed/tree/main/envoy.code.check
6
6
  Author: Ryan Northey
@@ -1,5 +1,5 @@
1
1
  abstracts>=0.0.12
2
- aio.core>=0.10.0
2
+ aio.core>=0.10.1
3
3
  aio.run.checker>=0.5.7
4
4
  envoy.base.utils>=0.4.7
5
5
  flake8>=6
@@ -15,7 +15,7 @@ setup(**{
15
15
  },
16
16
  'install_requires': (
17
17
  'abstracts>=0.0.12',
18
- 'aio.core>=0.10.0',
18
+ 'aio.core>=0.10.1',
19
19
  'aio.run.checker>=0.5.7',
20
20
  'envoy.base.utils>=0.4.7',
21
21
  'flake8>=6',
@@ -46,5 +46,5 @@ Code checker used in Envoy proxy's CI
46
46
  ),
47
47
  'python_requires': '>=3.10.0',
48
48
  'url': 'https://github.com/envoyproxy/toolshed/tree/main/envoy.code.check',
49
- 'version': '0.5.7',
49
+ 'version': '0.5.9',
50
50
  })
@@ -51,10 +51,10 @@ __all__ = (
51
51
  "AYapfCheck",
52
52
  "base",
53
53
  "checker",
54
+ "extensions",
54
55
  "flake8",
55
56
  "glint",
56
57
  "gofmt",
57
- "extensions",
58
58
  "shellcheck",
59
59
  "changelog",
60
60
  "yamllint",