docker-image-pin 0.4.1__tar.gz → 0.4.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docker-image-pin
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Checks if Docker images are properly pinned in docker-compose.yml and Dockerfile files
5
5
  Author: GideonBear
6
6
  License-Expression: GPL-3.0-only
@@ -4,7 +4,7 @@ requires = [ "uv-build>=0.8.8,<0.9" ]
4
4
 
5
5
  [project]
6
6
  name = "docker-image-pin"
7
- version = "0.4.1"
7
+ version = "0.4.2"
8
8
  description = "Checks if Docker images are properly pinned in docker-compose.yml and Dockerfile files"
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-only"
@@ -0,0 +1,142 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import string
5
+ from argparse import ArgumentParser
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Sequence
12
+
13
+
14
+ default_allows = {
15
+ "debian": "major-minor",
16
+ "postgres": "major-minor",
17
+ "atdr.meo.ws/archiveteam/warrior-dockerfile": "latest",
18
+ "lukaszlach/docker-tc": "latest",
19
+ }
20
+
21
+
22
+ class Args(argparse.Namespace):
23
+ files: Sequence[Path]
24
+
25
+
26
+ def parse_args() -> Args:
27
+ parser = ArgumentParser("docker-image-pin")
28
+
29
+ parser.add_argument(
30
+ "files",
31
+ nargs="+",
32
+ type=Path,
33
+ )
34
+
35
+ return parser.parse_args(namespace=Args())
36
+
37
+
38
+ def process_line(file: Path, lnr: int, line: str) -> int: # noqa: C901, PLR0911, PLR0912
39
+ def log(msg: str) -> None:
40
+ print(f"({file}:{lnr + 1}) {msg}")
41
+
42
+ def invalid(msg: str) -> int:
43
+ log(f"Invalid: {msg}")
44
+ return 1
45
+
46
+ def warn(msg: str) -> None:
47
+ log(f"Warning: {msg}")
48
+
49
+ line = line.strip()
50
+ if not (line.startswith(("image:", "FROM"))):
51
+ return 0
52
+
53
+ allow = None
54
+ if "#" in line:
55
+ line, comment = line.split("#")
56
+ line = line.strip()
57
+ comment = comment.strip()
58
+ if comment.startswith("allow-"):
59
+ allow = comment.removeprefix("allow-")
60
+
61
+ if allow == "all":
62
+ return 0
63
+
64
+ line = line.removeprefix("image:").strip()
65
+ line = line.removeprefix("FROM").strip()
66
+ try:
67
+ rest, sha = line.split("@")
68
+ except ValueError:
69
+ return invalid("no '@'")
70
+ try:
71
+ image, version = rest.split(":")
72
+ except ValueError:
73
+ return invalid("no ':' in leading part")
74
+
75
+ default_allow = default_allows.get(image)
76
+ if default_allow:
77
+ if allow:
78
+ warn(
79
+ "allow comment specified while "
80
+ "there is a default allow for this image. "
81
+ f"(specified '{allow}', default '{default_allow}')"
82
+ )
83
+ allow = default_allow
84
+
85
+ if version in {"latest", "stable"}:
86
+ if allow != version:
87
+ return invalid(
88
+ f"[{version}] uses dynamic tag '{version}' instead of pinned version"
89
+ )
90
+ else:
91
+ if "-" in version:
92
+ version, _extra = version.split("-")
93
+ version = version.removeprefix("v") # Optional prefix
94
+ parts = version.split(".")
95
+ if len(parts) > 3 and allow != "weird-version": # noqa: PLR2004
96
+ # major.minor.patch.???
97
+ return invalid(
98
+ "[weird-version] version contains more than three parts "
99
+ "(major.minor.patch.???)"
100
+ )
101
+ if len(parts) == 2 and allow != "major-minor": # noqa: PLR2004
102
+ # major.minor
103
+ return invalid(
104
+ "[major-minor] version contains only two parts (major.minor). "
105
+ "Can the version be pinned further?"
106
+ )
107
+ if len(parts) == 1 and allow != "major":
108
+ # major
109
+ return invalid(
110
+ "[major] version contains only one part (major). "
111
+ "Can the version be pinned further?"
112
+ )
113
+ if len(parts) == 0:
114
+ msg = "Unreachable"
115
+ raise AssertionError(msg)
116
+
117
+ if not sha.startswith("sha256:"):
118
+ return invalid("invalid hash (doesn't start with 'sha256:'")
119
+ sha = sha.removeprefix("sha256:")
120
+ if not is_valid_sha256(sha):
121
+ return invalid("invalid sha256 digest")
122
+
123
+ return 0
124
+
125
+
126
+ def main() -> int:
127
+ args = parse_args()
128
+
129
+ retval = 0
130
+ for file in args.files:
131
+ content = file.read_text()
132
+
133
+ for lnr, line in enumerate(content.splitlines()):
134
+ line_retval = process_line(file, lnr, line)
135
+ if line_retval == 1:
136
+ retval = 1
137
+
138
+ return retval
139
+
140
+
141
+ def is_valid_sha256(s: str) -> bool:
142
+ return len(s) == 64 and all(c in string.hexdigits for c in s) # noqa: PLR2004
@@ -1,145 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import string
5
- from argparse import ArgumentParser
6
- from pathlib import Path
7
- from typing import TYPE_CHECKING
8
-
9
-
10
- if TYPE_CHECKING:
11
- from collections.abc import Sequence
12
-
13
-
14
- default_allows = {
15
- "debian": "major-minor",
16
- "postgres": "major-minor",
17
- "atdr.meo.ws/archiveteam/warrior-dockerfile": "latest",
18
- "lukaszlach/docker-tc": "latest",
19
- }
20
-
21
-
22
- class Args(argparse.Namespace):
23
- files: Sequence[Path]
24
-
25
-
26
- def parse_args() -> Args:
27
- parser = ArgumentParser("docker-image-pin")
28
-
29
- parser.add_argument(
30
- "files",
31
- nargs="+",
32
- type=Path,
33
- )
34
-
35
- return parser.parse_args(namespace=Args())
36
-
37
-
38
- def main() -> int: # noqa: C901, PLR0912, PLR0915, FIX002, TD003 # TODO(GideonBear): extract line to function
39
- args = parse_args()
40
-
41
- retval = 0
42
- for file in args.files:
43
- content = file.read_text()
44
-
45
- for lnr, line in enumerate(content.splitlines()):
46
-
47
- def log(msg: str) -> None:
48
- print(f"({file}:{lnr + 1}) {msg}") # noqa: B023
49
-
50
- def invalid(msg: str) -> None:
51
- nonlocal retval
52
- retval = 1
53
- log(f"Invalid: {msg}")
54
-
55
- def warn(msg: str) -> None:
56
- log(f"Warning: {msg}")
57
-
58
- line = line.strip()
59
- if not (line.startswith(("image:", "FROM"))):
60
- continue
61
-
62
- allow = None
63
- if "#" in line:
64
- line, comment = line.split("#")
65
- line = line.strip()
66
- comment = comment.strip()
67
- if comment.startswith("allow-"):
68
- allow = comment.removeprefix("allow-")
69
-
70
- line = line.removeprefix("image:").strip()
71
- line = line.removeprefix("FROM").strip()
72
- try:
73
- rest, sha = line.split("@")
74
- except ValueError:
75
- invalid("no '@'")
76
- continue
77
- try:
78
- image, version = rest.split(":")
79
- except ValueError:
80
- invalid("no ':' in leading part")
81
- continue
82
-
83
- default_allow = default_allows.get(image)
84
- if default_allow:
85
- if allow:
86
- warn(
87
- "allow comment specified while "
88
- "there is a default allow for this image. "
89
- f"(specified '{allow}', default '{default_allow}')"
90
- )
91
- allow = default_allow
92
-
93
- if version in {"latest", "stable"}:
94
- if allow != version:
95
- invalid(
96
- f"[{version}] uses dynamic tag '{version}' "
97
- f"instead of pinned version"
98
- )
99
- continue
100
- else:
101
- if "-" in version:
102
- version, _extra = version.split("-")
103
- version = version.removeprefix("v") # Optional prefix
104
- parts = version.split(".")
105
- if len(parts) == 3: # noqa: PLR2004
106
- # major.minor.patch
107
- continue
108
- if len(parts) > 3 and allow != "weird-version": # noqa: PLR2004
109
- # major.minor.patch.???0
110
- invalid(
111
- "[weird-version] version contains more than three parts "
112
- "(major.minor.patch.???)"
113
- )
114
- continue
115
- if len(parts) == 2 and allow != "major-minor": # noqa: PLR2004
116
- # major.minor
117
- invalid(
118
- "[major-minor] version contains only two parts (major.minor). "
119
- "Can the version be pinned further?"
120
- )
121
- continue
122
- if len(parts) == 1 and allow != "major":
123
- # major
124
- invalid(
125
- "[major] version contains only one part (major). "
126
- "Can the version be pinned further?"
127
- )
128
- continue
129
- if len(parts) == 0:
130
- msg = "Unreachable"
131
- raise AssertionError(msg)
132
-
133
- if not sha.startswith("sha256:"):
134
- invalid("invalid hash (doesn't start with 'sha256:'")
135
- continue
136
- sha = sha.removeprefix("sha256:")
137
- if not is_valid_sha256(sha):
138
- invalid("invalid sha256 digest")
139
- continue
140
-
141
- return retval
142
-
143
-
144
- def is_valid_sha256(s: str) -> bool:
145
- return len(s) == 64 and all(c in string.hexdigits for c in s) # noqa: PLR2004