Skip to content

Commit f4e5ed0

Browse files
authored
Merge pull request #251 from projectsyn/integration-test
Integration test for catalog compilation
2 parents 2b27bbe + 66bbe8b commit f4e5ed0

5 files changed

Lines changed: 303 additions & 2 deletions

File tree

.github/workflows/test.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,38 @@ jobs:
8282
run: echo "/opt/bin" >> $GITHUB_PATH
8383
- name: Run benchmarks on Python ${{ matrix.python-version }}
8484
run: make bench_py${{ matrix.python-version }}
85+
integration:
86+
runs-on: ubuntu-latest
87+
steps:
88+
- uses: actions/checkout@v2
89+
- uses: actions/setup-python@v2
90+
with:
91+
python-version: '3.9'
92+
- name: Install Poetry, setup Poetry virtualenv, and build Kapitan helm bindings
93+
run: |
94+
pip install poetry
95+
poetry env use python3.9
96+
poetry install
97+
poetry run build_kapitan_helm_binding
98+
- name: Install jsonnet-bundler
99+
run: |
100+
mkdir -p /opt/bin && curl -sLo /opt/bin/jb \
101+
https://github.com/jsonnet-bundler/jsonnet-bundler/releases/download/v0.4.0/jb-linux-amd64 \
102+
&& chmod +x /opt/bin/jb
103+
- name: Update PATH
104+
run: echo "/opt/bin" >> $GITHUB_PATH
105+
- name: Pull in SSH deploy key for integration tests
106+
env:
107+
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
108+
run: |
109+
mkdir -p ~/.ssh
110+
echo "github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==" >> ~/.ssh/known_hosts
111+
ssh-agent -a $SSH_AUTH_SOCK > /dev/null
112+
ssh-add - <<< "${{ secrets.CATALOG_DEPLOY_KEY }}"
113+
- name: Run catalog compile integration test
114+
run: make test_integration
115+
env:
116+
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
85117
docs:
86118
runs-on: ubuntu-latest
87119
steps:

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,7 @@ docker:
5757
inject-version:
5858
sed -i "s/^__git_version__.*$$/__git_version__ = '${GITVERSION}'/" commodore/__init__.py
5959
poetry version "${PYVERSION}"
60+
61+
.PHONY: test_integration
62+
test_integration:
63+
poetry run pytest -m integration

commodore/config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ def __init__(
3030
):
3131
self._work_dir = work_dir.resolve()
3232
self.api_url = api_url
33-
self.api_token = None
3433
self.api_token = api_token
3534
self._components = {}
3635
self._config_repos = {}

tests/test_catalog_compile.py

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
import copy
2+
import os
3+
import pytest
4+
import re
5+
import yaml
6+
7+
from datetime import datetime, timedelta
8+
from pathlib import Path
9+
from unittest.mock import patch
10+
from typing import Iterable
11+
12+
import git
13+
14+
from commodore.cluster import Cluster
15+
from commodore.config import Config
16+
17+
import commodore.compile as commodore_compile
18+
19+
20+
@pytest.fixture
21+
def config(tmp_path: Path):
22+
"""
23+
Setup config object for tests
24+
"""
25+
26+
return Config(
27+
tmp_path,
28+
api_url="https://syn.example.com",
29+
api_token="token",
30+
)
31+
32+
33+
cluster_resp = {
34+
"id": "c-test",
35+
"tenant": "t-test",
36+
"displayName": "test-cluster",
37+
"facts": {
38+
"cloud": "local",
39+
"distribution": "k3s",
40+
},
41+
"gitRepo": {
42+
"url": "ssh://git@github.com/projectsyn/test-cluster-catalog.git",
43+
},
44+
}
45+
46+
tenant_resp = {
47+
"id": "t-test",
48+
"displayName": "Test tenant",
49+
"gitRepo": {
50+
"url": "https://github.com/projectsyn/test-tenant.git",
51+
},
52+
"globalGitRepoURL": "https://github.com/projectsyn/commodore-defaults.git",
53+
}
54+
55+
56+
def _mock_load_cluster_from_api(cfg: Config, cluster_id: str):
57+
assert cluster_id == "c-test"
58+
return Cluster(cluster_resp, tenant_resp)
59+
60+
61+
def _verify_target(
62+
target_dir: Path, expected_classes: Iterable[str], tname: str, bootstrap=False
63+
):
64+
tpath = target_dir / f"{tname}.yml"
65+
assert tpath.is_file()
66+
classes = copy.copy(expected_classes)
67+
if not bootstrap:
68+
classes.append(f"components.{tname}")
69+
with open(tpath) as t:
70+
tcontents = yaml.safe_load(t)
71+
assert all(k in tcontents for k in ["classes", "parameters"])
72+
assert tcontents["classes"] == classes
73+
tparams = tcontents["parameters"]
74+
assert "_instance" in tparams
75+
assert tparams["_instance"] == tname
76+
assert bootstrap or (
77+
"kapitan" in tparams
78+
and "vars" in tparams["kapitan"]
79+
and "target" in tparams["kapitan"]["vars"]
80+
and tparams["kapitan"]["vars"]["target"] == tname
81+
)
82+
83+
84+
def _verify_commit_message(
85+
tmp_path: Path,
86+
config: Config,
87+
commit_msg: str,
88+
short_sha_len: int,
89+
catalog_repo: git.Repo,
90+
):
91+
"""
92+
Parse and check catalog commit message
93+
"""
94+
95+
rev_re_fragment = fr"(?P<commit_sha>[0-9a-f]{{{short_sha_len}}})"
96+
97+
component_commit_re = re.compile(
98+
r"^ \* (?P<component_name>[a-z-]+): "
99+
+ r"(?P<component_version>(None|v[0-9]+.[0-9]+.[0-9]+|[a-z0-9]{40})) "
100+
+ fr"\({rev_re_fragment}\)$"
101+
)
102+
global_commit_re = re.compile(fr"^ \* global: {rev_re_fragment}$")
103+
tenant_commit_re = re.compile(fr"^ \* customer: {rev_re_fragment}$")
104+
compile_ts_re = re.compile(r"^Compilation timestamp: (?P<ts>[0-9T.:-]+)$")
105+
106+
global_rev = git.Repo(tmp_path / "inventory/classes/global").head.commit.hexsha[
107+
:short_sha_len
108+
]
109+
tenant_rev = git.Repo(tmp_path / "inventory/classes/t-test").head.commit.hexsha[
110+
:short_sha_len
111+
]
112+
catalog_commit_ts = catalog_repo.head.commit.committed_datetime
113+
114+
assert commit_msg.startswith(
115+
"Automated catalog update from Commodore\n\nComponent commits:\n"
116+
)
117+
commit_msg_lines = commit_msg.split("\n")[3:]
118+
119+
components = config.get_components()
120+
component_count = len(components.keys())
121+
122+
component_lines = commit_msg_lines[:component_count]
123+
for line in component_lines:
124+
m = component_commit_re.match(line)
125+
assert m, f"Unable to parse component commit line {line}"
126+
cname = m.group("component_name")
127+
assert cname in components
128+
c = components[cname]
129+
assert str(c.version) == m.group("component_version")
130+
assert c.repo.head.commit.hexsha[:short_sha_len] == m.group("commit_sha")
131+
132+
# Remaining lines should be config commit shas and compilation timestamp
133+
rem_lines = commit_msg_lines[component_count:]
134+
assert len(rem_lines) == 7
135+
136+
# empty line before configuration commits
137+
assert rem_lines[0] == ""
138+
139+
assert rem_lines[1] == "Configuration commits:"
140+
global_match = global_commit_re.match(rem_lines[2])
141+
assert global_match, "Could not parse global repo commit"
142+
assert global_rev == global_match.group("commit_sha")
143+
tenant_match = tenant_commit_re.match(rem_lines[3])
144+
assert tenant_match, "Could not parse tenant repo commit"
145+
assert tenant_rev == tenant_match.group("commit_sha")
146+
147+
# empty line after config commits
148+
assert rem_lines[4] == ""
149+
150+
compile_ts_match = compile_ts_re.match(rem_lines[5])
151+
assert compile_ts_match
152+
compile_ts_str = compile_ts_match.group("ts")
153+
compile_ts = datetime.fromisoformat(compile_ts_str)
154+
# if compile timestamp doesn't have tzinfo, set same tzinfo as committed ts
155+
if compile_ts.tzinfo is None:
156+
compile_ts = compile_ts.replace(tzinfo=catalog_commit_ts.tzinfo)
157+
print(abs(compile_ts - catalog_commit_ts))
158+
# Commit message timestamp and commit timestamp should be within 1 second of each other
159+
assert abs(compile_ts - catalog_commit_ts) < timedelta(seconds=1)
160+
161+
# last line empty due to trailing \n in commit message
162+
assert rem_lines[6] == ""
163+
164+
165+
@pytest.mark.integration
166+
@patch.object(
167+
commodore_compile,
168+
"load_cluster_from_api",
169+
side_effect=_mock_load_cluster_from_api,
170+
)
171+
def test_catalog_compile(load_cluster, config: Config, tmp_path: Path, capsys):
172+
os.chdir(tmp_path)
173+
cluster_id = "c-test"
174+
expected_components = ["argocd", "metrics-server"]
175+
expected_dirs = [
176+
tmp_path / "catalog",
177+
tmp_path / "catalog/manifests",
178+
tmp_path / "catalog/refs",
179+
tmp_path / "compiled",
180+
tmp_path / "dependencies",
181+
tmp_path / "dependencies/lib",
182+
tmp_path / "dependencies/libs",
183+
tmp_path / "inventory",
184+
tmp_path / "inventory/classes/components",
185+
tmp_path / "inventory/classes/defaults",
186+
tmp_path / "inventory/classes/global",
187+
tmp_path / "inventory/classes/t-test",
188+
tmp_path / "inventory/classes/params",
189+
tmp_path / "inventory/targets",
190+
tmp_path / "vendor",
191+
tmp_path / "vendor/lib",
192+
]
193+
expected_classes = ["params.cluster"]
194+
for c in expected_components:
195+
expected_dirs.extend(
196+
[
197+
tmp_path / "dependencies" / c,
198+
tmp_path / "vendor" / c,
199+
]
200+
)
201+
expected_classes.append(f"defaults.{c}")
202+
expected_classes.append("global.commodore")
203+
204+
config.push = True
205+
commodore_compile.compile(config, cluster_id)
206+
207+
# Verify our mocked load cluster was called
208+
assert load_cluster.called
209+
210+
# Stdout success msg
211+
captured = capsys.readouterr()
212+
assert "Catalog compiled!" in captured.out
213+
214+
# Check config for expected components
215+
assert sorted(config.get_components().keys()) == sorted(expected_components)
216+
217+
# Output dirs
218+
for output_dir in expected_dirs:
219+
assert output_dir.is_dir()
220+
221+
# Verify params.cluster
222+
with open(tmp_path / "inventory/classes/params/cluster.yml") as f:
223+
fcontents = yaml.safe_load(f)
224+
assert "parameters" in fcontents
225+
params = fcontents["parameters"]
226+
assert all(k in params for k in ["cloud", "cluster", "customer", "facts"])
227+
assert "provider" in params["cloud"]
228+
assert params["cloud"]["provider"] == cluster_resp["facts"]["cloud"]
229+
assert all(
230+
k in params["cluster"] for k in ["catalog_url", "dist", "name", "tenant"]
231+
)
232+
assert params["cluster"]["catalog_url"] == cluster_resp["gitRepo"]["url"]
233+
assert params["cluster"]["dist"] == cluster_resp["facts"]["distribution"]
234+
assert params["cluster"]["name"] == cluster_resp["id"]
235+
assert params["cluster"]["tenant"] == cluster_resp["tenant"]
236+
assert "name" in params["customer"]
237+
assert params["customer"]["name"] == cluster_resp["tenant"]
238+
for k, v in params["facts"].items():
239+
assert v == cluster_resp["facts"][k]
240+
241+
# TODO: Targets
242+
target_dir = tmp_path / "inventory/targets"
243+
244+
_verify_target(target_dir, expected_classes, "cluster", bootstrap=True)
245+
for cn in expected_components:
246+
_verify_target(target_dir, expected_classes, cn)
247+
248+
# Catalog checks
249+
catalog_manifests = tmp_path / "catalog/manifests"
250+
found_components = {cn: False for cn in expected_components}
251+
for f in (catalog_manifests / "apps").iterdir():
252+
for c in expected_components:
253+
if c in f.name:
254+
found_components[c] = True
255+
assert all(found_components)
256+
257+
short_sha_len = 6
258+
259+
catalog_repo = git.Repo(tmp_path / "catalog")
260+
commit_msg = catalog_repo.head.commit.message
261+
262+
_verify_commit_message(tmp_path, config, commit_msg, short_sha_len, catalog_repo)
263+
264+
assert not catalog_repo.is_dirty()
265+
assert not catalog_repo.untracked_files

tox.ini

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ deps =
1818
commands = \
1919
pytest \
2020
bench: -m bench \
21-
!bench: -m "not bench" -n auto \
21+
!bench: -m "not bench and not integration" -n auto \
2222
{posargs}
2323

2424

@@ -90,6 +90,7 @@ python_functions =
9090
bench_*
9191
markers =
9292
bench
93+
integration
9394
addopts =
9495
--color=yes
9596
--doctest-modules

0 commit comments

Comments
 (0)