Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for HelmRelease that depends on the local GitRepository #638

Merged
merged 13 commits into from
Apr 27, 2024
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
Loading