Skip to content

Commit b20e75e

Browse files
NiccoloFeimnencia
andauthored
feat: generate image catalogs for bake images (#305)
Generate default cluster image catalogs for all combinations of types and OS versions, including all supported PostgreSQL versions. The catalogs also include predefined labels to easily identify the type, OS version, date, and origin of the catalog. Closes cloudnative-pg/artifacts#1 Signed-off-by: Niccolò Fei <niccolo.fei@enterprisedb.com> Signed-off-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com> Co-authored-by: Marco Nenciarini <marco.nenciarini@enterprisedb.com>
1 parent 75d5e0f commit b20e75e

3 files changed

Lines changed: 229 additions & 0 deletions

File tree

.github/catalogs_generator.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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+
supported_img_types = ["minimal", "standard", "system"]
31+
supported_os_names = ["bullseye", "bookworm", "trixie"]
32+
min_supported_major = 13
33+
34+
repo_name = "cloudnative-pg/postgresql"
35+
full_repo_name = f"ghcr.io/{repo_name}"
36+
pg_regexp = r"(\d+)(?:\.\d+|beta\d+|rc\d+|alpha\d+)-(\d{12})"
37+
_token_cache = {"value": None, "expires_at": 0}
38+
39+
40+
def get_json(image_name):
41+
data = check_output(
42+
[
43+
"docker",
44+
"run",
45+
"--rm",
46+
"quay.io/skopeo/stable",
47+
"list-tags",
48+
f"docker://{image_name}",
49+
]
50+
)
51+
repo_json = json.loads(data.decode("utf-8"))
52+
return repo_json
53+
54+
55+
def get_token(repository_name):
56+
global _token_cache
57+
now = time.time()
58+
59+
if _token_cache["value"] and now < _token_cache["expires_at"]:
60+
return _token_cache["value"]
61+
62+
url = "https://ghcr.io/token?scope=repository:{}:pull".format(repository_name)
63+
with urllib.request.urlopen(url) as response:
64+
data = json.load(response)
65+
token = data["token"]
66+
67+
_token_cache["value"] = token
68+
_token_cache["expires_at"] = now + 300
69+
return token
70+
71+
72+
def get_digest(repository_name, tag):
73+
token = get_token(repository_name)
74+
media_types = [
75+
"application/vnd.oci.image.index.v1+json",
76+
"application/vnd.oci.image.manifest.v1+json",
77+
"application/vnd.docker.distribution.manifest.v2+json",
78+
]
79+
url = f"https://ghcr.io/v2/{repository_name}/manifests/{tag}"
80+
req = urllib.request.Request(url)
81+
req.add_header("Authorization", "Bearer {}".format(token))
82+
req.add_header("Accept", ",".join(media_types))
83+
with urllib.request.urlopen(req) as response:
84+
digest = response.headers.get("Docker-Content-Digest")
85+
return digest
86+
87+
88+
def write_catalog(tags, version_re, img_type, os_name, output_dir="."):
89+
image_suffix = f"-{img_type}-{os_name}"
90+
version_re = re.compile(rf"^{version_re}{re.escape(image_suffix)}$")
91+
92+
# Filter out all the tags which do not match the version regexp
93+
tags = [item for item in tags if version_re.search(item)]
94+
95+
# Filter out preview versions
96+
exclude_preview = re.compile(r"(alpha|beta|rc)")
97+
tags = [item for item in tags if not exclude_preview.search(item)]
98+
99+
# Sort the tags according to semantic versioning
100+
tags.sort(key=lambda v: version.Version(v.removesuffix(image_suffix)), reverse=True)
101+
102+
results = {}
103+
for item in tags:
104+
match = version_re.search(item)
105+
if not match:
106+
continue
107+
108+
major = match.group(1)
109+
110+
# Skip too old versions
111+
if int(major) < min_supported_major:
112+
continue
113+
114+
if major not in results:
115+
digest = get_digest(repo_name, item)
116+
results[major] = [f"{full_repo_name}:{item}@{digest}"]
117+
118+
catalog = {
119+
"apiVersion": "postgresql.cnpg.io/v1",
120+
"kind": "ClusterImageCatalog",
121+
"metadata": {
122+
"name": f"postgresql{image_suffix}",
123+
"labels": {
124+
"images.cnpg.io/family": "postgresql",
125+
"images.cnpg.io/type": img_type,
126+
"images.cnpg.io/os": os_name,
127+
"images.cnpg.io/date": time.strftime("%Y%m%d"),
128+
"images.cnpg.io/publisher": "cnpg.io",
129+
},
130+
},
131+
"spec": {
132+
"images": [
133+
{"major": int(major), "image": images[0]}
134+
for major, images in sorted(results.items(), key=lambda x: int(x[0]))
135+
]
136+
},
137+
}
138+
139+
os.makedirs(output_dir, exist_ok=True)
140+
output_file = os.path.join(output_dir, f"catalog{image_suffix}.yaml")
141+
with open(output_file, "w") as f:
142+
yaml.dump(catalog, f, sort_keys=False)
143+
144+
145+
if __name__ == "__main__":
146+
parser = argparse.ArgumentParser(
147+
description="CloudNativePG ClusterImageCatalog YAML generator"
148+
)
149+
parser.add_argument(
150+
"--output-dir", default=".", help="Directory to save the YAML files"
151+
)
152+
args = parser.parse_args()
153+
154+
repo_json = get_json(full_repo_name)
155+
tags = repo_json["Tags"]
156+
157+
for img_type in supported_img_types:
158+
for os_name in supported_os_names:
159+
print(f"Generating catalog-{img_type}-{os_name}.yaml")
160+
write_catalog(tags, pg_regexp, img_type, os_name, args.output_dir)

.github/workflows/bake.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,15 @@ jobs:
5151
with:
5252
environment: ${{ github.event.inputs.environment }}
5353
postgresql_version: ${{ matrix.version }}
54+
55+
Catalogs:
56+
name: Update Catalogs
57+
needs: Bake
58+
runs-on: ubuntu-24.04
59+
permissions:
60+
contents: write
61+
steps:
62+
- name: Repository Dispatch
63+
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
64+
with:
65+
event-type: update-catalogs

.github/workflows/catalogs.yml

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

0 commit comments

Comments
 (0)