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 pushing Helm charts to OCI registries, rather than just Github Pages-based chart repos. #187

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ charts:
# --reset flag. It defaults to "0.0.1-set.by.chartpress". This is a valid
# SemVer 2 version, which is required for a helm lint command to succeed.
resetVersion: 1.2.3-dev
# Base path, relative to `chartpress.yaml`, where named charts are kept.
# Default is same directory as `chartpress.yaml`
basePath: ./helmcharts

# baseVersion sets the base version for development tags,
# instead of using the latest tag from `git describe`.
Expand All @@ -189,6 +192,15 @@ charts:
repo:
git: jupyterhub/helm-chart
published: https://jupyterhub.github.io/helm-chart

# Publishing Helm charts to OCI registries (with a custom path prefix) is also supported,
# via defining an `oci` key under `repo`.
# For example, the following will push a chart named `binderhub` to Github's OCI registry under the path
# `ghcr.io/jupyterhub/helm-charts/binderhub`
# repo:
# oci: ghcr.io/jupyterhub
# prefix: helm-charts

# Additional paths that when modified should lead to an updated Chart.yaml
# version, other than the chart directory in <chart name> or any path that
# influence the images of the chart. These paths should be set relative to
Expand Down
143 changes: 131 additions & 12 deletions chartpress.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,17 @@ def _get_all_image_paths(name, options):
return list(set(paths))


def _get_chart_base_path(options):
"""
Return the basePath which will be prepended to the chart name when loading the chart directory,
or an empty value, meaning the chart directory is assumed to be in the same root as `chartpress.yaml`.
"""
if options.get("basePath"):
return options["basePath"]
else:
return ""


def _get_all_chart_paths(options):
"""
Returns the unique paths that when changed should trigger a version update
Expand Down Expand Up @@ -697,7 +708,7 @@ def build_images(
return values_file_modifications


def _update_values_file_with_modifications(name, modifications):
def _update_values_file_with_modifications(name, modifications, base_path):
"""
Update <name>/values.yaml file with a dictionary of modifications with its
root level keys representing a path within the values.yaml file.
Expand All @@ -715,7 +726,7 @@ def _update_values_file_with_modifications(name, modifications):
}
}
"""
values_file = os.path.join(name, "values.yaml")
values_file = os.path.join(base_path, name, "values.yaml")

with open(values_file) as f:
values = yaml.load(f)
Expand Down Expand Up @@ -799,6 +810,7 @@ def build_chart(
long=False,
strict_version=False,
base_version=None,
base_path="",
):
"""
Update Chart.yaml's version, using specified version or by constructing one.
Expand All @@ -818,7 +830,7 @@ def build_chart(
- 0.9.0
"""
# read Chart.yaml
chart_file = os.path.join(name, "Chart.yaml")
chart_file = os.path.join(base_path, name, "Chart.yaml")
with open(chart_file) as f:
chart = yaml.load(f)

Expand All @@ -844,6 +856,101 @@ def build_chart(
return version


def publish_chart_oci(
chart_name,
chart_version,
chart_base,
chart_oci_repo,
chart_oci_prefix,
force=False,
):
"""
Update a Helm chart stored in an OCI registry (e.g. ghcr.io).

The strategy adopted to do this is:

1. Clone the Helm chart registry as found in the gh-pages branch of a git
reposistory.
2. If --force-publish-chart isn't specified, then verify that we won't
overwrite an existing chart version.
3. Create a temporary directory and `helm package` the chart into a file
within this temporary directory now only containing the chart .tar file.
4. Generate a index.yaml with `helm repo index` based on charts found in the
temporary directory folder (a single one), and then merge in the bigger
and existing index.yaml from the cloned Helm chart registry using the
--merge flag.
5. Copy the new index.yaml and packaged Helm chart .tar into the gh-pages
branch, commit it, and push it back to the origin remote.

Note that if we would add the new chart .tar file next to the other .tar
files and use `helm repo index` we would recreate `index.yaml` and update
all the timestamps etc. which is something we don't want. Using `helm repo
index` on a directory with only the new chart .tar file allows us to avoid
this issue.

Also note that the --merge flag will not override existing entries to the
fresh index.yaml file with the index.yaml from the --merge flag. Due to
this, it is as we would have a --force-publish-chart by default.
"""

# clone/fetch the Helm chart repo and checkout its gh-pages branch, note the
# use of cwd (current working directory)

chart_dir = f"{chart_base}/{chart_name}"
_check_call(["git", "fetch"], cwd=chart_dir, echo=True)

# check if a chart with the same name and version has already been published. If
# there is, the behaviour depends on `--force-publish-chart`
# and chart_version and make a decision based on the --force-publish-chart
# flag if that is the case, but always log what's done

try:
_check_call(
[
"helm",
"show",
"chart",
"oci://" + chart_oci_repo + "/" + chart_oci_prefix + "/" + chart_name,
"--version",
chart_version,
]
)
except subprocess.CalledProcessError:
_log(f"Chart of version {chart_version} not already published, continuing.")
else:
if force:
_log(f"Chart of version {chart_version} already exists, overwriting it.")
else:
_log(
f"Skipping chart publishing of version {chart_version}, it is already published"
)
return

# package the latest version into a temporary directory
# and run helm repo index with --merge to update index.yaml
# without refreshing all of the timestamps
with TemporaryDirectory() as td:
_check_call(
[
"helm",
"package",
chart_dir,
"--dependency-update",
"--destination",
td + "/",
]
)

_check_call(
[
"helm",
"push",
os.path.join(td, chart_name + "-" + chart_version + ".tgz"),
"oci://" + chart_oci_repo + "/" + chart_oci_prefix,
]
)


def publish_pages(
chart_name,
chart_version,
Expand Down Expand Up @@ -1188,6 +1295,7 @@ def main(argv=None):
if base_version:
base_version = _check_base_version(base_version)

chart_base_path = _get_chart_base_path(chart)
if not args.list_images:
# update Chart.yaml with a version
chart_version = build_chart(
Expand All @@ -1197,6 +1305,7 @@ def main(argv=None):
base_version=base_version,
long=args.long,
strict_version=args.publish_chart,
base_path=chart_base_path,
)

if "images" in chart:
Expand Down Expand Up @@ -1235,19 +1344,29 @@ def main(argv=None):

# update values.yaml
_update_values_file_with_modifications(
chart["name"], values_file_modifications
chart["name"], values_file_modifications, chart_base_path
)

# publish chart
if args.publish_chart:
publish_pages(
chart_name=chart["name"],
chart_version=chart_version,
chart_repo_github_path=chart["repo"]["git"],
chart_repo_url=chart["repo"]["published"],
extra_message=args.extra_message,
force=args.force_publish_chart,
)
if "oci" in chart["repo"]:
publish_chart_oci(
chart_name=chart["name"],
chart_version=chart_version,
chart_base=chart_base_path,
chart_oci_repo=chart["repo"]["oci"],
chart_oci_prefix=chart["repo"]["prefix"],
force=args.force_publish_chart,
)
if "git" in chart["repo"]:
publish_pages(
chart_name=chart["name"],
chart_version=chart_version,
chart_repo_github_path=chart["repo"]["git"],
chart_repo_url=chart["repo"]["published"],
extra_message=args.extra_message,
force=args.force_publish_chart,
)


if __name__ == "__main__":
Expand Down