Skip to content

Commit 46bcba8

Browse files
authored
[ISV-4964] Implement Preflight version invalidation tool (#694)
* [ISV-4964] Implement Preflight version invalidation tool * [ISV-4964] Fixup tests * [ISV-4964] Ignore log branch in coverage * [ISV-4964] Add missing types * [ISV-4964] Require auth for Pyxis read * [ISV-4964] Create preflight invalidation deployment playbook * [ISV-4964] Add deployment example * [ISV-4964] Remove debug message * [ISV-4964] Rename invalidation script * [ISV-4964] Use released image for preflight invalidation * [ISV-4964] Add preflight invalidation role to deployment playbook * [ISV-4964] Use in-API filter for versions
1 parent 1837ce0 commit 46bcba8

File tree

8 files changed

+517
-0
lines changed

8 files changed

+517
-0
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
- name: Deploy preflight invalidation CronJob
3+
hosts: "{{ clusters }}"
4+
roles:
5+
- preflight-invalidation
6+
vars:
7+
pyxis_url: "{{ 'https://pyxis.engineering.redhat.com' if env == 'prod' else 'https://pyxis.' + env + '.engineering.redhat.com' }}"
8+
environment:
9+
K8S_AUTH_API_KEY: "{{ ocp_token }}"
10+
K8S_AUTH_HOST: "{{ ocp_host }}"

ansible/playbooks/deploy.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
- ../vaults/{{ env }}/ocp-token.yml
77
roles:
88
- operator-pipeline
9+
- role: preflight-invalidation
10+
when: env == 'prod' or env == 'stage'
911
environment:
1012
K8S_AUTH_API_KEY: '{{ ocp_token }}'
1113
K8S_AUTH_HOST: '{{ ocp_host }}'
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
preflight_invalidation_namespace: preflight-invalidation
3+
4+
preflight_invalidation_private_key_local_path: ../../vaults/{{ env }}/operator-pipeline.key
5+
preflight_invalidation_private_cert_local_path: ../../vaults/{{ env }}/operator-pipeline.pem
6+
7+
preflight_invalidation_image_pull_spec: quay.io/redhat-isv/operator-pipelines-images:released
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---
2+
- name: Create Namespace
3+
kubernetes.core.k8s:
4+
state: present
5+
apply: true
6+
definition:
7+
kind: Namespace
8+
apiVersion: v1
9+
metadata:
10+
name: "{{ preflight_invalidation_namespace }}"
11+
12+
- name: Create cert secret
13+
no_log: true
14+
kubernetes.core.k8s:
15+
state: present
16+
force: true
17+
namespace: "{{ preflight_invalidation_namespace }}"
18+
definition:
19+
apiVersion: v1
20+
kind: Secret
21+
type: Opaque
22+
metadata:
23+
name: operator-pipeline-certs
24+
labels:
25+
app: operator-pipeline
26+
data:
27+
pyxis-client.key: "{{ lookup('file', preflight_invalidation_private_key_local_path, rstrip=False) | b64encode }}"
28+
pyxis-client.pem: "{{ lookup('file', preflight_invalidation_private_cert_local_path, rstrip=False) | b64encode }}"
29+
30+
- name: Create invalidation CronJob
31+
kubernetes.core.k8s:
32+
state: present
33+
apply: true
34+
namespace: "{{ preflight_invalidation_namespace }}"
35+
definition:
36+
apiVersion: v1
37+
kind: CronJob
38+
metadata:
39+
name: preflight-invalidation-cronjob
40+
spec:
41+
schedule: "0 9 * * MON" # At 09:00 on Monday
42+
jobTemplate:
43+
spec:
44+
template:
45+
spec:
46+
containers:
47+
- name: preflight-invalidator
48+
image: "{{ preflight_invalidation_image_pull_spec }}"
49+
args:
50+
- invalidate-preflight-versions
51+
- --pyxis-url
52+
- "{{ pyxis_url }}"
53+
volumeMounts:
54+
- name: operator-pipeline-certs
55+
mountPath: "/etc/pyxis-ssl"
56+
env:
57+
- name: PYXIS_CERT_PATH
58+
value: "/etc/pyxis-ssl/pyxis-client.pem"
59+
- name: PYXIS_KEY_PATH
60+
value: "/etc/pyxis-ssl/pyxis-client.key"
61+
restartPolicy: OnFailure
62+
volumes:
63+
- name: operator-pipeline-certs
64+
secret:
65+
secretName: operator-pipeline-certs

docs/preflight-invalidation.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Preflight invalidation CronJob
2+
3+
The repository contains a CronJob that updates enabled preflight versions
4+
weekly. https://issues.redhat.com/browse/ISV-4964
5+
6+
After changes, the CronJob can be deployed using Ansible.
7+
8+
```bash
9+
ansible-playbook \
10+
-i ansible/inventory/clusters \
11+
-e "clusters=prod-cluster" \
12+
-e "ocp_token=[TOKEN]" \
13+
-e "env=prod" \
14+
--vault-password-file [PWD_FILE] \
15+
playbooks/preflight-invalidation.yml
16+
```
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
"""
2+
Tool automating invalidation of older versions of Preflight in Pyxis based on
3+
rules in https://issues.redhat.com/browse/ISV-4964.
4+
"""
5+
6+
from datetime import datetime, timedelta
7+
from typing import Any
8+
import sys
9+
import argparse as ap
10+
from dataclasses import dataclass
11+
import urllib.parse
12+
import json
13+
import logging
14+
import pprint
15+
from itertools import islice
16+
from packaging.version import Version
17+
import requests
18+
19+
from requests import Response
20+
21+
from operatorcert import pyxis
22+
23+
logger = logging.getLogger(__name__)
24+
25+
26+
@dataclass
27+
class PreflightVersion:
28+
"""Object representing a Preflight version in Pyxis."""
29+
30+
id: str
31+
created: datetime
32+
version: Version
33+
enabled_for_testing: bool
34+
35+
36+
def parse_versions(data: dict[str, Any]) -> list[PreflightVersion]:
37+
"""Parse raw Pyxis dictionary into PreflightVersion object."""
38+
versions = []
39+
for version in data["data"]:
40+
_id = version["_id"]
41+
enabled = version["enabled_for_testing"]
42+
created = datetime.fromisoformat(version["creation_date"])
43+
version = Version(version["version"])
44+
versions.append(
45+
PreflightVersion(
46+
id=_id, created=created, version=version, enabled_for_testing=enabled
47+
)
48+
)
49+
50+
return versions
51+
52+
53+
def get_version_data_page(url: str, page: int, page_size: int) -> bytes:
54+
"""Get single page of preflight data."""
55+
preflight_filter = (
56+
"name==github.com/redhat-openshift-ecosystem/openshift-preflight"
57+
+ ";enabled_for_testing==true"
58+
)
59+
params = {
60+
"filter": preflight_filter,
61+
"page": page,
62+
"page_size": page_size,
63+
}
64+
65+
resp: Response = pyxis.get(url, params)
66+
if resp.status_code != 200:
67+
logger.warning("Pyxis returned code %s: %s", resp.status_code, resp.content)
68+
raise RuntimeError()
69+
70+
return resp.content
71+
72+
73+
def get_versions(pyxis_url: str, page_size: int = 100) -> list[PreflightVersion]:
74+
"""Get a list of current Preflight versions in Pyxis."""
75+
url = urllib.parse.urljoin(pyxis_url, "v1/tools")
76+
77+
versions: list[PreflightVersion] = []
78+
79+
page = 0
80+
data = json.loads(get_version_data_page(url, page, page_size))
81+
82+
versions.extend(parse_versions(data))
83+
84+
total = int(data["total"])
85+
while len(versions) != total:
86+
page += 1
87+
data = json.loads(get_version_data_page(url, page, page_size))
88+
versions.extend(parse_versions(data))
89+
total = int(data["total"])
90+
91+
return versions
92+
93+
94+
def get_versions_to_disable(versions: list[PreflightVersion]) -> list[PreflightVersion]:
95+
"""Get Preflight versions to disable based on current versions in Pyxis."""
96+
versions.sort(reverse=True, key=lambda v: v.version)
97+
98+
to_update = []
99+
100+
# ignore two newest versions
101+
older_versions = islice(versions, 2, None)
102+
103+
for version in older_versions:
104+
now = datetime.now(version.created.tzinfo)
105+
106+
if now - version.created > timedelta(days=90):
107+
to_update.append(version)
108+
109+
return to_update
110+
111+
112+
def disable_version(pyxis_url: str, version: PreflightVersion) -> None:
113+
"""Disable a Preflight version in Pyxis."""
114+
url = urllib.parse.urljoin(pyxis_url, f"v1/tools/id/{version.id}")
115+
pyxis.patch(url, {"enabled_for_testing": False})
116+
117+
118+
def synchronize_versions(pyxis_url: str, dry_run: bool, log_current: bool) -> None:
119+
"""
120+
Invalidate older versions of Preflight in Pyxis based on rules in
121+
https://issues.redhat.com/browse/ISV-4964
122+
"""
123+
current = get_versions(pyxis_url)
124+
if log_current or dry_run: # pragma: no cover
125+
logger.info("Current versions in Pyxis: %s", pprint.pformat(current))
126+
to_disable = get_versions_to_disable(current)
127+
128+
if dry_run: # pragma: no cover
129+
logger.info(
130+
"Versions to be disabled: %s",
131+
pprint.pformat([(v.id, v.version) for v in to_disable]),
132+
)
133+
return
134+
135+
for version in to_disable:
136+
disable_version(pyxis_url, version)
137+
logger.info(
138+
"Disabled version: %s", pprint.pformat((version.id, version.version))
139+
)
140+
141+
142+
def main() -> int: # pragma: no cover
143+
"""
144+
Invalidate older versions of Preflight in Pyxis based on rules in
145+
https://issues.redhat.com/browse/ISV-4964
146+
"""
147+
logging.basicConfig(level=logging.DEBUG)
148+
parser = ap.ArgumentParser(
149+
description="Tool automating invalidation of older Preflight versions in Pyxis. "
150+
"https://issues.redhat.com/browse/ISV-4964"
151+
)
152+
parser.add_argument(
153+
"-d",
154+
"--dry-run",
155+
action="store_true",
156+
help="print versions to be disabled and exit",
157+
)
158+
parser.add_argument(
159+
"-c", "--log-current", action="store_true", help="log current versions in Pyxis"
160+
)
161+
parser.add_argument(
162+
"--pyxis-url",
163+
default="https://pyxis.engineering.redhat.com/",
164+
help="base URL for Pyxis container metadata API",
165+
)
166+
args = parser.parse_args()
167+
168+
retries = 5
169+
while retries > 0:
170+
try:
171+
synchronize_versions(args.pyxis_url, args.dry_run, args.log_current)
172+
break
173+
except (requests.HTTPError, RuntimeError):
174+
retries -= 1
175+
176+
if retries == 0:
177+
logger.error("Failed to update preflight versions")
178+
return 1
179+
180+
return 0
181+
182+
183+
if __name__ == "__main__": # pragma: no cover
184+
sys.exit(main())

0 commit comments

Comments
 (0)