c2cciutils 1.7.0.dev174__py3-none-any.whl → 1.8.0.dev45__py3-none-any.whl

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.

Potentially problematic release.


This version of c2cciutils might be problematic. Click here for more details.

Files changed (35) hide show
  1. c2cciutils/__init__.py +15 -230
  2. c2cciutils/applications-versions.yaml +3 -3
  3. c2cciutils/applications_definition.py +20 -22
  4. c2cciutils/configuration.py +83 -554
  5. c2cciutils/env.py +8 -31
  6. c2cciutils/lib/docker.py +2 -8
  7. c2cciutils/lib/oidc.py +188 -0
  8. c2cciutils/package-lock.json +115 -127
  9. c2cciutils/package.json +1 -1
  10. c2cciutils/publish.py +26 -44
  11. c2cciutils/schema.json +3 -230
  12. c2cciutils/scripts/__init__.py +1 -3
  13. c2cciutils/scripts/clean.py +4 -11
  14. c2cciutils/scripts/docker_logs.py +4 -4
  15. c2cciutils/scripts/docker_versions_gen.py +0 -1
  16. c2cciutils/scripts/download_applications.py +0 -2
  17. c2cciutils/scripts/env.py +2 -6
  18. c2cciutils/scripts/k8s/__init__.py +1 -3
  19. c2cciutils/scripts/k8s/wait.py +2 -2
  20. c2cciutils/scripts/main.py +4 -16
  21. c2cciutils/scripts/publish.py +45 -31
  22. c2cciutils/scripts/trigger_image_update.py +3 -8
  23. c2cciutils/scripts/version.py +5 -4
  24. {c2cciutils-1.7.0.dev174.dist-info → c2cciutils-1.8.0.dev45.dist-info}/LICENSE +1 -1
  25. {c2cciutils-1.7.0.dev174.dist-info → c2cciutils-1.8.0.dev45.dist-info}/METADATA +29 -58
  26. c2cciutils-1.8.0.dev45.dist-info/RECORD +37 -0
  27. {c2cciutils-1.7.0.dev174.dist-info → c2cciutils-1.8.0.dev45.dist-info}/WHEEL +1 -1
  28. {c2cciutils-1.7.0.dev174.dist-info → c2cciutils-1.8.0.dev45.dist-info}/entry_points.txt +0 -3
  29. c2cciutils/audit.py +0 -229
  30. c2cciutils/pr_checks.py +0 -286
  31. c2cciutils/scripts/audit.py +0 -41
  32. c2cciutils/scripts/docker_versions_update.py +0 -85
  33. c2cciutils/scripts/pr_checks.py +0 -78
  34. c2cciutils/security.py +0 -59
  35. c2cciutils-1.7.0.dev174.dist-info/RECORD +0 -42
c2cciutils/env.py CHANGED
@@ -10,9 +10,7 @@ import c2cciutils.configuration
10
10
 
11
11
 
12
12
  class PrintVersions:
13
- """
14
- Print some tools versions.
15
- """
13
+ """Print some tools versions."""
16
14
 
17
15
  def __init__(self, config: c2cciutils.configuration.PrintVersions) -> None:
18
16
  """Construct."""
@@ -24,9 +22,7 @@ class PrintVersions:
24
22
 
25
23
 
26
24
  class PrintConfig:
27
- """
28
- Print the configuration.
29
- """
25
+ """Print the configuration."""
30
26
 
31
27
  def __init__(self, config: c2cciutils.configuration.Configuration) -> None:
32
28
  """Construct."""
@@ -40,61 +36,42 @@ class PrintConfig:
40
36
 
41
37
 
42
38
  def print_environment_variables() -> None:
43
- """
44
- Print the environment variables.
45
- """
46
-
39
+ """Print the environment variables."""
47
40
  for name, value in sorted(os.environ.items()):
48
41
  if name != "GITHUB_EVENT":
49
42
  print(f"{name}: {value}")
50
43
 
51
44
 
52
45
  def print_github_event_file() -> None:
53
- """
54
- Print the GitHub event file.
55
- """
56
-
46
+ """Print the GitHub event file."""
57
47
  if "GITHUB_EVENT_PATH" in os.environ:
58
48
  with open(os.environ["GITHUB_EVENT_PATH"], encoding="utf-8") as event:
59
49
  print(event.read())
60
50
 
61
51
 
62
52
  def print_github_event_object() -> None:
63
- """
64
- Print the GitHub event object.
65
- """
66
-
53
+ """Print the GitHub event object."""
67
54
  github_event = json.loads(os.environ["GITHUB_EVENT"])
68
55
  print(yaml.dump(github_event, indent=2))
69
56
 
70
57
 
71
58
  def print_python_package_version() -> None:
72
- """
73
- Print the version of the Python packages.
74
- """
75
-
59
+ """Print the version of the Python packages."""
76
60
  subprocess.run(["python3", "-m", "pip", "freeze", "--all"]) # pylint: disable=subprocess-run-check
77
61
 
78
62
 
79
63
  def print_node_package_version() -> None:
80
- """
81
- Print the version of the Python packages.
82
- """
83
-
64
+ """Print the version of the Python packages."""
84
65
  subprocess.run(["npm", "list", "--global"]) # pylint: disable=subprocess-run-check
85
66
 
86
67
 
87
68
  def print_debian_package_version() -> None:
88
- """
89
- Print the version of the Python packages.
90
- """
91
-
69
+ """Print the version of the Python packages."""
92
70
  subprocess.run(["dpkg", "--list"]) # pylint: disable=subprocess-run-check
93
71
 
94
72
 
95
73
  def print_environment(config: c2cciutils.configuration.Configuration, prefix: str = "Print ") -> None:
96
74
  """Print the GitHub environment information."""
97
-
98
75
  functions = [
99
76
  (
100
77
  "version",
c2cciutils/lib/docker.py CHANGED
@@ -1,6 +1,4 @@
1
- """
2
- Some utility functions for Docker images.
3
- """
1
+ """Some utility functions for Docker images."""
4
2
 
5
3
  import os
6
4
  import subprocess # nosec: B404
@@ -26,7 +24,6 @@ def get_dpkg_packages_versions(
26
24
  Where `debian_11` corresponds on last path element for 'Debian 11'
27
25
  from https://repology.org/repositories/statistics
28
26
  """
29
-
30
27
  dpkg_configuration = c2cciutils.get_config().get("dpkg", {})
31
28
 
32
29
  os_release = {}
@@ -103,9 +100,7 @@ def get_dpkg_packages_versions(
103
100
 
104
101
 
105
102
  def get_versions_config() -> tuple[dict[str, dict[str, str]], bool]:
106
- """
107
- Get the versions from the config file.
108
- """
103
+ """Get the versions from the config file."""
109
104
  if os.path.exists("ci/dpkg-versions.yaml"):
110
105
  with open("ci/dpkg-versions.yaml", encoding="utf-8") as versions_file:
111
106
  return (
@@ -127,7 +122,6 @@ def check_versions(
127
122
  The versions of packages in the image should be present in the config file.
128
123
  The versions of packages in the image shouldn't be older than the versions of the config file.
129
124
  """
130
-
131
125
  result, versions_image = get_dpkg_packages_versions(image, default_distribution, default_release)
132
126
  if not result:
133
127
  return False
c2cciutils/lib/oidc.py ADDED
@@ -0,0 +1,188 @@
1
+ """
2
+ Manage OpenID Connect (OIDC) token exchange for external services.
3
+
4
+ Inspired by
5
+ https://github.com/pypa/gh-action-pypi-publish/blob/unstable/v1/oidc-exchange.py
6
+ """
7
+
8
+ import base64
9
+ import json
10
+ import os
11
+ import sys
12
+ from typing import NoReturn
13
+
14
+ import id as oidc_id
15
+ import requests
16
+
17
+
18
+ class _OidcError(Exception):
19
+ pass
20
+
21
+
22
+ def _fatal(message: str) -> NoReturn:
23
+ # HACK: GitHub Actions' annotations don't work across multiple lines naively;
24
+ # translating `\n` into `%0A` (i.e., HTML percent-encoding) is known to work.
25
+ # See: https://github.com/actions/toolkit/issues/193
26
+ message = message.replace("\n", "%0A")
27
+ print(f"::error::Trusted publishing exchange failure: {message}", file=sys.stderr)
28
+ raise _OidcError(message)
29
+
30
+
31
+ def _debug(message: str) -> None:
32
+ print(f"::debug::{message.title()}", file=sys.stderr)
33
+
34
+
35
+ def _render_claims(token: str) -> str:
36
+ _, payload, _ = token.split(".", 2)
37
+
38
+ # urlsafe_b64decode needs padding; JWT payloads don't contain any.
39
+ payload += "=" * (4 - (len(payload) % 4))
40
+ claims = json.loads(base64.urlsafe_b64decode(payload))
41
+
42
+ return f"""
43
+ The claims rendered below are **for debugging purposes only**. You should **not**
44
+ use them to configure a trusted publisher unless they already match your expectations.
45
+
46
+ If a claim is not present in the claim set, then it is rendered as `MISSING`.
47
+
48
+ * `sub`: `{claims.get('sub', 'MISSING')}`
49
+ * `repository`: `{claims.get('repository', 'MISSING')}`
50
+ * `repository_owner`: `{claims.get('repository_owner', 'MISSING')}`
51
+ * `repository_owner_id`: `{claims.get('repository_owner_id', 'MISSING')}`
52
+ * `job_workflow_ref`: `{claims.get('job_workflow_ref', 'MISSING')}`
53
+ * `ref`: `{claims.get('ref')}`
54
+
55
+ See https://docs.pypi.org/trusted-publishers/troubleshooting/ for more help.
56
+ """
57
+
58
+
59
+ def _get_token(hostname: str) -> str:
60
+ # Indices are expected to support `https://{hostname}/_/oidc/audience`,
61
+ # which tells OIDC exchange clients which audience to use.
62
+ audience_resp = requests.get(f"https://{hostname}/_/oidc/audience", timeout=5)
63
+ audience_resp.raise_for_status()
64
+
65
+ _debug(f"selected trusted publishing exchange endpoint: https://{hostname}/_/oidc/mint-token")
66
+
67
+ try:
68
+ oidc_token = oidc_id.detect_credential(audience=audience_resp.json()["audience"])
69
+ except oidc_id.IdentityError as identity_error:
70
+ _fatal(
71
+ f"""
72
+ OpenID Connect token retrieval failed: {identity_error}
73
+
74
+ This generally indicates a workflow configuration error, such as insufficient
75
+ permissions. Make sure that your workflow has `id-token: write` configured
76
+ at the job level, e.g.:
77
+
78
+ ```yaml
79
+ permissions:
80
+ id-token: write
81
+ ```
82
+
83
+ Learn more at https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings.
84
+ """
85
+ )
86
+
87
+ # Now we can do the actual token exchange.
88
+ mint_token_resp = requests.post(
89
+ f"https://{hostname}/_/oidc/mint-token",
90
+ json={"token": oidc_token},
91
+ timeout=5,
92
+ )
93
+
94
+ try:
95
+ mint_token_payload = mint_token_resp.json()
96
+ except requests.JSONDecodeError:
97
+ # Token exchange failure normally produces a JSON error response, but
98
+ # we might have hit a server error instead.
99
+ _fatal(
100
+ f"""
101
+ Token request failed: the index produced an unexpected
102
+ {mint_token_resp.status_code} response.
103
+
104
+ This strongly suggests a server configuration or downtime issue; wait
105
+ a few minutes and try again.
106
+
107
+ You can monitor PyPI's status here: https://status.python.org/
108
+ """ # noqa: E702
109
+ )
110
+
111
+ # On failure, the JSON response includes the list of errors that
112
+ # occurred during minting.
113
+ if not mint_token_resp.ok:
114
+ reasons = "\n".join(
115
+ f'* `{error["code"]}`: {error["description"]}'
116
+ for error in mint_token_payload["errors"] # noqa: W604
117
+ )
118
+
119
+ rendered_claims = _render_claims(oidc_token)
120
+
121
+ _fatal(
122
+ f"""
123
+ Token request failed: the server refused the request for the following reasons:
124
+
125
+ {reasons}
126
+
127
+ This generally indicates a trusted publisher configuration error, but could
128
+ also indicate an internal error on GitHub or PyPI's part.
129
+
130
+ {rendered_claims}
131
+ """
132
+ )
133
+
134
+ pypi_token = mint_token_payload.get("token")
135
+ if not isinstance(pypi_token, str):
136
+ _fatal(
137
+ """
138
+ Token response error: the index gave us an invalid response.
139
+
140
+ This strongly suggests a server configuration or downtime issue; wait
141
+ a few minutes and try again.
142
+ """
143
+ )
144
+
145
+ # Mask the newly minted PyPI token, so that we don't accidentally leak it in logs.
146
+ print(f"::add-mask::{pypi_token}")
147
+
148
+ # This final print will be captured by the subshell in `twine-upload.sh`.
149
+ return pypi_token
150
+
151
+
152
+ def pypi_login() -> None:
153
+ """
154
+ Connect to PyPI using OpenID Connect and mint a token for the user.
155
+
156
+ See Also
157
+ --------
158
+ - https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect
159
+ - https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-pypi
160
+
161
+ """
162
+ pypirc_filename = os.path.expanduser("~/.pypirc")
163
+
164
+ if os.path.exists(pypirc_filename):
165
+ print(f"::info::{pypirc_filename} already exists; consider as already logged in.") # noqa: E702
166
+ return
167
+
168
+ if "ACTIONS_ID_TOKEN_REQUEST_TOKEN" not in os.environ:
169
+ print(
170
+ """::error::Not available, you probably miss the permission `id-token: write`.
171
+ ```
172
+ permissions:
173
+ id-token: write
174
+ ```
175
+ See also: https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect"""
176
+ )
177
+ return
178
+
179
+ try:
180
+ token = _get_token("pypi.org")
181
+ with open(pypirc_filename, "w", encoding="utf-8") as pypirc_file:
182
+ pypirc_file.write("[pypi]\n")
183
+ pypirc_file.write("repository: https://upload.pypi.org/legacy/\n")
184
+ pypirc_file.write("username: __token__\n")
185
+ pypirc_file.write(f"password: {token}\n")
186
+ except _OidcError:
187
+ # Already visible in logs; no need to re-raise.
188
+ return