diff --git a/scripts/src/chartrepomanager/chartrepomanager.py b/scripts/src/chartrepomanager/chartrepomanager.py index 04a1e87f..997d584c 100644 --- a/scripts/src/chartrepomanager/chartrepomanager.py +++ b/scripts/src/chartrepomanager/chartrepomanager.py @@ -1,3 +1,17 @@ +"""This files contains the logic for packaging and releasing the Helm chart in the +hosted Helm repository. It updates the index file and creates GitHub releases. + +A GitHub release (named after the chart+version that is being added) is created under +the following conditions: +* If the chart's source has been provided, it creates an archive (helm package) and + uploads it as a GitHub release. +* If a tarball has been provided, it uploads it directly as a GitHub release. +* No release is created if the user only provided a report file. + +The index entry corresponding to the Chart is either created or updated with the latest +version. +""" + import argparse import shutil import os @@ -28,6 +42,16 @@ def get_modified_charts(api_url): + """Get the category, organization, chart name, and new version corresponding to + the chart being added or modified by this PR. + + Args: + api_url (str): URL of the GitHub PR + + Returns: + (str, str, str, str): category, organization, chart, and version (e.g. partner, + hashicorp, vault, 1.4.0) + """ files = prartifact.get_modified_files(api_url) pattern = re.compile(r"charts/(\w+)/([\w-]+)/([\w-]+)/([\w\.-]+)/.*") for file_path in files: @@ -56,6 +80,18 @@ def get_current_commit_sha(): def check_chart_source_or_tarball_exists(category, organization, chart, version): + """Check if the chart's source or chart's tarball is present + + Args: + category (str): Type of profile (community, partners, or redhat) + organization (str): Name of the organization (ex: hashicorp) + chart (str): Name of the chart (ex: vault) + version (str): The version of the chart (ex: 1.4.0) + + Returns: + (bool, bool): First boolean indicates the presence of the chart's source + Second boolean indicates the presence of the chart's tarball + """ src = os.path.join("charts", category, organization, chart, version, "src") if os.path.exists(src): return True, False @@ -70,13 +106,30 @@ def check_chart_source_or_tarball_exists(category, organization, chart, version) def check_report_exists(category, organization, chart, version): + """Check if a report was provided by the user + + Args: + category (str): Type of profile (community, partners, or redhat) + organization (str): Name of the organization (ex: hashicorp) + chart (str): Name of the chart (ex: vault) + version (str): The version of the chart (ex: 1.4.0) + + Returns: + (bool, str): a boolean set to True if the report.yaml file is present, and the + path to the report.yaml file. + """ report_path = os.path.join( "charts", category, organization, chart, version, "report.yaml" ) return os.path.exists(report_path), report_path -def generate_report(chart_file_name): +def generate_report(): + """Creates report file using the content generated by chart-pr-review + + Returns: + str: Path to the report.yaml file. + """ cwd = os.getcwd() report_content = urllib.parse.unquote(os.environ.get("REPORT_CONTENT")) print("[INFO] Report content:") @@ -88,6 +141,18 @@ def generate_report(chart_file_name): def prepare_chart_source_for_release(category, organization, chart, version): + """Create an archive file of the Chart for the GitHub release. + + When the PR contains the chart's source, we package it using "helm package" and + place the archive file in the ".cr-release-packages" directory. This directory will + contain all assets that should be uploaded as a GitHub Release. + + Args: + category (str): Type of profile (community, partners, or redhat) + organization (str): Name of the organization (ex: hashicorp) + chart (str): Name of the chart (ex: vault) + version (str): The version of the chart (ex: 1.4.0) + """ print( "[INFO] prepare chart source for release. %s, %s, %s, %s" % (category, organization, chart, version) @@ -107,11 +172,27 @@ def prepare_chart_source_for_release(category, organization, chart, version): def prepare_chart_tarball_for_release( category, organization, chart, version, signed_chart ): + """Move the provided tarball (and signing key if needed) to the release directory + + The tarball is moved to the ".cr-release-packages" directory. If the archive has + been signed with "helm package --sign", the provenance file is also included. + + Args: + category (str): Type of profile (community, partners, or redhat) + organization (str): Name of the organization (ex: hashicorp) + chart (str): Name of the chart (ex: vault) + version (str): The version of the chart (ex: 1.4.0) + signed_chart (bool): Set to True if the tarball chart is signed. + + Returns: + str: Path to the public key file used to sign the tarball + """ print( "[INFO] prepare chart tarball for release. %s, %s, %s, %s" % (category, organization, chart, version) ) chart_file_name = f"{chart}-{version}.tgz" + # Path to the provided tarball path = os.path.join( "charts", category, organization, chart, version, chart_file_name ) @@ -150,6 +231,13 @@ def get_key_file(category, organization, chart, version): def push_chart_release(repository, organization, commit_hash): + """Call chart-release to create the GitHub release. + + Args: + repository (str): Name of the Github repository + organization (str): Name of the organization (ex: hashicorp) + commit_hash (str): Hash of the HEAD commit + """ print( "[INFO]push chart release. %s, %s, %s " % (repository, organization, commit_hash) @@ -179,6 +267,14 @@ def push_chart_release(repository, organization, commit_hash): def create_worktree_for_index(branch): + """Create a detached worktree for the given branch + + Args: + branch (str): Name of the Git branch + + Returns: + str: Path to local detached worktree + """ dr = tempfile.mkdtemp(prefix="crm-") upstream = os.environ["GITHUB_SERVER_URL"] + "/" + os.environ["GITHUB_REPOSITORY"] out = subprocess.run( @@ -218,15 +314,19 @@ def create_worktree_for_index(branch): return dr -def create_index_from_chart( - indexdir, repository, branch, category, organization, chart, version, chart_url -): - print( - "[INFO] create index from chart. %s, %s, %s, %s, %s" - % (category, organization, chart, version, chart_url) - ) - path = os.path.join("charts", category, organization, chart, version) - chart_file_name = f"{chart}-{version}.tgz" +def create_index_from_chart(chart_file_name): + """Prepare the index entry for this chart + + Given that a chart tarball could be created (i.e. the user provided either the + chart's source or tarball), the content of Chart.yaml is used for the index entry. + + Args: + chart_file_name (str): Name of the chart's archive + + Returns: + dict: content of Chart.yaml, to be used as index entry. + """ + print("[INFO] create index from chart. %s" % (chart_file_name)) out = subprocess.run( [ "helm", @@ -244,6 +344,31 @@ def create_index_from_chart( def create_index_from_report(category, ocp_version_range, report_path): + """Prepare the index entry for this chart. + + In the case only a report was provided by the user, we need to craft an index entry + for this chart. + + To that end, this functions performs the following actions: + * Get the list of annotations from report file + * Override / set additional annotations: + * Replaces the certifiedOpenShiftVersions annotation with the + testedOpenShiftVersion annotation. + * Adds supportedOpenShiftVersions if not already set. + * Adds (overrides) providerType. + * Use report.medatata.chart as a base for the index entry + * Merge (override) annotations into the index entry' annotations + * Add digest to index entry if known. + + Args: + category (str): Type of profile (community, partners, or redhat) + ocp_version_range (str): Range of supported OCP versions + report_path (str): Path to the report.yaml file + + Returns: + dict: Index entry for this chart + + """ print( "[INFO] create index from report. %s, %s, %s" % (category, ocp_version_range, report_path) @@ -260,7 +385,6 @@ def create_index_from_report(category, ocp_version_range, report_path): else: annotations["charts.openshift.io/providerType"] = category - chart_url = report_info.get_report_chart_url(report_path) chart_entry = report_info.get_report_chart(report_path) if "annotations" in chart_entry: annotations = chart_entry["annotations"] | annotations @@ -271,7 +395,7 @@ def create_index_from_report(category, ocp_version_range, report_path): if "package" in digests: chart_entry["digest"] = digests["package"] - return chart_entry, chart_url + return chart_entry def set_package_digest(chart_entry): @@ -315,7 +439,6 @@ def update_index_and_push( indexdir, repository, branch, - category, organization, chart, version, @@ -324,6 +447,23 @@ def update_index_and_push( pr_number, web_catalog_only, ): + """Update the Helm repository index file + + Args: + indexfile (str): Name of the index file to update (index.yaml or + unpublished-certified-charts.yaml) + indexdir (str): Path to the local worktree + repository (str): Name of the GitHub repository + branch (str): Name of the git branch + organization (str): Name of the organization (ex: hashicorp) + chart (str): Name of the chart (ex: vault) + version (str): The version of the chart (ex: 1.4.0) + chart_url (str): URL of the Chart + chart_entry (dict): Index entry to add + pr_number (str): Git Pull Request ID + web_catalog_only (bool): Set to True if the provider has chosen the Web Catalog + Only option. + """ token = os.environ.get("GITHUB_TOKEN") print(f"Downloading {indexfile}") r = requests.get( @@ -459,6 +599,26 @@ def update_index_and_push( def update_chart_annotation( category, organization, chart_file_name, chart, ocp_version_range, report_path ): + """Untar the helm release that was placed under .cr-release-packages, update the + chart's annotations, and repackage the Helm release. + + In particular, following manipulations are performed on annotations: + * Gets the dict of annotations from the report file. + * Replaces the certifiedOpenShiftVersions annotation with the + testedOpenShiftVersion annotation. + * Adds supportedOpenShiftVersions if not already set. + * Adds (overrides) providerType. + * Adds provider if not already set. + * Merge (overrides) those annotations into the Chart's annotations. + + Args: + category (str): Type of profile (community, partners, or redhat) + organization (str): Name of the organization (ex: hashicorp) + chart_file_name (str): Name of the chart's archive + chart (str): Name of the chart (ex: vault) + ocp_version_range (str): Range of supported OCP versions + report_path (str): Path to the report.yaml file + """ print( "[INFO] Update chart annotation. %s, %s, %s, %s, %s" % (category, organization, chart_file_name, chart, ocp_version_range) @@ -607,7 +767,7 @@ def main(): shutil.copy(report_path, "report.yaml") else: print("[INFO] Generate report") - report_path = generate_report(chart_file_name) + report_path = generate_report() print("[INFO] Updating chart annotation") update_chart_annotation( @@ -621,16 +781,7 @@ def main(): chart_url = f"https://github.com/{args.repository}/releases/download/{organization}-{chart}-{version}/{chart_file_name}" print("[INFO] Helm package was released at %s" % chart_url) print("[INFO] Creating index from chart") - chart_entry = create_index_from_chart( - indexdir, - args.repository, - branch, - category, - organization, - chart, - version, - chart_url, - ) + chart_entry = create_index_from_chart(chart_file_name) else: report_path = os.path.join( "charts", category, organization, chart, version, "report.yaml" @@ -640,9 +791,8 @@ def main(): if signedchart.check_report_for_signed_chart(report_path): public_key_file = get_key_file(category, organization, chart, version) print("[INFO] Creating index from report") - chart_entry, chart_url = create_index_from_report( - category, ocp_version_range, report_path - ) + chart_url = report_info.get_report_chart_url(report_path) + chart_entry = create_index_from_report(category, ocp_version_range, report_path) if not web_catalog_only: current_dir = os.getcwd() @@ -658,7 +808,6 @@ def main(): indexdir, args.repository, branch, - category, organization, chart, version,