Skip to content

Commit 2e7c232

Browse files
committed
airgap: Script for building images.list for airgap solution
Signed-off-by: Peter Razumovsky <prazumovsky@mirantis.com>
1 parent b47b092 commit 2e7c232

File tree

3 files changed

+228
-0
lines changed

3 files changed

+228
-0
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Build airgap bundle (images + OCI charts) when a tag is pushed.
2+
# Step 1: collect chart OCI refs into charts.list
3+
# Step 2: collect image refs into images.list
4+
# Step 3: run make_bundle.sh (skopeo copy images + charts, then tar)
5+
name: Airgap bundle
6+
7+
on:
8+
push:
9+
tags: ["*"]
10+
11+
jobs:
12+
airgap-bundle:
13+
runs-on: ubuntu-latest
14+
env:
15+
REPO_ROOT: ${{ github.workspace }}
16+
# Set repo variable OCI_CHARTS_REGISTRY to override (e.g. ghcr.io/owner/pelagia-charts)
17+
REGISTRY: ${{ github.ref_type == 'tag' && vars.REGISTRY_PROD || vars.REGISTRY_CI }}
18+
DEV_VERSION: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || '' }}
19+
HELM_REPO: ${{ github.event_name == 'push' && 'pelagia' || 'pelagia/pr' }}
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v4
23+
with:
24+
fetch-depth: 0
25+
fetch-tags: true
26+
27+
- name: Set up Python
28+
uses: actions/setup-python@v5
29+
with:
30+
python-version: "3.12"
31+
32+
- name: Install PyYAML
33+
run: pip install pyyaml
34+
35+
- name: Collect chart OCI refs
36+
run: python3 build/scripts/collect_chart_oci_refs.py
37+
env:
38+
OCI_CHARTS_REGISTRY: ${{ env.REGISTRY }}/${{ env.HELM_REPO }}
39+
OUTPUT_FILE: ${{ github.workspace }}/charts.list
40+
41+
- name: Collect chart images
42+
run: python3 build/scripts/collect_chart_images.py
43+
env:
44+
IMAGE_REGISTRY: ${{ env.REGISTRY }}
45+
OUTPUT_FILE: ${{ github.workspace }}/images.list
46+
47+
- name: Build airgap bundle
48+
run: bash build/scripts/make_bundle.sh
49+
env:
50+
FULL_IMAGES_LIST_FILE: ${{ github.workspace }}/images.list
51+
FULL_CHARTS_LIST_FILE: ${{ github.workspace }}/charts.list
52+
AIRGAP_BUNDLE_DIR: ${{ github.workspace }}/bundle/images
53+
AIRGAP_BUNDLE_FILE: ${{ github.workspace }}/airgap-bundle-ceph.tar.gz
54+
# Optional: set DOCKER_CONFIG_PATH if charts/images need private registry auth
55+
# DOCKER_CONFIG_PATH: ${{ github.workspace }}/.docker/config.json
56+
57+
- name: Upload airgap bundle
58+
uses: actions/upload-artifact@v4
59+
with:
60+
name: airgap-bundle-${{ github.ref_name }}
61+
path: ${{ github.workspace }}/airgap-bundle-ceph.tar.gz
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Collects all container images in format registry/repo/image:tag from the
4+
"images" section (including nested subsections) of values of all Helm charts
5+
under charts/ and writes them to images.list (unique, sorted).
6+
7+
Parses values.yaml directly so all repository+tag pairs are collected even
8+
when tag is an object (e.g. latest/squid/tentacle).
9+
10+
Registry prefix is taken from IMAGE_REGISTRY env (e.g. set from REGISTRY in CI).
11+
Requires: PyYAML (pip install pyyaml)
12+
Usage: run from repo root, or set REPO_ROOT env to the repo root.
13+
Set IMAGE_REGISTRY for the image registry prefix (e.g. registry.example.com).
14+
Override output with OUTPUT_FILE env (default: images.list in repo root).
15+
"""
16+
17+
import os
18+
import sys
19+
from pathlib import Path
20+
21+
try:
22+
import yaml
23+
except ImportError:
24+
sys.stderr.write("Error: PyYAML required. Install with: pip install pyyaml\n")
25+
sys.exit(1)
26+
27+
28+
def collect_images_from_obj(obj: dict, registry: str, out: list[str]) -> None:
29+
"""Recursively collect registry/repo:tag from nodes that have repository and tag."""
30+
if not isinstance(obj, dict):
31+
return
32+
if "repository" in obj and "tag" in obj:
33+
repo = str(obj["repository"]).strip()
34+
if not repo:
35+
return
36+
tag = obj["tag"]
37+
if isinstance(tag, str):
38+
t = tag.strip()
39+
if t:
40+
ref = f"{registry}/{repo}:{t}" if registry else f"{repo}:{t}"
41+
out.append(ref)
42+
elif isinstance(tag, dict):
43+
for v in tag.values():
44+
if isinstance(v, str):
45+
t = v.strip()
46+
if t:
47+
ref = f"{registry}/{repo}:{t}" if registry else f"{repo}:{t}"
48+
out.append(ref)
49+
for v in obj.values():
50+
collect_images_from_obj(v, registry, out)
51+
52+
53+
def parse_values_file(path: Path, registry: str) -> list[str]:
54+
"""Parse a values.yaml and return list of image references (registry from IMAGE_REGISTRY)."""
55+
with open(path, encoding="utf-8") as f:
56+
data = yaml.safe_load(f) or {}
57+
out: list[str] = []
58+
if "images" in data:
59+
collect_images_from_obj(data["images"], registry, out)
60+
return out
61+
62+
63+
def main() -> None:
64+
script_dir = Path(__file__).resolve().parent
65+
repo_root = Path(os.environ.get("REPO_ROOT", script_dir.parent.parent))
66+
charts_dir = repo_root / "charts"
67+
output_file = Path(os.environ.get("OUTPUT_FILE", str(repo_root / "images.list")))
68+
registry = (os.environ.get("IMAGE_REGISTRY") or "").strip()
69+
70+
all_images: list[str] = []
71+
for chart_path in sorted(charts_dir.iterdir()):
72+
if not chart_path.is_dir():
73+
continue
74+
chart_yaml = chart_path / "Chart.yaml"
75+
values_yaml = chart_path / "values.yaml"
76+
if not chart_yaml.exists() or not values_yaml.exists():
77+
continue
78+
print(f"Processing chart: {chart_path}", file=sys.stderr)
79+
try:
80+
all_images.extend(parse_values_file(values_yaml, registry))
81+
except Exception as e:
82+
print(f"Warning: failed to parse {values_yaml}: {e}", file=sys.stderr)
83+
84+
lines = sorted(set(s for s in all_images if s.strip()))
85+
output_file.write_text("\n".join(lines) + ("\n" if lines else ""))
86+
print(f"Written {len(lines)} unique image(s) to {output_file}", file=sys.stderr)
87+
88+
89+
if __name__ == "__main__":
90+
main()
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Collects chart names from charts/*/Chart.yaml and writes refs to charts.list:
4+
<registry>/<repo>/<chart-name>:<version>
5+
(no oci:// prefix; make_bundle.sh uses oci: transport when copying.)
6+
7+
Chart version is taken from VERSION env if set, otherwise from `make get-version`
8+
(run from repo root; same version logic as the rest of the build).
9+
10+
Requires: PyYAML (pip install pyyaml)
11+
Env:
12+
REPO_ROOT - repo root (default: parent of build/ parent)
13+
OCI_CHARTS_REGISTRY - e.g. ghcr.io/owner/pelagia-charts (no oci:// prefix)
14+
VERSION - chart version (optional; if unset, runs make get-version)
15+
OUTPUT_FILE - output path (default: charts.list in repo root)
16+
"""
17+
18+
import os
19+
import subprocess
20+
import sys
21+
from pathlib import Path
22+
23+
try:
24+
import yaml
25+
except ImportError:
26+
sys.stderr.write("Error: PyYAML required. Install with: pip install pyyaml\n")
27+
sys.exit(1)
28+
29+
30+
def main() -> None:
31+
script_dir = Path(__file__).resolve().parent
32+
repo_root = Path(os.environ.get("REPO_ROOT", script_dir.parent.parent))
33+
charts_dir = repo_root / "charts"
34+
registry = (os.environ.get("OCI_CHARTS_REGISTRY") or "").strip()
35+
version = (os.environ.get("VERSION") or "").strip()
36+
output_file = Path(os.environ.get("OUTPUT_FILE", str(repo_root / "charts.list")))
37+
38+
if not registry:
39+
sys.stderr.write("Error: OCI_CHARTS_REGISTRY env is required (e.g. ghcr.io/owner/charts)\n")
40+
sys.exit(1)
41+
if not version:
42+
try:
43+
result = subprocess.run(
44+
["make", "get-version"],
45+
cwd=repo_root,
46+
capture_output=True,
47+
text=True,
48+
check=True,
49+
)
50+
version = (result.stdout or "").strip()
51+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
52+
sys.stderr.write(f"Error: VERSION env unset and 'make get-version' failed: {e}\n")
53+
sys.exit(1)
54+
if not version:
55+
sys.stderr.write("Error: VERSION env is required or run from repo with 'make get-version' available\n")
56+
sys.exit(1)
57+
58+
refs: list[str] = []
59+
for chart_path in sorted(charts_dir.iterdir()):
60+
if not chart_path.is_dir():
61+
continue
62+
chart_yaml = chart_path / "Chart.yaml"
63+
if not chart_yaml.exists():
64+
continue
65+
with open(chart_yaml, encoding="utf-8") as f:
66+
data = yaml.safe_load(f) or {}
67+
name = (data.get("name") or "").strip()
68+
if not name:
69+
continue
70+
refs.append(f"{registry}/{name}:{version}")
71+
72+
output_file.write_text("\n".join(refs) + ("\n" if refs else ""))
73+
print(f"Written {len(refs)} chart ref(s) to {output_file}", file=sys.stderr)
74+
75+
76+
if __name__ == "__main__":
77+
main()

0 commit comments

Comments
 (0)