Skip to content

Commit ce45bc0

Browse files
MGMT-19400 - create tag management flow to tag assisted installer components according to capbcoa versions
1 parent 209c7d7 commit ce45bc0

File tree

5 files changed

+635
-8
lines changed

5 files changed

+635
-8
lines changed

scripts/tag_reconciler.py

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
#!/usr/bin/env python3
2+
3+
import re
4+
import os
5+
import sys
6+
import requests
7+
import logging
8+
from typing import Any
9+
from ruamel.yaml import YAML
10+
11+
logging.basicConfig(
12+
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
13+
)
14+
logger = logging.getLogger("tag-reconciler")
15+
16+
VERSIONS_FILE = "versions.yaml"
17+
GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
18+
19+
yaml = YAML(typ="rt")
20+
yaml.default_flow_style = False
21+
yaml.explicit_start = False
22+
yaml.preserve_quotes = True
23+
yaml.width = 4096
24+
yaml.indent(mapping=2, sequence=2, offset=0)
25+
26+
27+
class TagReconciler:
28+
def read_versions_file(self) -> dict[str, Any]:
29+
if not os.path.exists(VERSIONS_FILE):
30+
logger.error(f"{VERSIONS_FILE} does not exist")
31+
sys.exit(1)
32+
33+
try:
34+
with open(VERSIONS_FILE, "r") as f:
35+
data = yaml.load(f)
36+
return data or {}
37+
except Exception as e:
38+
logger.error(f"Error reading {VERSIONS_FILE}: {e}")
39+
return {}
40+
41+
def tag_exists(self, repo: str, tag: str) -> bool:
42+
url = f"https://api.github.com/repos/{repo}/git/refs/tags/{tag}"
43+
headers = {
44+
"Accept": "application/vnd.github+json",
45+
"Authorization": f"token {GITHUB_TOKEN}",
46+
"X-GitHub-Api-Version": "2022-11-28",
47+
}
48+
49+
try:
50+
response = requests.get(url, headers=headers)
51+
return response.status_code == 200
52+
except requests.RequestException as e:
53+
logger.error(f"Request error checking tag {tag} for {repo}: {e}")
54+
return False
55+
56+
def create_tag(self, repo: str, commit_sha: str, tag: str) -> bool:
57+
message = f"Version {tag} - Tagged by CAPBCOA CI"
58+
url = f"https://api.github.com/repos/{repo}/git/tags"
59+
headers = {
60+
"Accept": "application/vnd.github+json",
61+
"Authorization": f"Bearer {GITHUB_TOKEN}",
62+
"X-GitHub-Api-Version": "2022-11-28",
63+
}
64+
data = {
65+
"tag": tag,
66+
"message": message,
67+
"object": commit_sha,
68+
"type": "commit",
69+
}
70+
71+
try:
72+
response = requests.post(url, headers=headers, json=data)
73+
if response.status_code != 201:
74+
logger.error(
75+
f"Failed to create tag object: {response.status_code} {response.text}"
76+
)
77+
return False
78+
79+
tag_obj = response.json()
80+
tag_sha = tag_obj["sha"]
81+
82+
ref_url = f"https://api.github.com/repos/{repo}/git/refs"
83+
ref_data = {
84+
"ref": f"refs/tags/{tag}",
85+
"sha": tag_sha,
86+
}
87+
88+
ref_response = requests.post(ref_url, headers=headers, json=ref_data)
89+
if ref_response.status_code != 201:
90+
logger.error(
91+
f"Failed to create tag reference: {ref_response.status_code}"
92+
)
93+
return False
94+
95+
logger.info(f"Created tag {tag} for {repo}")
96+
return True
97+
98+
except requests.RequestException as e:
99+
logger.error(f"Request error creating tag {tag} for {repo}: {e}")
100+
return False
101+
102+
def extract_base_repo(self, full_repo_name: str) -> str:
103+
parts = full_repo_name.split("/")
104+
if len(parts) > 2:
105+
return f"{parts[0]}/{parts[1]}"
106+
return full_repo_name
107+
108+
def reconcile_tag(self, version: dict[str, Any]) -> bool:
109+
version_name = version.get("name", "")
110+
if not version_name:
111+
logger.error("Version entry missing 'name' field")
112+
return False
113+
114+
logger.info(f"Processing version {version_name}")
115+
success = True
116+
117+
repositories = version.get("repositories", {})
118+
if not repositories:
119+
logger.warning(f"No repositories found for version {version_name}")
120+
return True
121+
122+
for repo_name, repo_info in repositories.items():
123+
commit_sha = repo_info.get("commit_sha", "")
124+
125+
if not re.match(r"^openshift/", repo_name) or not commit_sha:
126+
logger.warning(f"Skipping repository {repo_name}")
127+
continue
128+
129+
base_repo = self.extract_base_repo(repo_name)
130+
if self.tag_exists(base_repo, version_name):
131+
logger.info(f"Tag {version_name} already exists for {base_repo}")
132+
continue
133+
if not self.create_tag(base_repo, commit_sha, version_name):
134+
logger.error(f"Failed to create tag {version_name} for {base_repo}")
135+
success = False
136+
137+
return success
138+
139+
def reconcile_tags(self) -> bool:
140+
versions_data = self.read_versions_file()
141+
versions = versions_data.get("versions", [])
142+
143+
if not versions:
144+
logger.info("No versions to process")
145+
return True
146+
147+
logger.info(f"Processing {len(versions)} versions")
148+
success = True
149+
150+
for version in versions:
151+
if not self.reconcile_tag(version):
152+
success = False
153+
154+
return success
155+
156+
157+
def main() -> None:
158+
try:
159+
logger.info("Starting tag reconciliation")
160+
reconciler = TagReconciler()
161+
success = reconciler.reconcile_tags()
162+
163+
if not success:
164+
logger.error("Tag reconciliation completed with errors")
165+
sys.exit(1)
166+
167+
except Exception as e:
168+
logger.error(f"Error in tag reconciliation: {e}")
169+
sys.exit(1)
170+
171+
172+
if __name__ == "__main__":
173+
main()

scripts/test_runner.py

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import sys
5+
import subprocess
6+
import logging
7+
import yaml
8+
from datetime import datetime
9+
from typing import Any
10+
11+
logging.basicConfig(
12+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
13+
)
14+
logger = logging.getLogger("test-runner")
15+
16+
RELEASE_CANDIDATES_FILE = "release-candidates.yaml"
17+
18+
19+
def read_release_candidates() -> dict[str, Any]:
20+
try:
21+
with open(RELEASE_CANDIDATES_FILE, "r") as f:
22+
data = yaml.safe_load(f)
23+
if not data or "snapshots" not in data:
24+
logger.error(f"Invalid format in {RELEASE_CANDIDATES_FILE}")
25+
sys.exit(1)
26+
return data
27+
except Exception as e:
28+
logger.error(f"Error reading {RELEASE_CANDIDATES_FILE}: {e}")
29+
sys.exit(1)
30+
31+
32+
def find_pending_snapshot(data: dict[str, Any]) -> tuple[int, dict[str, Any]] | None:
33+
for i, snapshot in enumerate(data["snapshots"]):
34+
if "metadata" in snapshot and snapshot["metadata"].get("status") == "pending":
35+
return i, snapshot
36+
return None, None
37+
38+
39+
def export_component_variables(snapshot: dict[str, Any]) -> None:
40+
env_vars = {}
41+
component_env_map = {
42+
"kubernetes-sigs/cluster-api": "CAPI_VERSION",
43+
"metal3-io/cluster-api-provider-metal3": "CAPM3_VERSION",
44+
"openshift/assisted-service/assisted-service": "ASSISTED_SERVICE_IMAGE",
45+
"openshift/assisted-image-service/assisted-image-service": "ASSISTED_IMAGE_SERVICE_IMAGE",
46+
"openshift/assisted-installer-agent/assisted-installer-agent": "ASSISTED_INSTALLER_AGENT_IMAGE",
47+
"openshift/assisted-installer/assisted-installer-controller": "ASSISTED_INSTALLER_CONTROLLER_IMAGE",
48+
"openshift/assisted-installer/assisted-installer": "ASSISTED_INSTALLER_IMAGE",
49+
}
50+
for component, info in snapshot["versions"].items():
51+
env_var = component_env_map.get(component)
52+
if not env_var:
53+
logger.warning(
54+
f"No environment variable mapping for component: {component}"
55+
)
56+
continue
57+
58+
if (
59+
"kubernetes-sigs/cluster-api" in component
60+
or "metal3-io/cluster-api-provider-metal3" in component
61+
):
62+
value = info["version"]
63+
else:
64+
value = info.get("image_url", "")
65+
66+
if value:
67+
env_vars[env_var] = value
68+
os.environ[env_var] = value
69+
logger.info(f"Exported {env_var}={value}")
70+
return env_vars
71+
72+
73+
def run_ansible_tests() -> bool:
74+
try:
75+
logger.info("Running Ansible tests...")
76+
ansible_cmd = [
77+
"ansible-playbook",
78+
"test/ansible/run_test.yaml",
79+
"-i",
80+
"test/ansible/inventory.yaml",
81+
]
82+
result = subprocess.run(
83+
ansible_cmd, check=False, capture_output=False, text=True
84+
)
85+
86+
if result.returncode == 0:
87+
logger.info("Ansible tests passed successfully")
88+
return True
89+
else:
90+
logger.error(f"Ansible tests failed with return code {result.returncode}")
91+
return False
92+
93+
except Exception as e:
94+
logger.error(f"Error running Ansible tests: {e}")
95+
return False
96+
97+
98+
def update_snapshot_status(snapshot_index: int, success: bool) -> None:
99+
try:
100+
data = read_release_candidates()
101+
102+
if snapshot_index >= len(data["snapshots"]):
103+
logger.error(f"Snapshot index {snapshot_index} out of range")
104+
return
105+
106+
snapshot = data["snapshots"][snapshot_index]
107+
if "metadata" not in snapshot:
108+
snapshot["metadata"] = {}
109+
110+
snapshot["metadata"]["status"] = "successful" if success else "failed"
111+
snapshot["metadata"]["tested_at"] = datetime.now().isoformat()
112+
113+
with open(RELEASE_CANDIDATES_FILE, "w") as f:
114+
yaml.dump(data, f, default_flow_style=False)
115+
116+
logger.info(
117+
f"Updated snapshot {snapshot_index} status to {'successful' if success else 'failed'}"
118+
)
119+
120+
except Exception as e:
121+
logger.error(f"Error updating snapshot status: {e}")
122+
123+
124+
def main() -> None:
125+
try:
126+
logger.info("Starting test runner")
127+
data = read_release_candidates()
128+
129+
snapshot_index, snapshot = find_pending_snapshot(data)
130+
if snapshot is None:
131+
logger.info("No pending snapshots to process")
132+
return
133+
134+
logger.info(f"Processing pending snapshot {snapshot_index}")
135+
export_component_variables(snapshot)
136+
137+
success = run_ansible_tests()
138+
update_snapshot_status(snapshot_index, success)
139+
140+
if success:
141+
logger.info("Snapshot processed successfully")
142+
else:
143+
logger.error("Snapshot testing failed")
144+
sys.exit(1)
145+
146+
except Exception as e:
147+
logger.error(f"Test runner failed: {e}")
148+
sys.exit(1)
149+
150+
151+
if __name__ == "__main__":
152+
main()

0 commit comments

Comments
 (0)