c2cciutils 1.8.0.dev64__py3-none-any.whl → 1.8.0.dev68__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.
- c2cciutils/__init__.py +1 -194
- c2cciutils/applications-versions.yaml +0 -1
- c2cciutils/configuration.py +1 -598
- c2cciutils/schema.json +2 -317
- {c2cciutils-1.8.0.dev64.dist-info → c2cciutils-1.8.0.dev68.dist-info}/METADATA +1 -219
- c2cciutils-1.8.0.dev68.dist-info/RECORD +25 -0
- c2cciutils-1.8.0.dev68.dist-info/entry_points.txt +9 -0
- c2cciutils/lib/docker.py +0 -141
- c2cciutils/lib/oidc.py +0 -188
- c2cciutils/package-lock.json +0 -370
- c2cciutils/package.json +0 -9
- c2cciutils/publish.py +0 -451
- c2cciutils/schema-applications.json +0 -50
- c2cciutils/scripts/clean.py +0 -103
- c2cciutils/scripts/docker_versions_gen.py +0 -33
- c2cciutils/scripts/pin_pipenv.py +0 -54
- c2cciutils/scripts/publish.py +0 -477
- c2cciutils/scripts/trigger_image_update.py +0 -84
- c2cciutils/scripts/version.py +0 -245
- c2cciutils-1.8.0.dev64.dist-info/RECORD +0 -37
- c2cciutils-1.8.0.dev64.dist-info/entry_points.txt +0 -18
- {c2cciutils-1.8.0.dev64.dist-info → c2cciutils-1.8.0.dev68.dist-info}/LICENSE +0 -0
- {c2cciutils-1.8.0.dev64.dist-info → c2cciutils-1.8.0.dev68.dist-info}/WHEEL +0 -0
c2cciutils/lib/docker.py
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
"""Some utility functions for Docker images."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import subprocess # nosec: B404
|
|
5
|
-
from typing import Optional, cast
|
|
6
|
-
|
|
7
|
-
import yaml
|
|
8
|
-
from debian_inspector.version import Version
|
|
9
|
-
|
|
10
|
-
import c2cciutils.configuration
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def get_dpkg_packages_versions(
|
|
14
|
-
image: str,
|
|
15
|
-
default_distribution: Optional[str] = None,
|
|
16
|
-
default_release: Optional[str] = None,
|
|
17
|
-
) -> tuple[bool, dict[str, Version]]:
|
|
18
|
-
"""
|
|
19
|
-
Get the versions of the dpkg packages installed in the image.
|
|
20
|
-
|
|
21
|
-
`get_dpkg_packages_versions("org/image:tag")` will return something like:
|
|
22
|
-
(true, {"debian_11/api": "2.2.0", ...})
|
|
23
|
-
|
|
24
|
-
Where `debian_11` corresponds on last path element for 'Debian 11'
|
|
25
|
-
from https://repology.org/repositories/statistics
|
|
26
|
-
"""
|
|
27
|
-
dpkg_configuration = c2cciutils.get_config().get("dpkg", {})
|
|
28
|
-
|
|
29
|
-
os_release = {}
|
|
30
|
-
try:
|
|
31
|
-
os_release_process = subprocess.run(
|
|
32
|
-
["docker", "run", "--rm", "--entrypoint=", image, "cat", "/etc/os-release"],
|
|
33
|
-
stdout=subprocess.PIPE,
|
|
34
|
-
check=True,
|
|
35
|
-
)
|
|
36
|
-
os_release = dict([e.split("=") for e in os_release_process.stdout.decode().split("\n") if e])
|
|
37
|
-
except subprocess.CalledProcessError:
|
|
38
|
-
print("Info: /etc/os-release not found in the image")
|
|
39
|
-
|
|
40
|
-
lsb_release = {}
|
|
41
|
-
try:
|
|
42
|
-
lsb_release_process = subprocess.run(
|
|
43
|
-
["docker", "run", "--rm", "--entrypoint=", image, "cat", "/etc/lsb-release"],
|
|
44
|
-
stdout=subprocess.PIPE,
|
|
45
|
-
check=True,
|
|
46
|
-
)
|
|
47
|
-
lsb_release = dict([e.split("=") for e in lsb_release_process.stdout.decode().split("\n") if e])
|
|
48
|
-
except subprocess.CalledProcessError:
|
|
49
|
-
print("Info: /etc/lsb-release not found in the image")
|
|
50
|
-
|
|
51
|
-
distribution = os_release.get("ID", lsb_release.get("DISTRIB_ID", default_distribution))
|
|
52
|
-
release = os_release.get("VERSION_ID", lsb_release.get("DISTRIB_RELEASE", default_release))
|
|
53
|
-
if distribution is None:
|
|
54
|
-
print("Could not get the distribution of the image, you should provide a default distribution")
|
|
55
|
-
return False, {}
|
|
56
|
-
if release is None:
|
|
57
|
-
print("Could not get the release of the image, you should provide a default release")
|
|
58
|
-
return False, {}
|
|
59
|
-
|
|
60
|
-
distribution_final = distribution.strip('"').lower()
|
|
61
|
-
release_final = release.strip('"').replace(".", "_")
|
|
62
|
-
prefix = f"{distribution_final}_{release_final}/"
|
|
63
|
-
print(f"Found distribution '{distribution_final}', release '{release_final}'.")
|
|
64
|
-
|
|
65
|
-
if distribution_final == "ubuntu" and release_final == "18_04":
|
|
66
|
-
print("Warning: Ubuntu 18.04 is not supported")
|
|
67
|
-
return False, {}
|
|
68
|
-
|
|
69
|
-
package_version: dict[str, Version] = {}
|
|
70
|
-
packages_status_process = subprocess.run(
|
|
71
|
-
["docker", "run", "--rm", "--entrypoint=", image, "dpkg", "--status"],
|
|
72
|
-
stdout=subprocess.PIPE,
|
|
73
|
-
check=True,
|
|
74
|
-
)
|
|
75
|
-
packages_status_1 = packages_status_process.stdout.decode().split("\n")
|
|
76
|
-
packages_status_2 = [e.split(": ", maxsplit=1) for e in packages_status_1]
|
|
77
|
-
packages_status = [e for e in packages_status_2 if len(e) == 2]
|
|
78
|
-
package = None
|
|
79
|
-
version = None
|
|
80
|
-
for name, value in packages_status:
|
|
81
|
-
if name == "Package":
|
|
82
|
-
if package is not None:
|
|
83
|
-
if version is None:
|
|
84
|
-
print(f"Error: Missing version for package {package}")
|
|
85
|
-
else:
|
|
86
|
-
if package not in dpkg_configuration.get("ignored_packages", []):
|
|
87
|
-
package = dpkg_configuration.get("packages_mapping", {}).get(package, package)
|
|
88
|
-
if package in package_version and version != package_version[package]:
|
|
89
|
-
print(
|
|
90
|
-
f"The package {package} has different version ({package_version[package]} != {version})"
|
|
91
|
-
)
|
|
92
|
-
if package not in ("base-files",):
|
|
93
|
-
package_version[package] = version
|
|
94
|
-
package = value
|
|
95
|
-
version = None
|
|
96
|
-
if name == "Version" and version is None:
|
|
97
|
-
version = Version.from_string(value)
|
|
98
|
-
|
|
99
|
-
return True, {f"{prefix}{k}": v for k, v in package_version.items()}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def get_versions_config() -> tuple[dict[str, dict[str, str]], bool]:
|
|
103
|
-
"""Get the versions from the config file."""
|
|
104
|
-
if os.path.exists("ci/dpkg-versions.yaml"):
|
|
105
|
-
with open("ci/dpkg-versions.yaml", encoding="utf-8") as versions_file:
|
|
106
|
-
return (
|
|
107
|
-
cast(dict[str, dict[str, str]], yaml.load(versions_file.read(), Loader=yaml.SafeLoader)),
|
|
108
|
-
True,
|
|
109
|
-
)
|
|
110
|
-
return {}, False
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def check_versions(
|
|
114
|
-
versions_config: dict[str, str],
|
|
115
|
-
image: str,
|
|
116
|
-
default_distribution: Optional[str] = None,
|
|
117
|
-
default_release: Optional[str] = None,
|
|
118
|
-
) -> bool:
|
|
119
|
-
"""
|
|
120
|
-
Check if the versions are correct.
|
|
121
|
-
|
|
122
|
-
The versions of packages in the image should be present in the config file.
|
|
123
|
-
The versions of packages in the image shouldn't be older than the versions of the config file.
|
|
124
|
-
"""
|
|
125
|
-
result, versions_image = get_dpkg_packages_versions(image, default_distribution, default_release)
|
|
126
|
-
if not result:
|
|
127
|
-
return False
|
|
128
|
-
|
|
129
|
-
success = True
|
|
130
|
-
for package, version in versions_image.items():
|
|
131
|
-
if package not in versions_config:
|
|
132
|
-
print(f"Package {package} with version {version} is not in the config file for the image {image}")
|
|
133
|
-
success = False
|
|
134
|
-
elif Version.from_string(versions_config[package]) > version:
|
|
135
|
-
print(
|
|
136
|
-
f"Package {package} is older than the config file for the image {image}: "
|
|
137
|
-
f"{versions_config[package]} > {version}."
|
|
138
|
-
)
|
|
139
|
-
success = False
|
|
140
|
-
|
|
141
|
-
return success
|
c2cciutils/lib/oidc.py
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
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
|