Merge ~ubuntu-docker-images/ubuntu-docker-images/+git/templates:add_script_for_ubuntu into ~ubuntu-docker-images/ubuntu-docker-images/+git/templates:main

Proposed by Samir Akarioh
Status: Merged
Merged at revision: 0c01c112996781987bd31c7b543abffc01fe3226
Proposed branch: ~ubuntu-docker-images/ubuntu-docker-images/+git/templates:add_script_for_ubuntu
Merge into: ~ubuntu-docker-images/ubuntu-docker-images/+git/templates:main
Diff against target: 587 lines (+557/-1)
3 files modified
.devcontainer/Dockerfile (+1/-1)
README.md (+31/-0)
generate_ubuntu_yaml.py (+525/-0)
Reviewer Review Type Date Requested Status
Cristovao Cordeiro Approve
Samir Akarioh (community) Approve
Review via email: mp+431915@code.launchpad.net

Commit message

feat : add Ubuntu yaml file generator

This commit add a script which permit to generate
the ubuntu yaml file

Co-Signed-Off: Samir Akarioh and Cristovao Cordeiro

To post a comment you must log in.
Revision history for this message
Cristovao Cordeiro (cjdc) wrote :

I guess PyYaml needs to be installed. If so, please adjust the readme and .devcontainer accordingly

review: Needs Fixing
Revision history for this message
Samir Akarioh (samiraka) wrote :

by .devcontainer you want to says the dockerfile ?

Revision history for this message
Cristovao Cordeiro (cjdc) wrote :

> by .devcontainer you want to says the dockerfile ?

yes

Revision history for this message
Cristovao Cordeiro (cjdc) wrote :

please make the script executable

review: Needs Fixing
Revision history for this message
Samir Akarioh (samiraka) wrote :
Revision history for this message
Samir Akarioh (samiraka) wrote :

> please make the script executable
What do you mean by that?

Revision history for this message
Cristovao Cordeiro (cjdc) wrote :

please format the file with black and isort and flake8

review: Needs Fixing
Revision history for this message
Samir Akarioh (samiraka) wrote :

Done

Revision history for this message
Cristovao Cordeiro (cjdc) :
review: Needs Fixing
Revision history for this message
Cristovao Cordeiro (cjdc) wrote (last edit ):

> no need it's already install with renderdown :
> https://github.com/ValentinViennot/RenderDown/blob/master/requirements.txt

and so is boto3

Revision history for this message
Samir Akarioh (samiraka) wrote :

it's better to do a MP on github to add boto3 no ?

Revision history for this message
Cristovao Cordeiro (cjdc) wrote :

> it's better to do a MP on github to add boto3 no ?

does renderdown need it?

Revision history for this message
Samir Akarioh (samiraka) wrote :

no

Revision history for this message
Cristovao Cordeiro (cjdc) wrote :

> no

then it shouldn't go there. let's update the README and .devcontainer in this project instead

Revision history for this message
Samir Akarioh (samiraka) wrote :

i updated it

Revision history for this message
Cristovao Cordeiro (cjdc) wrote :
review: Needs Fixing
Revision history for this message
Samir Akarioh (samiraka) wrote :

Done

Revision history for this message
Samir Akarioh (samiraka) :
review: Needs Fixing
Revision history for this message
Samir Akarioh (samiraka) wrote :

I review your script, you want me to do the correction or ?

Revision history for this message
Cristovao Cordeiro (cjdc) wrote :

thanks. fixed

Revision history for this message
Samir Akarioh (samiraka) :
review: Needs Information
Revision history for this message
Cristovao Cordeiro (cjdc) :
Revision history for this message
Samir Akarioh (samiraka) :
review: Approve
Revision history for this message
Cristovao Cordeiro (cjdc) :
review: Approve
Revision history for this message
Cristovao Cordeiro (cjdc) wrote :

I think you can merge it now Samir

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index fa24741..3c8c2b3 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -6,6 +6,6 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
6 && apt-get -y install --no-install-recommends python3-mako python3-yaml6 && apt-get -y install --no-install-recommends python3-mako python3-yaml
77
8RUN cd /usr/bin && git clone https://github.com/valentinviennot/RenderDown \8RUN cd /usr/bin && git clone https://github.com/valentinviennot/RenderDown \
9 && pip3 --disable-pip-version-check --no-cache-dir install -r /usr/bin/RenderDown/requirements.txt \ 9 && pip3 --disable-pip-version-check --no-cache-dir install -r /usr/bin/RenderDown/requirements.txt boto3 \
10 && rm -rf /tmp/pip-tmp10 && rm -rf /tmp/pip-tmp
11ENV RENDERDOWN /usr/bin/RenderDown/renderdown.py11ENV RENDERDOWN /usr/bin/RenderDown/renderdown.py
diff --git a/README.md b/README.md
index 0166dea..29d4e38 100644
--- a/README.md
+++ b/README.md
@@ -15,8 +15,39 @@ The DevContainer will provide you with a working environment out of the box. **Y
15```bash15```bash
16git clone https://github.com/misterw97/RenderDown16git clone https://github.com/misterw97/RenderDown
17sudo apt update && sudo apt install -y python3-mako python3-yaml17sudo apt update && sudo apt install -y python3-mako python3-yaml
18pip install boto3 # if you want to run the generate_ubuntu_yaml file
18```19```
1920
21#### Generate_ubuntu_yaml
22
23This script allows to generate the ubuntu.yaml file in order to use it by the RenderDown script. It uses the template ubuntu.yaml located in the template folder.
24
25Here are the available arguments and examples of commands:
26
27```
28usage: generate_ubuntu_yaml.py [-h] [--provider PROVIDER] [--username USERNAME] [--password PASSWORD] [--data-dir DATA_DIR] [--unpublished-suite UNPUBLISHED_SUITE]
29 [--unpublished-tags UNPUBLISHED_TAGS] [--unpublished-archs UNPUBLISHED_ARCHS]
30
31Generate documentation about Ubuntu for ECR and DockerHub
32
33options:
34 -h, --help show this help message and exit
35 --provider PROVIDER aws or docker
36 --username USERNAME Username of provider
37 --password PASSWORD Password of provider
38 --data-dir DATA_DIR Where you will find the output template (folder), If it does not exist then it is created
39 --unpublished-suite UNPUBLISHED_SUITE
40 an Ubuntu Suite (e.g. jammy).
41 --unpublished-tags UNPUBLISHED_TAGS
42 list of tags separated by comma (e.g. 'kinetic 22.10 22.10_edge',kinetic)
43 --unpublished-archs UNPUBLISHED_ARCHS
44 list of archs separated by comma (e.g amd64,arm)
45```
46
47Example: `./generate_ubuntu_yaml.py --provider docker --username admin --password admin`
48
49If you give the --unpublished-suite argument you need to give --unpublished-tags and --unpublished-archs. When these options are given, the script will take your information into the YAML file without checking the registry.
50
20## Running51## Running
2152
22Create README files for all registries and namespaces:53Create README files for all registries and namespaces:
diff --git a/generate_ubuntu_yaml.py b/generate_ubuntu_yaml.py
23new file mode 10075554new file mode 100755
index 0000000..b7ca361
--- /dev/null
+++ b/generate_ubuntu_yaml.py
@@ -0,0 +1,525 @@
1#!/usr/bin/env python3
2
3import argparse
4import datetime
5import json
6import logging
7import os
8import subprocess
9from typing import Dict, List
10
11import boto3
12import requests
13import sys
14import yaml
15
16logging.basicConfig(stream=sys.stdout, level=logging.INFO)
17NOW = datetime.datetime.now()
18SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
19
20
21def cli_args() -> argparse.ArgumentParser:
22 """Argument parser"""
23 parser = argparse.ArgumentParser(
24 description="Generate documentation about Ubuntu for ECR and DockerHub"
25 )
26
27 parser.add_argument(
28 "--provider",
29 default="docker",
30 dest="provider",
31 help="aws or docker",
32 required=True,
33 )
34 parser.add_argument(
35 "--username",
36 default="admin",
37 dest="username",
38 help="either the Docker Hub username, or the AWS access key ID",
39 required=True,
40 )
41 parser.add_argument(
42 "--password",
43 default="admin",
44 dest="password",
45 help="either the Docker Hub password/token, or the AWS secret access key",
46 required=True,
47 )
48 parser.add_argument(
49 "--token-docker",
50 dest="dockertoken",
51 default=None,
52 help="JWT token for Docker Hub authentication. \
53 Only useful for the 'docker' provider.",
54 )
55 parser.add_argument(
56 "--repository-basename",
57 dest="repository",
58 default=None,
59 help="repository basename of the ubuntu images. \
60 Used to infer existing information.",
61 )
62 parser.add_argument(
63 "--data-dir",
64 default="data",
65 dest="data_dir",
66 help="""The path of the folder
67 where the data file will be
68 saved ( if not exist, the script
69 will create the folder)""",
70 )
71 parser.add_argument(
72 "--unpublished-suite",
73 dest="unpublished_suite",
74 help="""an Ubuntu Suite (e.g. jammy).
75 if given we will take the
76 tags pass on command lines (required)
77 and the arches for this section
78 of the yaml file.
79 """,
80 )
81 parser.add_argument(
82 "--unpublished-tags",
83 dest="unpublished_tags",
84 help="""list of tags
85 (e.g. 'kinetic 22.10 22.10_edge kinetic)""",
86 )
87 parser.add_argument(
88 "--unpublished-archs",
89 dest="unpublished_archs",
90 help="list of archs (e.g amd64 arm)",
91 )
92
93 return parser
94
95
96def validate_args(
97 parser: argparse.ArgumentParser,
98) -> argparse.ArgumentParser.parse_args:
99 """Parse and validate the CLI arguments"""
100 args = parser.parse_args()
101 if any(
102 [
103 args.unpublished_suite is None,
104 args.unpublished_tags is None,
105 args.unpublished_archs is None,
106 ]
107 ) and not all(
108 [
109 args.unpublished_suite is None,
110 args.unpublished_tags is None,
111 args.unpublished_archs is None,
112 ]
113 ):
114 parser.error(
115 """--unpublished-suite need
116 --unpublished-archs and --unpublished_tags"""
117 )
118
119 return args
120
121
122def build_image_endpoint(provider: str, repo_base: str = None) -> (str, str):
123 """Define the image's registry URL"""
124 if provider == "aws":
125 registry_url = "docker://public.ecr.aws/"
126 staging_repo = "rocksdev"
127 else:
128 registry_url = "docker://docker.io/"
129 staging_repo = "rocksdev4staging"
130
131 if repo_base is None:
132 logging.warning("Using staging repository")
133 url = f"{registry_url}{staging_repo}/ubuntu"
134 namespace = staging_repo
135 else:
136 url = f"{registry_url}{repo_base}/ubuntu"
137 namespace = repo_base
138
139 logging.info(f"Using {url} to collect information")
140
141 return url, namespace
142
143
144def add_yaml_representer():
145 def str_presenter(dumper, data):
146 """
147 Permit to format
148 multiline string into
149 yaml file
150 """
151
152 c = "tag:yaml.org,2002:str"
153 if len(data.splitlines()) > 1: # check for multiline string
154 return dumper.represent_scalar(c, data, style="|")
155 return dumper.represent_scalar(c, data)
156
157 yaml.add_representer(str, str_presenter)
158 yaml.representer.SafeRepresenter.add_representer(str, str_presenter)
159
160
161def _process_run(command: List[str], **kwargs) -> str:
162 """Run a command and handle its output."""
163 logging.info(f"Execute process: {command!r}, kwargs={kwargs!r}")
164 try:
165 out = subprocess.run(
166 command,
167 **kwargs,
168 capture_output=True,
169 check=True,
170 universal_newlines=True,
171 )
172 except subprocess.CalledProcessError as err:
173 msg = f"Failed to run command: {err!s}"
174 if err.stderr:
175 msg += f" ({err.stderr.strip()!s})"
176 raise Exception(msg) from err
177
178 return out.stdout.strip()
179
180
181def get_arches(release: str, image_url: str) -> List[str]:
182 """
183 Permit to get the arches associated to the release
184 """
185 logging.info(f"Getting the arches for {release}")
186 command = ["skopeo", "inspect", f"{image_url}:{release}", "--raw"]
187 manifest = json.loads(_process_run(command))["manifests"]
188 arches = []
189 for arch in manifest:
190 arches.append(arch["platform"]["architecture"])
191 return arches
192
193
194def get_dockerhub_token(username: str, password: str) -> str:
195 """
196 Permit to get the token associated to the docker account
197 """
198 logging.info("Getting the token form Docker")
199
200 url_token = "https://hub.docker.com/v2/users/login"
201 data = {"username": username, "password": password}
202 get_token = requests.post(url_token, json=data)
203 get_token.raise_for_status()
204 return get_token.json()["token"]
205
206
207def get_tags_docker(
208 release: str, token: str, image_url: str, image_namespace: str
209) -> List[str]:
210 """
211 Permit to get the tags associated to the release
212 """
213 logging.info(f"Getting the tags from Docker for {release}")
214 tags = []
215 command = [
216 "skopeo",
217 "inspect",
218 f"{image_url}:{release}",
219 "--raw",
220 ]
221 result_json = _process_run(command)
222 digest = json.loads(result_json)["manifests"][0]["digest"]
223
224 url_dockerhub = "https://hub.docker.com/v2/repositories/"
225 url_dockerhub += f"{image_namespace}/ubuntu/tags/?page_size=999"
226 Headers = {"Authorization": f"JWT {token}"}
227 get_the_tags = requests.get(url_dockerhub, headers=Headers)
228 get_the_tags = get_the_tags.json()["results"]
229 for image in get_the_tags:
230 for info_image in image["images"]:
231 if info_image["digest"] == digest and image["name"] not in tags:
232 tags.append(image["name"])
233
234 return tags
235
236
237def get_tags_aws(release: str, client: boto3.Session, image_url: str) -> List[str]:
238 """
239 Permit to get the tags associated to the release
240 """
241 logging.info(f"Getting the tags from AWS for {release}")
242
243 tags = []
244 command = [
245 "skopeo",
246 "inspect",
247 f"{image_url}:{release}",
248 ]
249 result_json = _process_run(command)
250 digest = json.loads(result_json)["Digest"]
251 response = client.describe_image_tags(repositoryName="ubuntu")
252
253 for image in response["imageTagDetails"]:
254 if (
255 image["imageDetail"]["imageDigest"] == digest
256 and image["imageTag"] not in tags
257 ):
258 tags.append(image["imageTag"])
259 return tags
260
261
262def get_fullname(release: str) -> str:
263 """
264 Permit to get the full name associated to the release
265 """
266 logging.info(f"Getting full name of {release} ")
267
268 command = ["ubuntu-distro-info", f"--series={release}", "-f"]
269 result_json = _process_run(command)
270 return result_json.replace("Ubuntu", "").strip()
271
272
273def get_support(series: str, is_lts: bool) -> Dict[str, Dict[str, str]]:
274 """Calculates the end of support dates for a given Ubuntu series"""
275 logging.info(f"Getting support information for the {series}")
276
277 base_cmd = ["ubuntu-distro-info", "--series", series]
278 eol_cmd = base_cmd + ["--day=eol"]
279
280 eol = int(_process_run(eol_cmd))
281 eol_date = NOW + datetime.timedelta(days=eol)
282
283 support = {"support": {"until": f"{eol_date.month:02d}/{eol_date.year}"}}
284
285 if not is_lts:
286 return support
287
288 # The it is LTS, and lts_until=until
289 support["support"]["lts_until"] = support["support"]["until"]
290
291 eol_esm_cmd = base_cmd + ["--day=eol-esm"]
292
293 eol_esm = int(_process_run(eol_esm_cmd))
294 eol_esm_date = NOW + datetime.timedelta(days=eol_esm)
295 eol_esm_value = f"{eol_esm_date.month:02d}/{eol_esm_date.year}"
296 support["support"]["esm_until"] = eol_esm_value
297
298 return support
299
300
301def get_deprecated(series: str) -> Dict[str, Dict[str, object]]:
302 """
303 Calculated the deprecation date
304 and upgrade path for a deprecated release
305 """
306 logging.info(f"Getting support information for the {series}")
307
308 eol_cmd = ["ubuntu-distro-info", "--series", series, "--day=eol"]
309
310 eol = int(_process_run(eol_cmd))
311 eol_date = NOW + datetime.timedelta(days=eol)
312 # For now, the upgrade path is always the next release
313
314 this_release_cmd = ["ubuntu-distro-info", "--series", series, "--day=release"]
315 this_release = int(_process_run(this_release_cmd))
316 # add 60 days to the release date, to get the next development version
317 next_date = NOW + datetime.timedelta(days=this_release + 60)
318
319 following_dev_series_cmd = [
320 "ubuntu-distro-info",
321 "-d",
322 f"--date={next_date.year}-{next_date.month}-{next_date.day}",
323 ]
324 development_suite_at_eol = _process_run(following_dev_series_cmd)
325
326 upgrade_path_cmd = [
327 "ubuntu-distro-info",
328 "--series",
329 development_suite_at_eol,
330 "-r",
331 ]
332 upgrade_path = _process_run(upgrade_path_cmd).strip(" LTS")
333
334 return {
335 "deprecated": {
336 "date": f"{eol_date.month:02d}/{eol_date.year}",
337 "path": {"track": upgrade_path},
338 }
339 }
340
341
342def is_deprecated(series: str) -> bool:
343
344 """Checks whether a series is completely deprecated (both LTS and ESM)"""
345 logging.info(f"Checking is {series} is deprecated")
346 supported_cmd = "ubuntu-distro-info --supported"
347 supported_esm_cmd = supported_cmd + "-esm"
348 all_supported = _process_run(supported_cmd.split(" ")) + _process_run(
349 supported_esm_cmd.split(" ")
350 )
351 return series not in all_supported
352
353
354def is_lts(series: str) -> bool:
355
356 """Checks if a given series is LTS"""
357 logging.info(f"Checking is {series} is lts")
358
359 cmd = ["ubuntu-distro-info", "--series", series, "-f"]
360
361 return "LTS" in _process_run(cmd)
362
363
364def get_lowest_risk(tags: List[str]) -> str:
365 """
366 Get the lowest risk associated with the release
367 """
368 risk_sorted = ["stable", "candidate", "beta", "edge"]
369
370 all_tags_str = " ".join(tags)
371 for risk in risk_sorted:
372 if risk in all_tags_str:
373 return risk
374
375 return "edge"
376
377
378def get_release(series: str) -> str:
379 command = ["ubuntu-distro-info", f"--series={series}", "-r"]
380
381 return _process_run(command)
382
383
384def infer_registry_user(
385 provider: str, username: str, password: str, dh_token: str = None
386) -> object:
387 user = None
388 if provider == "aws":
389 logging.info("Connecting to AWS")
390 session = boto3.Session(
391 region_name="us-east-1",
392 aws_access_key_id=username,
393 aws_secret_access_key=password,
394 )
395 user = session.client("ecr-public")
396 else:
397 logging.info("Fetching Docker Hub token")
398 if dh_token:
399 user = dh_token
400 else:
401 user = get_dockerhub_token(username, password)
402
403 return user
404
405
406def build_releases_data(
407 list_of_series: List[str],
408 all_tags: List[str],
409 image_url: str,
410 image_ns: str,
411 arguments: argparse.ArgumentParser.parse_args,
412 registry_user: object,
413) -> Dict:
414 """Build the releases info data structure"""
415 releases = []
416 for count, series in enumerate(list_of_series):
417 if series not in all_tags and series != arguments.unpublished_suite:
418 logging.warning(
419 f"Series {series} does not exist in {image_url}. Skipping it..."
420 )
421 continue
422
423 release_data = {}
424
425 release = get_release(series)
426 if "LTS" in release:
427 release_data["type"] = "LTS"
428
429 release_data["track"] = release.rstrip(" LTS")
430
431 if arguments.unpublished_suite and arguments.unpublished_suite == series:
432 release_data["architectures"] = arguments.unpublished_archs.split()
433 release_data["version"] = get_fullname(arguments.unpublished_suite)
434 release_data["risk"] = get_lowest_risk(arguments.unpublished_tags.split())
435 release_data["tags"] = arguments.unpublished_tags.split()
436 else:
437 release_data["architectures"] = get_arches(series, image_url)
438 release_data["version"] = get_fullname(series)
439 if arguments.provider == "docker":
440 release_data["tags"] = get_tags_docker(
441 series, registry_user, image_url, image_ns
442 )
443 else:
444 release_data["tags"] = get_tags_aws(series, registry_user)
445 release_data["risk"] = get_lowest_risk(release_data["tags"])
446
447 if is_deprecated(series):
448 release_data["deprecated"] = get_deprecated(series)
449 else:
450 release_data["support"] = get_support(series, is_lts(series))
451
452 releases.append(release_data)
453
454 return releases
455
456
457def read_ubuntu_data_template() -> Dict:
458 """Reads and parses the YAML contents of the data template"""
459 template_file = f"{SCRIPT_DIR}/templates/ubuntu.yaml"
460 logging.info(f"Opening the template file {template_file}")
461 with open(template_file) as file:
462 try:
463 return yaml.safe_load(file)
464 except yaml.YAMLError as exc:
465 logging.error("Error when loading the ubuntu template file")
466 raise exc
467
468
469def create_data_dir(path: str):
470 """Create data dir if it doesn't exist"""
471 if not os.path.exists(path):
472 logging.info(f"Creating the {path} folder")
473
474 os.makedirs(path)
475
476
477def write_ubuntu_data_file(file_path: str, content: Dict):
478 """Write the YAML content into the ubuntu file path"""
479 with open(file_path, "w") as file:
480 logging.info(f"Create the yaml file {file_path}")
481 yaml.dump(content, file)
482
483
484def main():
485 arguments = validate_args(cli_args())
486 registry_user = infer_registry_user(
487 arguments.provider,
488 arguments.username,
489 arguments.password,
490 arguments.dockertoken,
491 )
492
493 add_yaml_representer()
494 url, ns = build_image_endpoint(arguments.provider, repo_base=arguments.repository)
495
496 logging.info(f"Getting all tags from {url}")
497 command_tags = ["skopeo", "list-tags", url]
498 existing_tags = json.loads(_process_run(command_tags))["Tags"]
499
500 logging.info("Getting all the series from ubuntu-distro-info")
501 command_suites = ["ubuntu-distro-info", "--all"]
502 series_names = _process_run(command_suites).split("\n")
503
504 if arguments.unpublished_suite and arguments.unpublished_suite not in series_names:
505 logging.error(
506 f"The provided unpublished suite {arguments.unpublished_suite}"
507 "is not recognized. Ignoring it"
508 )
509
510 logging.info("Building releases info")
511 releases = build_releases_data(
512 series_names, existing_tags, url, ns, arguments, registry_user
513 )
514
515 dict_file = read_ubuntu_data_template()
516 dict_file["releases"] = releases
517
518 create_data_dir(arguments.data_dir)
519
520 ubuntu_data_file = f"{arguments.data_dir}/ubuntu.yaml"
521 write_ubuntu_data_file(ubuntu_data_file, dict_file)
522
523
524if __name__ == "__main__":
525 main()

Subscribers

People subscribed via source and target branches

to all changes: