Skip to content

Commit 7d5bdea

Browse files
committed
build, salt: Generate OLMv1 manifest
1 parent c40a457 commit 7d5bdea

File tree

11 files changed

+2300
-0
lines changed

11 files changed

+2300
-0
lines changed

buildchain/buildchain/codegen.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,26 @@ def codegen_chart_cert_manager() -> types.TaskDict:
332332
"task_dep": ["check_for:tox", "check_for:helm"],
333333
}
334334

335+
def codegen_olm() -> types.TaskDict:
336+
"""Generate the SLS file for OLMv1 using the OLM render script."""
337+
target_sls = constants.ROOT / "salt/metalk8s/addons/olm/deployed/chart.sls"
338+
cmd = (
339+
f"{constants.OLM_RENDER_CMD} "
340+
f"--output {target_sls}"
341+
)
342+
343+
file_dep = [ constants.OLM_RENDER_SCRIPT ]
344+
345+
return {
346+
"name": "olm",
347+
"title": utils.title_with_subtask_name("CODEGEN"),
348+
"doc": codegen_olm.__doc__,
349+
"actions": [doit.action.CmdAction(cmd, cwd=constants.ROOT)],
350+
"file_dep": file_dep,
351+
"task_dep": ["check_for:tox"],
352+
}
353+
354+
335355

336356
# List of available code generation tasks.
337357
CODEGEN: Tuple[Callable[[], types.TaskDict], ...] = (
@@ -345,6 +365,7 @@ def codegen_chart_cert_manager() -> types.TaskDict:
345365
codegen_chart_prometheus_adapter,
346366
codegen_chart_thanos,
347367
codegen_chart_cert_manager,
368+
codegen_olm,
348369
)
349370

350371

buildchain/buildchain/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
PROMETHEUS_REPOSITORY: str = "quay.io/prometheus"
3333
THANOS_REPOSITORY: str = "quay.io/thanos"
3434
CERT_MANAGER_REPOSITORY: str = "quay.io/jetstack"
35+
OPERATOR_FRAMEWORK_REPOSITORYT: str = "quay.io/operator-framework"
3536

3637
# Paths {{{
3738

@@ -76,6 +77,8 @@
7677
CHART_ROOT: Path = ROOT / "charts"
7778
CHART_RENDER_SCRIPT: Path = CHART_ROOT / "render.py"
7879

80+
OLM_RENDER_SCRIPT: Path = ROOT / "olm/render.py"
81+
7982
# }}}
8083
# Vagrant parameters {{{
8184

@@ -145,6 +148,7 @@ def git_ref() -> Optional[str]:
145148
]
146149

147150
CHART_RENDER_CMD: str = f"tox -e chart-render -- --kube-version {versions.K8S_VERSION}"
151+
OLM_RENDER_CMD: str = f"tox -e olm-render -- -v v{versions.OLM_VERSION}"
148152

149153
# For mypy, see `--no-implicit-reexport` documentation.
150154
__all__ = ["ROOT"]

buildchain/buildchain/image.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,10 @@ def _local_image(name: str, **kwargs: Any) -> targets.LocalImage:
217217
"cert-manager-cainjector",
218218
"cert-manager-acmesolver",
219219
],
220+
constants.OPERATOR_FRAMEWORK_REPOSITORYT: [
221+
"catalogd",
222+
"operator-controller",
223+
],
220224
}
221225

222226
REMOTE_NAMES: Dict[str, str] = {

buildchain/buildchain/salt_tree.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,8 @@ def task(self) -> types.TaskDict:
344344
file_dep=[METALK8S_OPERATOR_MANIFESTS],
345345
),
346346
Path("salt/metalk8s/addons/metalk8s-operator/deployed/init.sls"),
347+
Path("salt/metalk8s/addons/olm/deployed/chart.sls"),
348+
Path("salt/metalk8s/addons/olm/deployed/init.sls"),
347349
Path("salt/metalk8s/addons/prometheus-adapter/deployed/chart.sls"),
348350
Path("salt/metalk8s/addons/prometheus-adapter/deployed/init.sls"),
349351
Path("salt/metalk8s/addons/prometheus-operator/macros.j2"),

buildchain/buildchain/versions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
CONTAINERD_RELEASE: str = "1"
3333
SOSREPORT_RELEASE: str = "2"
3434

35+
OLM_VERSION: str = "1.1.0"
36+
3537

3638
def load_version_information() -> None:
3739
"""Load version information from `VERSION`."""
@@ -225,6 +227,16 @@ def _version_prefix(version: str, prefix: str = "v") -> str:
225227
version="v0.36.1",
226228
digest="sha256:e542959e1b36d5046083d1b64a7049c356b68a44a173c58b3ae7c0c9ada932d5",
227229
),
230+
Image(
231+
name="catalogd",
232+
version=_version_prefix(OLM_VERSION),
233+
digest="sha256:95477a136772765fa2cfb02a6e5fb52bcc167ef7b5333f9e238b0b13b9e72f7b",
234+
),
235+
Image(
236+
name="operator-controller",
237+
version=_version_prefix(OLM_VERSION),
238+
digest="sha256:6272919257e695fcdadf0b57cc0a272084ebf2caca7571e2e5d79d6a56a788fa",
239+
),
228240
# Local images
229241
Image(
230242
name="metalk8s-alert-logger",

olm/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.yaml

olm/render.py

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
#!/usr/bin/env python
2+
3+
"""
4+
This script downloads operator controller and catalogd manifests and `renders` them into a useable
5+
salt chart.
6+
7+
To do so, it:
8+
- downloads manifests from github releases (optional)
9+
- merges duplicate objects
10+
- adds appropriate labels
11+
- changes names and namespaces of cert-manager resources
12+
- swaps image values
13+
"""
14+
15+
import argparse
16+
import io
17+
import pathlib
18+
import re
19+
import requests
20+
import sys
21+
import yaml
22+
23+
CONTROLLER_MANIFEST_URL = "https://github.com/operator-framework/operator-controller/releases/download/{version}/operator-controller.yaml"
24+
25+
CATALOGD_MANIFEST_URL = "https://github.com/operator-framework/catalogd/releases/download/{version}/catalogd.yaml"
26+
27+
REGISTRIES_CONF = """
28+
[[registry]]
29+
prefix = "{% endraw -%}{{ repo.registry_endpoint }}{%- raw %}"
30+
insecure = true
31+
location = "{% endraw -%}{{ repo.registry_endpoint }}{%- raw %}:80"
32+
[[registry]]
33+
prefix = "registry.metalk8s.lan"
34+
insecure = true
35+
location = "{% endraw -%}{{ repo.registry_endpoint }}{%- raw %}:80"
36+
"""
37+
38+
39+
START_BLOCK = """
40+
#!jinja | metalk8s_kubernetes
41+
42+
{%- from "metalk8s/map.jinja" import repo with context %}
43+
{%- from "metalk8s/repo/macro.sls" import build_image_name with context %}
44+
45+
{% raw %}
46+
"""
47+
48+
END_BLOCK = """
49+
{% endraw %}
50+
"""
51+
52+
class DownloadError(Exception):
53+
pass
54+
55+
def semver_regex_type(value):
56+
pat = re.compile(r"^v\d+\.\d+\.\d+$")
57+
if not pat.match(value):
58+
raise argparse.ArgumentTypeError("invalid value, must be ~v1.1.2")
59+
return value
60+
61+
def download_source_manifest(version):
62+
controller_response = requests.get(CONTROLLER_MANIFEST_URL.format(version=version))
63+
catalogd_response = requests.get(CATALOGD_MANIFEST_URL.format(version=version))
64+
if controller_response.status_code == 200 and catalogd_response.status_code == 200:
65+
return controller_response.content, catalogd_response.content
66+
raise DownloadError("Problem fetching catalogd or operator controller manifests")
67+
68+
"""
69+
Merge source manifests on top of destination
70+
"""
71+
def merge(source, destination):
72+
def _dict_merge(src, dst):
73+
for k, v in src.items():
74+
if k in dst and isinstance(dst[k], dict) and isinstance(v, dict):
75+
_dict_merge(v, dst[k])
76+
else:
77+
dst[k] = v
78+
to_add = []
79+
# check for each doc in source
80+
for sdoc in source:
81+
# does it exist in the destination docs ?
82+
found = False
83+
for ddoc in destination:
84+
if ddoc['kind'] == sdoc['kind'] and ddoc['metadata']['name'] == sdoc['metadata']['name']:
85+
found = True
86+
_dict_merge(sdoc, ddoc)
87+
if not found:
88+
to_add.append(sdoc)
89+
destination.extend(to_add)
90+
return destination
91+
92+
def add_labels(manifest, version):
93+
for doc in manifest:
94+
doc['metadata'].setdefault('labels', {}).update({
95+
"app.kubernetes.io/instance": "olm",
96+
"app.kubernetes.io/managed-by": "salt",
97+
"app.kubernetes.io/part-of": "metalk8s",
98+
"app.kubernetes.io/version": str(version),
99+
"heritage": "metalk8s",
100+
})
101+
return manifest
102+
103+
def add_tolerations(manifest):
104+
for doc in manifest:
105+
if doc['kind'] == "Deployment":
106+
doc['spec']['template']['spec'].setdefault('tolerations', []).extend([
107+
{
108+
"key": "node-role.kubernetes.io/bootstrap",
109+
"operator": "Exists",
110+
"effect": "NoSchedule",
111+
},
112+
{
113+
"key": "node-role.kubernetes.io/infra",
114+
"operator": "Exists",
115+
"effect": "NoSchedule",
116+
},
117+
])
118+
return manifest
119+
120+
def add_registries_conf(manifest):
121+
manifest.append({
122+
"apiVersion": "v1",
123+
"kind": "ConfigMap",
124+
"metadata": {
125+
"name": "registries-conf",
126+
"namespace": "olmv1-system",
127+
},
128+
"data": {
129+
"registries.conf": REGISTRIES_CONF,
130+
},
131+
})
132+
for doc in manifest:
133+
if doc['kind'] == "Deployment":
134+
doc['spec']['template']['spec']['volumes'].append({
135+
"name": "registries-conf",
136+
"configMap": {
137+
"name": "registries-conf",
138+
},
139+
})
140+
doc['spec']['template']['spec']['containers'][0]['volumeMounts'].append({
141+
"name": "registries-conf",
142+
"mountPath": "/etc/containers/",
143+
})
144+
return manifest
145+
146+
def add_node_selector(manifest):
147+
for doc in manifest:
148+
if doc['kind'] == "Deployment":
149+
doc['spec']['template']['spec'].setdefault('nodeSelector', {}).update({
150+
"kubernetes.io/os": "linux",
151+
"node-role.kubernetes.io/infra": "",
152+
})
153+
return manifest
154+
155+
def fixup_certmanager(manifest):
156+
for doc in manifest:
157+
if doc['apiVersion'] == "cert-manager.io/v1" and \
158+
doc['metadata'].get('namespace') == 'cert-manager':
159+
doc['metadata']['namespace'] = 'metalk8s-certs'
160+
if doc['kind'] == "MutatingWebhookConfiguration":
161+
doc['metadata']['annotations']['cert-manager.io/inject-ca-from-secret']\
162+
= "metalk8s-certs/olmv1-ca"
163+
return manifest
164+
165+
class multiline_string(str):
166+
pass
167+
168+
def represent_multiline_string(dumper, data):
169+
scalar = yaml.SafeDumper.represent_str(dumper, data)
170+
scalar.style = "|"
171+
return scalar
172+
173+
yaml.SafeDumper.add_representer(multiline_string, represent_multiline_string)
174+
175+
def render(manifest):
176+
def _fix_strings(obj):
177+
if isinstance(obj, dict):
178+
return dict((k, _fix_strings(v)) for (k, v) in obj.items())
179+
elif isinstance(obj, list):
180+
return [ _fix_strings(elem) for elem in obj ]
181+
elif isinstance(obj, str):
182+
if "\n" in obj:
183+
value = "\n".join(
184+
line for line in obj.splitlines() if not re.match(r"^\s*$", line)
185+
)
186+
return multiline_string(value)
187+
return obj
188+
else:
189+
return obj
190+
manifest = _fix_strings(manifest)
191+
out = START_BLOCK.lstrip()
192+
stream = io.StringIO()
193+
yaml.safe_dump_all(
194+
manifest,
195+
stream,
196+
default_flow_style=False,
197+
)
198+
stream.seek(0)
199+
out += re.sub(
200+
r"image: quay.io/operator-framework/(?P<image>.*):(?P<tag>.*)",
201+
r'image: {% endraw -%}{{ build_image_name("\g<image>", False) }}{%- raw %}:\g<tag>',
202+
stream.read(),
203+
)
204+
out += END_BLOCK
205+
return out
206+
207+
def main():
208+
parser = argparse.ArgumentParser()
209+
parser.add_argument('-v', '--version', type=semver_regex_type)
210+
parser.add_argument('-o', '--output', type=pathlib.Path)
211+
args = parser.parse_args()
212+
213+
# 1 - download the manifests
214+
controller_manifest, catalogd_manifest = download_source_manifest(version=args.version)
215+
216+
# 1.5 - interpret yaml
217+
controller = list(yaml.safe_load_all(controller_manifest))
218+
catalogd = list(yaml.safe_load_all(catalogd_manifest))
219+
220+
# 2 - merge manifests
221+
manifest = merge(catalogd, controller)
222+
223+
# 3- add labels, tolerations, nodeSelector
224+
manifest = add_labels(manifest, args.version)
225+
manifest = add_tolerations(manifest)
226+
manifest = add_node_selector(manifest)
227+
manifest = add_registries_conf(manifest)
228+
229+
# 4- Fix cert-manager objects
230+
manifest = fixup_certmanager(manifest)
231+
232+
# 5- render yaml with new images
233+
rendered = render(manifest)
234+
235+
if args.output:
236+
with open(args.output, "w") as fd:
237+
fd.write(rendered)
238+
else:
239+
sys.stdout.write(rendered)
240+
241+
242+
if __name__ == "__main__":
243+
main()

0 commit comments

Comments
 (0)