Skip to content

Commit 1e4b622

Browse files
committed
feat: generate image catalogs for bake images
Signed-off-by: Niccolò Fei <niccolo.fei@enterprisedb.com>
1 parent 75d5e0f commit 1e4b622

2 files changed

Lines changed: 208 additions & 0 deletions

File tree

.github/catalogs_generator.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
#
2+
# Copyright © contributors to CloudNativePG, established as
3+
# CloudNativePG a Series of LF Projects, LLC.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
# SPDX-License-Identifier: Apache-2.0
18+
#
19+
20+
import argparse
21+
import re
22+
import json
23+
import os
24+
import time
25+
import yaml
26+
import urllib.request
27+
from packaging import version
28+
from subprocess import check_output
29+
30+
min_supported_major = 13
31+
32+
repo_name = "cloudnative-pg/postgresql"
33+
full_repo_name = f"ghcr.io/{repo_name}"
34+
pg_regexp = r"(\d+)(?:\.\d+|beta\d+|rc\d+|alpha\d+)-(\d{12})"
35+
_token_cache = {"value": None, "expires_at": 0}
36+
37+
38+
def get_json(image_name):
39+
data = check_output([
40+
"docker",
41+
"run",
42+
"--rm",
43+
"quay.io/skopeo/stable",
44+
"list-tags",
45+
f"docker://{image_name}"
46+
])
47+
repo_json = json.loads(data.decode("utf-8"))
48+
return repo_json
49+
50+
51+
def get_token(repository_name):
52+
global _token_cache
53+
now = time.time()
54+
55+
if _token_cache["value"] and now < _token_cache["expires_at"]:
56+
return _token_cache["value"]
57+
58+
url = "https://ghcr.io/token?scope=repository:{}:pull".format(repository_name)
59+
with urllib.request.urlopen(url) as response:
60+
data = json.load(response)
61+
token = data["token"]
62+
63+
_token_cache["value"] = token
64+
_token_cache["expires_at"] = now + 300
65+
return token
66+
67+
68+
def get_digest(repository_name, tag):
69+
token = get_token(repository_name)
70+
media_types = [
71+
"application/vnd.oci.image.index.v1+json",
72+
"application/vnd.oci.image.manifest.v1+json",
73+
"application/vnd.docker.distribution.manifest.v2+json",
74+
]
75+
url = f"https://ghcr.io/v2/{repository_name}/manifests/{tag}"
76+
req = urllib.request.Request(url)
77+
req.add_header("Authorization", "Bearer {}".format(token))
78+
req.add_header("Accept", ",".join(media_types))
79+
with urllib.request.urlopen(req) as response:
80+
digest = response.headers.get("Docker-Content-Digest")
81+
return digest
82+
83+
84+
def write_catalog(tags, version_re, suffix, output_dir="."):
85+
version_re = re.compile(rf"^{version_re}{re.escape(suffix)}$")
86+
87+
# Filter out all the tags which do not match the version regexp
88+
tags = [item for item in tags if version_re.search(item)]
89+
90+
# Sort the tags according to semantic versioning
91+
tags.sort(
92+
key=lambda v: version.Version(v.removesuffix(suffix)),
93+
reverse=True
94+
)
95+
96+
results = {}
97+
for item in tags:
98+
match = version_re.search(item)
99+
if not match:
100+
continue
101+
102+
major = match.group(1)
103+
104+
# Skip too old versions
105+
if int(major) < min_supported_major:
106+
continue
107+
108+
if major not in results:
109+
digest = get_digest(repo_name, item)
110+
results[major] = [f"{full_repo_name}:{item}@{digest}"]
111+
112+
catalog = {
113+
"apiVersion": "postgresql.cnpg.io/v1",
114+
"kind": "ClusterImageCatalog",
115+
"metadata": {"name": f"postgresql{suffix}"},
116+
"spec": {
117+
"images": [
118+
{"major": int(major), "image": images[0]} for major, images in sorted(results.items(), key=lambda x: int(x[0]))
119+
]
120+
}
121+
}
122+
123+
os.makedirs(output_dir, exist_ok=True)
124+
output_file = os.path.join(output_dir, f"catalog{suffix}.yaml")
125+
with open(output_file, "w") as f:
126+
yaml.dump(catalog, f, sort_keys=False)
127+
128+
129+
if __name__ == "__main__":
130+
parser = argparse.ArgumentParser(description="CloudNativePG ClusterImageCatalog YAML generator")
131+
parser.add_argument("--output-dir", default=".", help="Directory to save the YAML files")
132+
args = parser.parse_args()
133+
134+
repo_json = get_json(full_repo_name)
135+
tags = repo_json["Tags"]
136+
137+
for suffix in [
138+
"-minimal-bullseye",
139+
"-standard-bullseye",
140+
"-system-bullseye",
141+
"-minimal-bookworm",
142+
"-standard-bookworm",
143+
"-system-bookworm",
144+
"-minimal-trixie",
145+
"-standard-trixie",
146+
"-system-trixie",
147+
]:
148+
print(f"Generating catalog{suffix}.yaml")
149+
write_catalog(tags, pg_regexp, suffix, args.output_dir)

.github/workflows/catalogs.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: catalogs-generator
2+
3+
on:
4+
push:
5+
schedule:
6+
- cron: "30 0 * * *"
7+
workflow_dispatch:
8+
9+
permissions: read-all
10+
11+
defaults:
12+
run:
13+
shell: "bash -Eeuo pipefail -x {0}"
14+
15+
jobs:
16+
generate-catalogs:
17+
runs-on: ubuntu-24.04
18+
steps:
19+
- name: Checkout code
20+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
21+
with:
22+
path: postgres-containers
23+
24+
- name: Checkout artifacts
25+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
26+
with:
27+
path: artifacts
28+
repository: cloudnative-pg/artifacts
29+
token: ${{ secrets.REPO_GHA_PAT }}
30+
ref: main
31+
32+
- name: Set up Python
33+
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
34+
with:
35+
python-version: 3.13
36+
37+
- name: Install Python dependencies
38+
run: |
39+
pip install packaging==25.0 PyYAML==6.0.2
40+
41+
- name: Generate catalogs
42+
run: |
43+
python postgres-containers/.github/catalogs_generator.py --output-dir artifacts/image-catalogs/
44+
cat artifacts/image-catalogs/*.yaml
45+
46+
- name: Diff
47+
working-directory: artifacts
48+
run: |
49+
git status
50+
git diff
51+
52+
- uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9
53+
if: ${{ github.ref == 'refs/heads/main' }}
54+
with:
55+
cwd: 'artifacts'
56+
add: 'image-catalogs'
57+
author_name: CloudNativePG Automated Updates
58+
author_email: noreply@cnpg.com
59+
message: 'chore: update imageCatalogs'

0 commit comments

Comments
 (0)