[ATMOSPHERE-335] Switch to "chart-vendor" (#1726)
This is an automated cherry-pick of #1719
/assign mnaser
diff --git a/build/sync-charts.py b/build/sync-charts.py
deleted file mode 100644
index a2be651..0000000
--- a/build/sync-charts.py
+++ /dev/null
@@ -1,277 +0,0 @@
-# Copyright (c) 2024 VEXXHOST, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-import asyncio
-import os
-import pathlib
-import textwrap
-from datetime import datetime, timezone
-
-import aiopath
-import aioshutil
-import platformdirs
-from asynctempfile import NamedTemporaryFile
-from gerrit import GerritClient
-from pydantic import BaseModel, HttpUrl, PrivateAttr
-from pydantic_yaml import parse_yaml_file_as, to_yaml_file
-
-
-class ChartRepository(BaseModel):
- url: HttpUrl
-
- @property
- def name(self):
- return self.url.host.replace(".", "-") + self.url.path.replace("/", "-")
-
-
-class ChartPatches(BaseModel):
- gerrit: dict[str, list[int]] = {}
-
-
-class ChartDependency(BaseModel):
- name: str
- repository: HttpUrl
- version: str
-
-
-class ChartRequirements(BaseModel):
- dependencies: list[ChartDependency] = []
-
-
-class ChartLock(BaseModel):
- dependencies: list[ChartDependency] = []
- digest: str
- generated: datetime
-
- class Config:
- json_encoders = {
- "generated": lambda dt: dt.isoformat(),
- }
-
-
-class Chart(BaseModel):
- name: str
- version: str
- repository: ChartRepository
- dependencies: list[ChartDependency] = []
- patches: ChartPatches = ChartPatches()
-
-
-async def patch(input: bytes, path: aiopath.AsyncPath):
- async with NamedTemporaryFile() as temp_file:
- await temp_file.write(
- textwrap.dedent(
- f"""\
- {path.name}/*
- """
- )
- .strip()
- .encode()
- )
- await temp_file.flush()
-
- proc = await asyncio.create_subprocess_shell(
- f"filterdiff -p1 -I {temp_file.name}",
- stdin=asyncio.subprocess.PIPE,
- stdout=asyncio.subprocess.PIPE,
- stderr=asyncio.subprocess.PIPE,
- )
- stdout, stderr = await proc.communicate(input=input)
- if proc.returncode != 0:
- raise Exception(stderr)
-
- async with NamedTemporaryFile() as temp_file:
- await temp_file.write(
- textwrap.dedent(
- f"""\
- {path.name}/Chart.yaml
- {path.name}/values_overrides/*
- """
- )
- .strip()
- .encode()
- )
- await temp_file.flush()
-
- proc = await asyncio.create_subprocess_shell(
- f"filterdiff -p1 -X {temp_file.name}",
- stdin=asyncio.subprocess.PIPE,
- stdout=asyncio.subprocess.PIPE,
- stderr=asyncio.subprocess.PIPE,
- )
- stdout, stderr = await proc.communicate(input=stdout)
- if proc.returncode != 0:
- raise Exception(stderr)
-
- proc = await asyncio.create_subprocess_shell(
- f"patch -p2 -d {path} -E",
- stdin=asyncio.subprocess.PIPE,
- stdout=asyncio.subprocess.PIPE,
- stderr=asyncio.subprocess.PIPE,
- )
- stdout, stderr = await proc.communicate(input=stdout)
- if proc.returncode != 0:
- raise Exception(stdout)
-
-
-class Config(BaseModel):
- charts: list[Chart]
-
- _workspace: pathlib.Path = PrivateAttr(
- default=pathlib.Path(
- platformdirs.user_cache_dir("atmosphere-sync-charts", "vexxhost")
- )
- )
-
- @property
- def repositories(self):
- repositories = []
-
- for chart in self.charts:
- if chart.repository in repositories:
- continue
- repositories.append(chart.repository)
-
- return repositories
-
- async def _helm(self, args: list[str]):
- proc = await asyncio.create_subprocess_shell(
- f"helm {' '.join(args)}",
- env={**dict(os.environ), **{"HOME": str(self._workspace)}},
- )
- await proc.communicate()
- if proc.returncode != 0:
- raise Exception(f"helm {' '.join(args)} failed")
-
- async def _fetch_chart(self, chart: Chart, path="charts"):
- charts_path: aiopath.AsyncPath = aiopath.AsyncPath(path)
- chart_path = charts_path / chart.name
-
- try:
- await aioshutil.rmtree(f"{path}/{chart.name}-{chart.version}")
- except FileNotFoundError:
- pass
-
- try:
- try:
- os.rename(
- f"{path}/{chart.name}", f"{path}/{chart.name}-{chart.version}"
- )
- except FileNotFoundError:
- pass
-
- await self._helm(
- [
- "fetch",
- "--untar",
- f"--destination={path}",
- f"{chart.repository.name}/{chart.name}",
- f"--version={chart.version}",
- ]
- )
- except Exception:
- os.rename(f"{path}/{chart.name}-{chart.version}", f"{path}/{chart.name}")
- raise
-
- try:
- await aioshutil.rmtree(f"{path}/{chart.name}-{chart.version}")
- except FileNotFoundError:
- pass
-
- if chart.dependencies:
- requirements = ChartRequirements(dependencies=chart.dependencies)
- to_yaml_file(f"{path}/{chart.name}/requirements.yaml", requirements)
-
- await asyncio.gather(
- *[
- aioshutil.rmtree(f"{path}/{chart.name}/charts/{req.name}")
- for req in chart.dependencies
- ]
- )
-
- await self._helm(
- ["dependency", "update", "--skip-refresh", f"{path}/{chart.name}"]
- )
-
- await asyncio.gather(
- *[
- aioshutil.unpack_archive(
- f"{path}/{chart.name}/charts/{req.name}-{req.version}.tgz",
- f"{path}/{chart.name}/charts",
- )
- for req in chart.dependencies
- ]
- )
-
- await asyncio.gather(
- *[
- (chart_path / "charts" / f"{req.name}-{req.version}.tgz").unlink()
- for req in chart.dependencies
- ]
- )
-
- for req in chart.dependencies:
- lock = parse_yaml_file_as(
- ChartLock,
- f"{path}/{chart.name}/charts/{req.name}/requirements.lock",
- )
- lock.generated = datetime.min.replace(tzinfo=timezone.utc)
- to_yaml_file(
- f"{path}/{chart.name}/charts/{req.name}/requirements.lock", lock
- )
-
- # Reset the generated time in the lock file to make things reproducible
- lock = parse_yaml_file_as(
- ChartLock, f"{path}/{chart.name}/requirements.lock"
- )
- lock.generated = datetime.min.replace(tzinfo=timezone.utc)
- to_yaml_file(f"{path}/{chart.name}/requirements.lock", lock)
-
- for gerrit, changes in chart.patches.gerrit.items():
- client = GerritClient(base_url=f"https://{gerrit}")
-
- for change_id in changes:
- change = client.changes.get(change_id)
- gerrit_patch = change.get_revision().get_patch(decode=True)
- await patch(input=gerrit_patch.encode(), path=chart_path)
-
- patches_path = charts_path / "patches" / chart.name
- if await patches_path.exists():
- patch_paths = sorted(
- [patch_path async for patch_path in patches_path.glob("*.patch")]
- )
- for patch_path in patch_paths:
- async with patch_path.open(mode="rb") as patch_file:
- patch_data = await patch_file.read()
- await patch(input=patch_data, path=chart_path)
-
- async def fetch_charts(self):
- await asyncio.gather(
- *[
- self._helm(["repo", "add", repo.name, str(repo.url)])
- for repo in self.repositories
- ]
- )
- await self._helm(["repo", "update"])
-
- await asyncio.gather(*[self._fetch_chart(chart) for chart in self.charts])
-
-
-async def main():
- config = parse_yaml_file_as(Config, ".charts.yml")
- await config.fetch_charts()
-
-
-if __name__ == "__main__":
- asyncio.run(main())
diff --git a/tox.ini b/tox.ini
index a361b7d..6e55c35 100644
--- a/tox.ini
+++ b/tox.ini
@@ -29,17 +29,9 @@
[testenv:sync-charts]
skipsdist = True
deps =
- aiopath
- aiofiles!=24.1.0
- aioshutil
- asynctempfile
- GitPython
- platformdirs
- pydantic
- pydantic-yaml
- python-gerrit-api
+ chart-vendor
commands =
- python3 {toxinidir}/build/sync-charts.py
+ chart-vendor
[testenv:linters]
skipsdist = True
@@ -48,7 +40,7 @@
pre-commit
commands =
pre-commit run --all-files --show-diff-on-failure
- python3 {toxinidir}/build/sync-charts.py --check
+ chart-vendor --check
python3 {toxinidir}/build/lint-jobs.py
[testenv:py3]
diff --git a/zuul.d/jobs.yaml b/zuul.d/jobs.yaml
index 7aaa2dd..928ee15 100644
--- a/zuul.d/jobs.yaml
+++ b/zuul.d/jobs.yaml
@@ -2,6 +2,8 @@
name: atmosphere-linters
parent: tox-linters
pre-run: zuul.d/playbooks/linters/pre.yml
+ required-projects:
+ - name: vexxhost/chart-vendor
- job:
name: atmosphere-tox-py3