Skip to content

Commit

Permalink
Allow reading/writing to a manifest file (#21)
Browse files Browse the repository at this point in the history
Allow reading/writing to a manifest file. Writing a manifest file can be
useful to checkpoint all the expected primary artifacts of the cluster
and used for driving tests, where just discovering artifacts form the
cluster may result in unexpectedly missing some (e.g. if a kustomization
is misconfigured)
  • Loading branch information
allenporter authored Feb 6, 2023
1 parent 5c21426 commit f5565cc
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 9 deletions.
68 changes: 61 additions & 7 deletions flux_local/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,29 @@
e.g. such as writing management plan for resources.
"""

import dataclasses
import datetime
from dataclasses import dataclass
from typing import Any
from pathlib import Path
from typing import Any, cast

import aiofiles
import yaml
from pydantic import BaseModel

__all__ = [
"read_manifest",
"write_manifest",
"Manifest",
"Cluster",
"Kustomization",
"HelmRepository",
"HelmRelease",
"HelmChart",
"ManifestException",
]


@dataclass
@dataclasses.dataclass
class HelmChart:
"""A representation of an instantiation of a chart for a HelmRelease."""

Expand Down Expand Up @@ -62,7 +68,7 @@ def chart_name(self) -> str:
return f"{self.repo_namespace}-{self.repo_name}/{self.name}"


@dataclass
@dataclasses.dataclass
class HelmRelease:
"""A representation of a Flux HelmRelease."""

Expand Down Expand Up @@ -93,7 +99,7 @@ def release_name(self) -> str:
return f"{self.namespace}-{self.name}"


@dataclass
@dataclasses.dataclass
class HelmRepository:
"""A representation of a flux HelmRepository."""

Expand Down Expand Up @@ -127,7 +133,7 @@ def repo_name(self) -> str:
return f"{self.namespace}-{self.name}"


@dataclass
@dataclasses.dataclass
class Kustomization:
"""A Kustomization is a set of declared cluster artifacts.
Expand All @@ -154,7 +160,7 @@ def id_name(self) -> str:
return f"{self.path}"


@dataclass
@dataclasses.dataclass
class Cluster:
"""A set of nodes that run containerized applications.
Expand Down Expand Up @@ -202,3 +208,51 @@ class Manifest(BaseModel):

clusters: list[Cluster]
"""A list of Clusters represented in the repo."""

@staticmethod
def parse_yaml(content: str) -> "Manifest":
"""Parse a serialized manifest."""
doc = next(yaml.load_all(content, Loader=yaml.Loader), None)
if not doc or "spec" not in doc:
raise ManifestException("Manifest file malformed, missing 'spec'")
return Manifest(clusters=doc["spec"])

def yaml(self) -> str:
"""Serialize the manifest as a yaml file."""
return cast(
str,
yaml.dump(
{"spec": [dataclasses.asdict(cluster) for cluster in self.clusters]},
sort_keys=False,
explicit_start=True,
),
)


class ManifestException(Exception):
"""Error raised while working with the Manifest."""


async def read_manifest(manifest_path: Path) -> Manifest:
"""Return the contents of a serialized manifest file."""
async with aiofiles.open(str(manifest_path)) as manifest_file:
content = await manifest_file.read()
return Manifest.parse_yaml(content)


async def write_manifest(manifest_path: Path, manifest: Manifest) -> None:
"""Write the specified manifest content to disk."""
content = manifest.yaml()
async with aiofiles.open(str(manifest_path), mode="w") as manifest_file:
await manifest_file.write(content)


async def update_manifest(manifest_path: Path, manifest: Manifest) -> None:
"""Write the specified manifest only if changed."""
async with aiofiles.open(str(manifest_path)) as manifest_file:
content = await manifest_file.read()
new_content = manifest.yaml()
if content == new_content:
return
async with aiofiles.open(str(manifest_path), mode="w") as manifest_file:
await manifest_file.write(new_content)
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = flux-local
version = 0.0.2
version = 0.0.3
description = flux-local is a python library and set of tools for managing a flux gitops repository, with validation steps to help improve quality of commits, PRs, and general local testing.
long_description = file: README.md
long_description_content_type = text/markdown
Expand Down
49 changes: 48 additions & 1 deletion tests/test_manifest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
"""Tests for manifest library."""

import json
from pathlib import Path

import pytest
import yaml

from flux_local.manifest import HelmRelease, HelmRepository
from flux_local.manifest import (
Cluster,
HelmRelease,
HelmRepository,
Manifest,
ManifestException,
read_manifest,
write_manifest,
)

TESTDATA_DIR = Path("tests/testdata/helm-repo")

Expand Down Expand Up @@ -34,3 +44,40 @@ def test_parse_helm_repository() -> None:
assert repo.name == "bitnami"
assert repo.namespace == "flux-system"
assert repo.url == "https://charts.bitnami.com/bitnami"


async def test_read_manifest_invalid_file() -> None:
"""Test reading an invalid manifest file."""
with pytest.raises(ManifestException, match="Manifest file malformed"):
await read_manifest(Path("/dev/null"))


async def test_write_manifest_file() -> None:
"""Test reading an invalid manifest file."""
await write_manifest(Path("/dev/null"), Manifest(clusters=[]))


async def test_read_write_empty_manifest(tmp_path: Path) -> None:
"""Test serializing and reading back a manifest."""
manifest = Manifest(clusters=[])
await write_manifest(tmp_path / "file.yaml", manifest)
new_manifest = await read_manifest(tmp_path / "file.yaml")
assert not new_manifest.clusters


async def test_read_write_manifest(tmp_path: Path) -> None:
"""Test serializing and reading back a manifest."""
manifest = Manifest(
clusters=[Cluster(name="cluster", path="./example", kustomizations=[])]
)
await write_manifest(tmp_path / "file.yaml", manifest)
new_manifest = await read_manifest(tmp_path / "file.yaml")
assert json.loads(new_manifest.json()) == {
"clusters": [
{
"name": "cluster",
"path": "./example",
"kustomizations": [],
},
]
}

0 comments on commit f5565cc

Please sign in to comment.