Skip to content

Commit b9e3ce6

Browse files
MGMT-19400 - create tag management flow to tag assisted installer components according to capbcoa versions
1 parent 8379cb1 commit b9e3ce6

11 files changed

+938
-8
lines changed

release-candidates.yaml

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
snapshots:
2+
- metadata:
3+
generated_at: '2025-03-10T10:32:04.642635'
4+
status: pending
5+
versions:
6+
kubernetes-sigs/cluster-api:
7+
commit_sha: main
8+
commit_url: https://github.com/kubernetes-sigs/cluster-api/commit/main
9+
version: v1.9.5
10+
image_url:
11+
metal3-io/cluster-api-provider-metal3:
12+
commit_sha: main
13+
commit_url: https://github.com/metal3-io/cluster-api-provider-metal3/commit/main
14+
version: v1.9.3
15+
image_url:
16+
openshift/assisted-service/assisted-service:
17+
commit_sha: 76d29d2a7f0899dcede9700fc88fcbad37b6ccca
18+
commit_url: https://github.com/openshift/assisted-service/commit/76d29d2a7f0899dcede9700fc88fcbad37b6ccca
19+
version: 76d29d2a7f0899dcede9700fc88fcbad37b6ccca
20+
image_url: quay.io/edge-infrastructure/assisted-service:latest-76d29d2a7f0899dcede9700fc88fcbad37b6ccca
21+
openshift/assisted-image-service/assisted-image-service:
22+
commit_sha: 2249c85d05600191b24e93dd92e733d49a1180ec
23+
commit_url: https://github.com/openshift/assisted-image-service/commit/2249c85d05600191b24e93dd92e733d49a1180ec
24+
version: 2249c85d05600191b24e93dd92e733d49a1180ec
25+
image_url: quay.io/edge-infrastructure/assisted-image-service:latest-2249c85d05600191b24e93dd92e733d49a1180ec
26+
openshift/assisted-installer-agent/assisted-installer-agent:
27+
commit_sha: cfe93a9779dea6ad2a628280b40071d23f3cb429
28+
commit_url: https://github.com/openshift/assisted-installer-agent/commit/cfe93a9779dea6ad2a628280b40071d23f3cb429
29+
version: cfe93a9779dea6ad2a628280b40071d23f3cb429
30+
image_url: quay.io/edge-infrastructure/assisted-installer-agent:latest-cfe93a9779dea6ad2a628280b40071d23f3cb429
31+
openshift/assisted-installer/assisted-installer-controller:
32+
commit_sha: c389a38405383961d26191799161c86127451635
33+
commit_url: https://github.com/openshift/assisted-installer/commit/c389a38405383961d26191799161c86127451635
34+
version: c389a38405383961d26191799161c86127451635
35+
image_url: quay.io/edge-infrastructure/assisted-installer-controller:latest-c389a38405383961d26191799161c86127451635
36+
openshift/assisted-installer/assisted-installer:
37+
commit_sha: c389a38405383961d26191799161c86127451635
38+
commit_url: https://github.com/openshift/assisted-installer/commit/c389a38405383961d26191799161c86127451635
39+
version: c389a38405383961d26191799161c86127451635
40+
image_url: quay.io/edge-infrastructure/assisted-installer:latest-c389a38405383961d26191799161c86127451635

scripts/github_auth.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import logging
5+
from github import Github, GithubIntegration
6+
7+
logger = logging.getLogger("github-auth")
8+
9+
def get_github_client():
10+
"""
11+
Creates a PyGithub client using GitHub App authentication
12+
13+
Environment variables:
14+
GITHUB_APP_ID: The ID of your GitHub App
15+
GITHUB_APP_INSTALLATION_ID: The installation ID for your GitHub App
16+
GITHUB_APP_PRIVATE_KEY: Private key .pem file
17+
"""
18+
try:
19+
app_id = os.environ["GITHUB_APP_ID"]
20+
installation_id = os.environ["GITHUB_APP_INSTALLATION_ID"]
21+
private_key = os.environ["GITHUB_APP_PRIVATE_KEY"]
22+
23+
if not all([app_id, installation_id, private_key]):
24+
logger.error("Missing GitHub App credentials in environment")
25+
raise ValueError("""
26+
Missing GitHub App credentials.
27+
Ensure GITHUB_APP_ID, GITHUB_APP_INSTALLATION_ID, and GITHUB_APP_PRIVATE_KEY are set.
28+
""")
29+
30+
integration = GithubIntegration(
31+
int(app_id),
32+
private_key
33+
)
34+
access_token = integration.get_access_token(int(installation_id))
35+
github_client = Github(access_token.token)
36+
logger.debug("Created GitHub client with new token")
37+
return github_client
38+
except Exception as e:
39+
logger.error(f"Error creating GitHub client: {e}")
40+
raise

scripts/tag_reconciler.py

+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#!/usr/bin/env python3
2+
3+
import re
4+
import os
5+
import sys
6+
import logging
7+
from typing import Any
8+
from ruamel.yaml import YAML
9+
from github_auth import get_github_client
10+
11+
logging.basicConfig(
12+
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
13+
)
14+
logger = logging.getLogger("tag-reconciler")
15+
16+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
17+
PROJECT_ROOT = os.path.dirname(SCRIPT_DIR)
18+
VERSIONS_FILE = os.path.join(PROJECT_ROOT, "versions.yaml")
19+
20+
yaml = YAML(typ="rt")
21+
yaml.default_flow_style = False
22+
yaml.explicit_start = False
23+
yaml.preserve_quotes = True
24+
yaml.width = 4096
25+
yaml.indent(mapping=2, sequence=2, offset=0)
26+
27+
28+
class TagReconciler:
29+
def __init__(self):
30+
self.github_client = get_github_client()
31+
32+
def read_versions_file(self) -> dict[str, Any]:
33+
if not os.path.exists(VERSIONS_FILE):
34+
logger.error(f"{VERSIONS_FILE} does not exist")
35+
sys.exit(1)
36+
37+
try:
38+
with open(VERSIONS_FILE, "r") as f:
39+
data = yaml.load(f)
40+
return data or {}
41+
except Exception as e:
42+
logger.error(f"Error reading {VERSIONS_FILE}: {e}")
43+
return {}
44+
45+
def tag_exists(self, repo: str, tag: str) -> bool:
46+
try:
47+
github_repo = self.github_client.get_repo(repo)
48+
github_repo.get_git_ref(f"tags/{tag}")
49+
return True
50+
except Exception as e:
51+
logger.error(f"Error checking tag {tag} for {repo}: {e}")
52+
return False
53+
54+
def create_tag(self, repo: str, commit_sha: str, tag: str) -> bool:
55+
message = f"Version {tag} - Tagged by CAPBCOA CI"
56+
57+
try:
58+
github_repo = self.github_client.get_repo(repo)
59+
tag_obj = github_repo.create_git_tag(
60+
tag=tag,
61+
message=message,
62+
object=commit_sha,
63+
type="commit"
64+
)
65+
66+
github_repo.create_git_ref(f"refs/tags/{tag}", tag_obj.sha)
67+
logger.info(f"Created tag {tag} for {repo}")
68+
return True
69+
except Exception as e:
70+
logger.error(f"Error creating tag {tag} for {repo}: {e}")
71+
return False
72+
73+
def extract_base_repo(self, full_repo_name: str) -> str:
74+
parts = full_repo_name.split("/")
75+
if len(parts) > 2:
76+
return f"{parts[0]}/{parts[1]}"
77+
return full_repo_name
78+
79+
def reconcile_tag(self, version: dict[str, Any]) -> bool:
80+
version_name = version.get("name", "")
81+
if not version_name:
82+
logger.error("Version entry missing 'name' field")
83+
return False
84+
85+
logger.info(f"Processing version {version_name}")
86+
success = True
87+
88+
repositories = version.get("repositories", {})
89+
if not repositories:
90+
logger.warning(f"No repositories found for version {version_name}")
91+
return True
92+
93+
for repo_name, repo_info in repositories.items():
94+
commit_sha = repo_info.get("commit_sha", "")
95+
96+
if not re.match(r"^openshift/", repo_name) or not commit_sha:
97+
logger.warning(f"Skipping repository {repo_name}")
98+
continue
99+
100+
base_repo = self.extract_base_repo(repo_name)
101+
if self.tag_exists(base_repo, version_name):
102+
logger.info(f"Tag {version_name} already exists for {base_repo}")
103+
continue
104+
if not self.create_tag(base_repo, commit_sha, version_name):
105+
logger.error(f"Failed to create tag {version_name} for {base_repo}")
106+
success = False
107+
108+
return success
109+
110+
def reconcile_tags(self) -> bool:
111+
versions_data = self.read_versions_file()
112+
versions = versions_data.get("versions", [])
113+
114+
if not versions:
115+
logger.info("No versions to process")
116+
return True
117+
118+
logger.info(f"Processing {len(versions)} versions")
119+
success = True
120+
121+
for version in versions:
122+
if not self.reconcile_tag(version):
123+
success = False
124+
125+
return success
126+
127+
128+
def main() -> None:
129+
try:
130+
logger.info("Starting tag reconciliation")
131+
reconciler = TagReconciler()
132+
success = reconciler.reconcile_tags()
133+
134+
if not success:
135+
logger.error("Tag reconciliation completed with errors")
136+
sys.exit(1)
137+
138+
except Exception as e:
139+
logger.error(f"Error in tag reconciliation: {e}")
140+
sys.exit(1)
141+
142+
143+
if __name__ == "__main__":
144+
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)