Skip to content

Commit 679b9b5

Browse files
Merge pull request #5351 from djoshy/ami-updater
NO-ISSUE: Add AMI update automation script
2 parents 7007a56 + 108dae4 commit 679b9b5

File tree

3 files changed

+395
-1
lines changed

3 files changed

+395
-1
lines changed

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ GOTAGS = "containers_image_openpgp exclude_graphdriver_devicemapper exclude_grap
3030

3131
all: binaries
3232

33-
.PHONY: clean test test-unit test-e2e verify update install-tools
33+
.PHONY: clean test test-unit test-e2e verify update update-amis install-tools
3434

3535
# Remove build artifaces
3636
# Example:
@@ -69,6 +69,13 @@ test-unit: install-go-junit-report
6969
# make update
7070
update:
7171
hack/update-templates.sh
72+
73+
# Update the AMI list from the OpenShift installer repository.
74+
# Example:
75+
# make update-amis
76+
update-amis:
77+
python3 hack/update_amis.py
78+
7279
go-deps:
7380
go mod tidy
7481
go mod vendor

hack/update_amis.py

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Script to update AMI list in ami.go from the OpenShift installer repository.
4+
5+
This script:
6+
1. Fetches the rhcos.json file from the installer repo
7+
2. Steps back through git history from HEAD to the last recorded commit
8+
3. Extracts new AMI IDs from each commit
9+
4. Validates that no duplicate AMIs are being added
10+
5. Updates ami.go with new AMIs and the latest commit hash
11+
"""
12+
13+
import os
14+
import re
15+
import sys
16+
import json
17+
import tempfile
18+
import subprocess
19+
from typing import Set, Dict, List, Tuple
20+
from pathlib import Path
21+
22+
23+
class Color:
24+
"""ANSI color codes for terminal output."""
25+
RED = '\033[0;31m'
26+
GREEN = '\033[0;32m'
27+
YELLOW = '\033[1;33m'
28+
BLUE = '\033[0;34m'
29+
NC = '\033[0m' # No Color
30+
31+
32+
def log_info(message: str):
33+
"""Print info message in green."""
34+
print(f"{Color.GREEN}[INFO]{Color.NC} {message}")
35+
36+
37+
def log_warn(message: str):
38+
"""Print warning message in yellow."""
39+
print(f"{Color.YELLOW}[WARN]{Color.NC} {message}")
40+
41+
42+
def log_error(message: str):
43+
"""Print error message in red."""
44+
print(f"{Color.RED}[ERROR]{Color.NC} {message}", file=sys.stderr)
45+
46+
47+
def run_command(cmd: List[str], cwd: str = None, check: bool = True) -> str:
48+
"""Run a shell command and return its output."""
49+
try:
50+
result = subprocess.run(
51+
cmd,
52+
cwd=cwd,
53+
capture_output=True,
54+
text=True,
55+
check=check
56+
)
57+
return result.stdout.strip()
58+
except subprocess.CalledProcessError as e:
59+
if check:
60+
log_error(f"Command failed: {' '.join(cmd)}")
61+
log_error(f"Error: {e.stderr}")
62+
raise
63+
return ""
64+
65+
66+
def get_current_commit_hash(ami_go_path: Path) -> str:
67+
"""Extract the current source commit hash from ami.go."""
68+
with open(ami_go_path, 'r') as f:
69+
for line in f:
70+
if 'source commit hash' in line:
71+
match = re.search(r'= ([a-f0-9]+)', line)
72+
if match:
73+
return match.group(1)
74+
raise ValueError("Could not find source commit hash in ami.go")
75+
76+
77+
def get_existing_amis(ami_go_path: Path) -> List[str]:
78+
"""Extract all existing AMI IDs from ami.go in order."""
79+
amis = []
80+
with open(ami_go_path, 'r') as f:
81+
in_ami_section = False
82+
for line in f:
83+
if 'AllowedAMIs = sets.New(' in line:
84+
in_ami_section = True
85+
continue
86+
if in_ami_section:
87+
if line.strip() == ')':
88+
break
89+
# Extract AMI IDs from the line
90+
ami_matches = re.findall(r'"(ami-[a-f0-9]+)"', line)
91+
amis.extend(ami_matches)
92+
return amis
93+
94+
95+
def extract_amis_from_json(json_content: str) -> Set[str]:
96+
"""Extract AMI IDs from rhcos.json content."""
97+
amis = set()
98+
try:
99+
data = json.loads(json_content)
100+
# Navigate through the JSON structure to find AMI IDs
101+
if 'amis' in data:
102+
for arch_data in data['amis']:
103+
if 'hvm' in arch_data:
104+
ami = arch_data['hvm']
105+
if ami.startswith('ami-'):
106+
amis.add(ami)
107+
108+
# Also search for AMI patterns in the entire JSON
109+
ami_matches = re.findall(r'ami-[a-f0-9]{8,}', json_content)
110+
amis.update(ami_matches)
111+
112+
except json.JSONDecodeError as e:
113+
log_warn(f"Failed to parse JSON: {e}")
114+
# Fall back to regex extraction
115+
ami_matches = re.findall(r'ami-[a-f0-9]{8,}', json_content)
116+
amis.update(ami_matches)
117+
118+
return amis
119+
120+
121+
def get_commits_between(repo_path: Path, start_commit: str, file_path: str) -> List[str]:
122+
"""Get list of commits from start_commit to HEAD for the given file."""
123+
# Get commits from start_commit (exclusive) to HEAD
124+
cmd = ['git', 'log', '--pretty=format:%H', f'{start_commit}..HEAD', '--', file_path]
125+
output = run_command(cmd, cwd=str(repo_path), check=False)
126+
127+
if not output:
128+
log_warn(f"No commits found between {start_commit} and HEAD")
129+
# Try getting recent commits
130+
cmd = ['git', 'log', '--pretty=format:%H', '-n', '10', '--', file_path]
131+
output = run_command(cmd, cwd=str(repo_path), check=False)
132+
133+
commits = [line.strip() for line in output.split('\n') if line.strip()]
134+
# Reverse to process oldest first
135+
return list(reversed(commits))
136+
137+
138+
def get_file_at_commit(repo_path: Path, commit: str, file_path: str) -> str:
139+
"""Get the content of a file at a specific commit."""
140+
cmd = ['git', 'show', f'{commit}:{file_path}']
141+
return run_command(cmd, cwd=str(repo_path), check=False)
142+
143+
144+
def update_ami_go_file(ami_go_path: Path, new_amis: Set[str], existing_amis: List[str],
145+
latest_commit: str) -> bool:
146+
"""Update the ami.go file with new AMIs and commit hash."""
147+
# Keep existing AMIs in order, then add new AMIs sorted at the end
148+
new_amis_sorted = sorted(new_amis)
149+
all_amis = existing_amis + new_amis_sorted
150+
151+
# Read the original file
152+
with open(ami_go_path, 'r') as f:
153+
lines = f.readlines()
154+
155+
# Find the AMI section
156+
start_idx = None
157+
end_idx = None
158+
for i, line in enumerate(lines):
159+
if 'AllowedAMIs = sets.New(' in line:
160+
start_idx = i
161+
elif start_idx is not None and line.strip() == ')':
162+
end_idx = i
163+
break
164+
165+
if start_idx is None or end_idx is None:
166+
log_error("Could not find AllowedAMIs section in ami.go")
167+
return False
168+
169+
# Always use 5 AMIs per line
170+
amis_per_line = 5
171+
172+
# Format AMIs for Go code (5 per line)
173+
formatted_lines = []
174+
for i in range(0, len(all_amis), amis_per_line):
175+
chunk = all_amis[i:i+amis_per_line]
176+
formatted = ', '.join(f'"{ami}"' for ami in chunk)
177+
# Always add comma at the end
178+
formatted += ','
179+
formatted_lines.append(f'\t{formatted}\n')
180+
181+
# Update commit hash in the lines before the AMI section
182+
for i in range(start_idx):
183+
if 'source commit hash' in lines[i]:
184+
lines[i] = re.sub(
185+
r'(source commit hash = )[a-f0-9]+',
186+
f'\\g<1>{latest_commit}',
187+
lines[i]
188+
)
189+
190+
# Reconstruct the file
191+
new_lines = (
192+
lines[:start_idx+1] + # Everything up to and including "AllowedAMIs = sets.New("
193+
formatted_lines + # The formatted AMI list
194+
[lines[end_idx]] + # The closing ")"
195+
lines[end_idx+1:] # Everything after
196+
)
197+
198+
# Write back to file
199+
with open(ami_go_path, 'w') as f:
200+
f.writelines(new_lines)
201+
202+
return True
203+
204+
205+
def main():
206+
"""Main function to update AMIs."""
207+
# Configuration
208+
REPO_URL = "https://github.com/openshift/installer"
209+
FILE_PATH = "data/data/coreos/rhcos.json"
210+
211+
# Determine the project root (parent of hack directory)
212+
script_dir = Path(__file__).parent.resolve()
213+
project_root = script_dir.parent
214+
AMI_GO_FILE = project_root / "pkg/controller/machine-set-boot-image/ami.go"
215+
216+
# Check if ami.go exists
217+
if not AMI_GO_FILE.exists():
218+
log_error(f"File not found: {AMI_GO_FILE}")
219+
sys.exit(1)
220+
221+
log_info("Starting AMI update process...")
222+
223+
# Get current commit hash from ami.go
224+
current_commit = get_current_commit_hash(AMI_GO_FILE)
225+
log_info(f"Current source commit in ami.go: {current_commit}")
226+
227+
# Get existing AMIs
228+
existing_amis = get_existing_amis(AMI_GO_FILE)
229+
existing_amis_set = set(existing_amis)
230+
log_info(f"Found {len(existing_amis)} existing AMI IDs in ami.go")
231+
232+
# Create temporary directory and clone with filter
233+
with tempfile.TemporaryDirectory() as temp_dir:
234+
repo_path = Path(temp_dir)
235+
236+
log_info(f"Cloning repository with filter for {FILE_PATH}...")
237+
238+
# Clone with --filter=blob:none --no-checkout for faster cloning
239+
run_command([
240+
'git', 'clone', '--quiet', '--filter=blob:none', '--no-checkout',
241+
'--depth=100', '--single-branch', '--branch=main',
242+
REPO_URL, str(repo_path)
243+
])
244+
245+
# Checkout only the specific file
246+
run_command(['git', 'checkout', 'main', '--', FILE_PATH], cwd=str(repo_path))
247+
248+
# Check if current commit is up to date with remote
249+
log_info("Checking if file is up to date...")
250+
latest_remote_commit = run_command(
251+
['git', 'log', '-n', '1', '--pretty=format:%H', '--', FILE_PATH],
252+
cwd=str(repo_path)
253+
)
254+
255+
if latest_remote_commit == current_commit:
256+
log_info("File is already up to date! No new commits to process.")
257+
sys.exit(0)
258+
259+
# Get commits to process
260+
log_info(f"Getting commit history from {current_commit[:8]} to latest...")
261+
commits = get_commits_between(repo_path, current_commit, FILE_PATH)
262+
263+
if not commits:
264+
log_warn("No new commits found")
265+
sys.exit(0)
266+
267+
log_info(f"Found {len(commits)} commit(s) to process")
268+
269+
# Track new AMIs and their source commits
270+
new_amis_map: Dict[str, str] = {}
271+
latest_commit = None
272+
273+
# Process each commit
274+
for commit in commits:
275+
log_info(f"Processing commit: {commit[:8]}...")
276+
277+
# Get file content at this commit
278+
content = get_file_at_commit(repo_path, commit, FILE_PATH)
279+
280+
if not content:
281+
log_warn(f"Could not retrieve {FILE_PATH} at commit {commit[:8]}, skipping...")
282+
continue
283+
284+
# Extract AMIs from this version
285+
commit_amis = extract_amis_from_json(content)
286+
287+
if not commit_amis:
288+
log_warn(f"No AMIs found in commit {commit[:8]}")
289+
continue
290+
291+
log_info(f" Found {len(commit_amis)} AMI(s) in this commit")
292+
latest_commit = commit
293+
294+
# Check each AMI
295+
for ami in commit_amis:
296+
# Check if AMI already exists in ami.go
297+
if ami in existing_amis_set:
298+
log_error(f"Duplicate AMI detected: {ami}")
299+
log_error(f" This AMI already exists in ami.go")
300+
log_error(f" Found in commit: {commit[:8]}")
301+
sys.exit(1)
302+
303+
# Check if AMI was already added by a previous commit in this run
304+
if ami in new_amis_map:
305+
log_error(f"Duplicate AMI detected: {ami}")
306+
log_error(f" First seen in commit: {new_amis_map[ami][:8]}")
307+
log_error(f" Duplicate in commit: {commit[:8]}")
308+
sys.exit(1)
309+
310+
# Add to new AMIs map
311+
new_amis_map[ami] = commit
312+
313+
# Check if we found any new AMIs
314+
if not new_amis_map:
315+
log_warn("No new AMIs found")
316+
sys.exit(0)
317+
318+
log_info(f"Found {len(new_amis_map)} new AMI(s) to add")
319+
320+
if not latest_commit:
321+
log_error("No valid commits were processed")
322+
sys.exit(1)
323+
324+
log_info(f"Latest processed commit: {latest_commit[:8]}")
325+
326+
# Update ami.go file
327+
log_info(f"Updating {AMI_GO_FILE}...")
328+
329+
success = update_ami_go_file(
330+
AMI_GO_FILE,
331+
set(new_amis_map.keys()),
332+
existing_amis,
333+
latest_commit
334+
)
335+
336+
if not success:
337+
log_error("Failed to update ami.go")
338+
sys.exit(1)
339+
340+
# Print summary
341+
log_info("Update complete!")
342+
log_info(f" - Updated source commit hash to: {latest_commit[:8]}")
343+
log_info(f" - Added {len(new_amis_map)} new AMI(s)")
344+
log_info(f" - Total AMI count: {len(existing_amis) + len(new_amis_map)}")
345+
346+
if __name__ == "__main__":
347+
try:
348+
main()
349+
except KeyboardInterrupt:
350+
log_warn("\nInterrupted by user")
351+
sys.exit(1)
352+
except Exception as e:
353+
log_error(f"Unexpected error: {e}")
354+
import traceback
355+
traceback.print_exc()
356+
sys.exit(1)

0 commit comments

Comments
 (0)