Skip to content

Commit

Permalink
Add support for HelmRelease that depends on the local GitRepository (#…
Browse files Browse the repository at this point in the history
…638)

Allow optimistically building GitRepository assuming they point at the
local repo.


Issue #616
  • Loading branch information
allenporter authored Apr 27, 2024
1 parent 78e7031 commit 56088e2
Show file tree
Hide file tree
Showing 35 changed files with 11,392 additions and 33 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: codespell-project/actions-codespell@master
with:
skip: ./tests/tool/__snapshots__/test_diagnostics.ambr,./tests/testdata/cluster9/clusters/dev/flux-system/gotk-components.yaml
- uses: chartboost/[email protected]
- name: Run yamllint
uses: ibiqlik/action-yamllint@v3
Expand Down
3 changes: 3 additions & 0 deletions .krmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.git
local-charts
tests/testdata/cluster9/local-charts
1 change: 1 addition & 0 deletions .yaml-lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
ignore: |
venv
tests/tool/testdata/
tests/testdata/cluster9/local-charts/
extends: default
rules:
truthy:
Expand Down
24 changes: 18 additions & 6 deletions flux_local/helm.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
CRD_KIND,
SECRET_KIND,
REPO_TYPE_OCI,
HELM_REPOSITORY,
GIT_REPOSITORY
)
from .exceptions import HelmException

Expand All @@ -63,11 +65,19 @@
HELM_BIN = "helm"


def _chart_name(repo: HelmRepository, release: HelmRelease) -> str:
def _chart_name(release: HelmRelease, repo: HelmRepository | None) -> str:
"""Return the helm chart name used for the helm template command."""
if repo.repo_type == REPO_TYPE_OCI:
return f"{repo.url}/{release.chart.name}"
return release.chart.chart_name
if release.chart.repo_kind == HELM_REPOSITORY:
if repo.repo_type == REPO_TYPE_OCI:
return f"{repo.url}/{release.chart.name}"
return release.chart.chart_name
elif release.chart.repo_kind == GIT_REPOSITORY:
return release.chart.name
raise HelmException(
f"Unable to find chart source for chart {release.chart.chart_name} "
f"kind {release.chart.repo_kind} for HelmRelease {release.name}"
)



class RepositoryConfig:
Expand Down Expand Up @@ -201,7 +211,9 @@ async def template(
iter([repo for repo in self._repos if repo.repo_name == release.repo_name]),
None,
)
if not repo:
# We'll attempt to make a chart name for a GitRepository below and it will
# be somewhat best effort.
if not repo and release.chart.repo_kind == HELM_REPOSITORY:
raise HelmException(
f"Unable to find HelmRepository for {release.chart.chart_name} for "
f"HelmRelease {release.name} "
Expand All @@ -211,7 +223,7 @@ async def template(
HELM_BIN,
"template",
release.name,
_chart_name(repo, release),
_chart_name(release, repo),
"--namespace",
release.namespace,
]
Expand Down
73 changes: 48 additions & 25 deletions flux_local/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
DEFAULT_NAMESPACE = "flux-system"
VALUE_PLACEHOLDER = "!!PLACEHOLDER!!"
VALUE_B64_PLACEHOLDER = base64.b64encode(VALUE_PLACEHOLDER.encode())
HELM_REPOSITORY = "HelmRepository"
GIT_REPOSITORY = "GitRepository"

REPO_TYPE_DEFAULT = "default"
REPO_TYPE_OCI = "oci"
Expand Down Expand Up @@ -93,14 +95,17 @@ class HelmChart(BaseManifest):
name: str
"""The name of the chart within the HelmRepository."""

version: Optional[str] = field(metadata={"serialize":"omit"})
version: Optional[str] = field(metadata={"serialize": "omit"})
"""The version of the chart."""

repo_name: str
"""The short name of the HelmRepository."""
"""The short name of the repository."""

repo_namespace: str
"""The namespace of the HelmRepository."""
"""The namespace of the repository."""

repo_kind: str = HELM_REPOSITORY
"""The kind of the soruceRef of the repository (e.g. HelmRepository, GitRepository)."""

@classmethod
def parse_doc(cls, doc: dict[str, Any], default_namespace: str) -> "HelmChart":
Expand All @@ -126,6 +131,7 @@ def parse_doc(cls, doc: dict[str, Any], default_namespace: str) -> "HelmChart":
version=version,
repo_name=source_ref["name"],
repo_namespace=source_ref.get("namespace", default_namespace),
repo_kind=source_ref.get("kind", HELM_REPOSITORY),
)

@property
Expand All @@ -149,15 +155,20 @@ class ValuesReference(BaseManifest):
name: str
"""The name of the resource."""

values_key: str = field(metadata=field_options(alias="valuesKey"), default="values.yaml")
values_key: str = field(
metadata=field_options(alias="valuesKey"), default="values.yaml"
)
"""The key in the resource that contains the values."""

target_path: Optional[str] = field(metadata=field_options(alias="targetPath"), default=None)
target_path: Optional[str] = field(
metadata=field_options(alias="targetPath"), default=None
)
"""The path in the HelmRelease values to store the values."""

optional: bool = False
"""Whether the reference is optional."""


@dataclass
class HelmRelease(BaseManifest):
"""A representation of a Flux HelmRelease."""
Expand All @@ -171,10 +182,10 @@ class HelmRelease(BaseManifest):
chart: HelmChart
"""A mapping to a specific helm chart for this HelmRelease."""

values: Optional[dict[str, Any]] = field(metadata={"serialize":"omit"})
values: Optional[dict[str, Any]] = field(metadata={"serialize": "omit"})
"""The values to install in the chart."""

values_from: Optional[list[ValuesReference]] = field(metadata={"serialize":"omit"})
values_from: Optional[list[ValuesReference]] = field(metadata={"serialize": "omit"})
"""A list of values to reference from an ConfigMap or Secret."""

images: list[str] | None = field(default=None)
Expand All @@ -194,7 +205,9 @@ def parse_doc(cls, doc: dict[str, Any]) -> "HelmRelease":
spec = doc["spec"]
values_from: list[ValuesReference] | None = None
if values_from_dict := spec.get("valuesFrom"):
values_from = [ValuesReference.from_dict(subdoc) for subdoc in values_from_dict]
values_from = [
ValuesReference.from_dict(subdoc) for subdoc in values_from_dict
]
return HelmRelease(
name=name,
namespace=namespace,
Expand Down Expand Up @@ -272,7 +285,7 @@ class ClusterPolicy(BaseManifest):
namespace: str | None = None
"""The namespace of the kustomization."""

doc: dict[str, Any] | None = field(metadata={"serialize":"omit"}, default=None)
doc: dict[str, Any] | None = field(metadata={"serialize": "omit"}, default=None)
"""The raw ClusterPolicy document."""

@classmethod
Expand All @@ -299,10 +312,12 @@ class ConfigMap(BaseManifest):
namespace: str | None = None
"""The namespace of the kustomization."""

data: dict[str, Any] | None = field(metadata={"serialize":"omit"}, default=None)
data: dict[str, Any] | None = field(metadata={"serialize": "omit"}, default=None)
"""The data in the ConfigMap."""

binary_data: dict[str, Any] | None = field(metadata={"serialize":"omit"}, default=None)
binary_data: dict[str, Any] | None = field(
metadata={"serialize": "omit"}, default=None
)
"""The binary data in the ConfigMap."""

@classmethod
Expand Down Expand Up @@ -332,10 +347,12 @@ class Secret(BaseManifest):
namespace: str | None = None
"""The namespace of the kustomization."""

data: dict[str, Any] | None = field(metadata={"serialize":"omit"}, default=None)
data: dict[str, Any] | None = field(metadata={"serialize": "omit"}, default=None)
"""The data in the Secret."""

string_data: dict[str, Any] | None = field(metadata={"serialize":"omit"}, default=None)
string_data: dict[str, Any] | None = field(
metadata={"serialize": "omit"}, default=None
)
"""The string data in the Secret."""

@classmethod
Expand Down Expand Up @@ -373,6 +390,7 @@ class SubstituteReference(BaseManifest):
optional: bool = False
"""Whether the reference is optional."""


@dataclass
class Kustomization(BaseManifest):
"""A Kustomization is a set of declared cluster artifacts.
Expand Down Expand Up @@ -406,34 +424,40 @@ class Kustomization(BaseManifest):
secrets: list[Secret] = field(default_factory=list)
"""The list of secrets referenced in the kustomization."""

source_path: str | None = field(metadata={"serialize":"omit"}, default=None)
source_path: str | None = field(metadata={"serialize": "omit"}, default=None)
"""Optional source path for this Kustomization, relative to the build path."""

source_kind: str | None = field(metadata={"serialize":"omit"}, default=None)
source_kind: str | None = field(metadata={"serialize": "omit"}, default=None)
"""The sourceRef kind that provides this Kustomization e.g. GitRepository etc."""

source_name: str | None = field(metadata={"serialize":"omit"}, default=None)
source_name: str | None = field(metadata={"serialize": "omit"}, default=None)
"""The name of the sourceRef that provides this Kustomization."""

source_namespace: str | None = field(metadata={"serialize":"omit"}, default=None)
source_namespace: str | None = field(metadata={"serialize": "omit"}, default=None)
"""The namespace of the sourceRef that provides this Kustomization."""

target_namespace: str | None = field(metadata={"serialize":"omit"}, default=None)
target_namespace: str | None = field(metadata={"serialize": "omit"}, default=None)
"""The namespace to target when performing the operation."""

contents: dict[str, Any] | None = field(metadata={"serialize":"omit"}, default=None)
contents: dict[str, Any] | None = field(
metadata={"serialize": "omit"}, default=None
)
"""Contents of the raw Kustomization document."""

images: list[str] | None = field(default=None)
"""The list of images referenced in the kustomization."""

postbuild_substitute: Optional[dict[str, Any]] = field(metadata={"serialize":"omit"}, default=None)
postbuild_substitute: Optional[dict[str, Any]] = field(
metadata={"serialize": "omit"}, default=None
)
"""A map of key/value pairs to substitute into the final YAML manifest, after building."""

postbuild_substitute_from: Optional[list[SubstituteReference]] = field(metadata={"serialize":"omit"}, default=None)
postbuild_substitute_from: Optional[list[SubstituteReference]] = field(
metadata={"serialize": "omit"}, default=None
)
"""A list of substitutions to reference from an ConfigMap or Secret."""

depends_on: list[str] | None = field(metadata={"serialize":"omit"}, default=None)
depends_on: list[str] | None = field(metadata={"serialize": "omit"}, default=None)
"""A list of namespaced names that this Kustomization depends on."""

@classmethod
Expand Down Expand Up @@ -488,7 +512,6 @@ def namespaced_name(self) -> str:
"""Return the namespace and name concatenated as an id."""
return f"{self.namespace}/{self.name}"


def validate_depends_on(self, all_ks: set[str]) -> None:
"""Validate depends_on values are all correct given the list of Kustomizations."""
depends_on = set(self.depends_on or {})
Expand All @@ -499,7 +522,7 @@ def validate_depends_on(self, all_ks: set[str]) -> None:
missing,
)
self.depends_on = list(depends_on - missing)

def update_postbuild_substitutions(self, substitutions: dict[str, Any]) -> None:
"""Update the postBuild.substitutions in the extracted values and raw doc contents."""
if self.postbuild_substitute is None:
Expand All @@ -512,6 +535,7 @@ def update_postbuild_substitutions(self, substitutions: dict[str, Any]) -> None:
post_build["substitute"] = substitute
substitute.update(substitutions)


@dataclass
class Cluster(BaseManifest):
"""A set of nodes that run containerized applications.
Expand Down Expand Up @@ -567,7 +591,6 @@ class Manifest(BaseManifest):
"""A list of Clusters represented in the repo."""



async def read_manifest(manifest_path: Path) -> Manifest:
"""Return the contents of a serialized manifest file.
Expand Down
14 changes: 14 additions & 0 deletions flux_local/tool/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,22 @@ async def run( # type: ignore[no-untyped-def]
"""Async Action implementation."""
path = kwargs.get("path") or "."

# Parse directories to ignore from `.krmignore`
krmignore = pathlib.Path(path) / ".krmignore"
ignoredirs = []
if krmignore.exists():
with krmignore.open() as fd:
ignoredirs = [
str(pathlib.Path(line.strip())) for line in fd.readlines()
]

errors = []
for root, dirs, files in os.walk(str(path)):
# Ignore any directories that we should not walk
rootpath = str(pathlib.Path(root))
if any([rootpath.startswith(i) for i in ignoredirs]):
continue

for file in files:
if not (file.endswith(".yaml") or file.endswith(".yml")):
continue
Expand Down
Loading

0 comments on commit 56088e2

Please sign in to comment.