diff --git a/README.md b/README.md index 46ea11b..47cfd75 100644 --- a/README.md +++ b/README.md @@ -308,6 +308,7 @@ GITHUB_OUTPUT=debug.out HELM_FILES="${helm_files[@]}" TEST=1 ./flux-helm-diff.sh | `dcgm-exporter` | Chart added in `head` that doesn't exist in `base` | Diff shows entire rendered template as added | | `metaflow` | Very non-standard way of publishing charts (not sure if should be supported) | TBD | | `nvidia-device-plugin` | HelmRepository (using `https`), minor chart version bump | Diff (with potentially breaking `nodeAffinity`) | +| `podinfo` | Unknown repository type (`HelmTypoRepository`) | `Unrecognised repo type` | | `weave-gitops-helm2oci` | Repository type changed from HelmRepository (type `oci`) to OCIRepository | No changes | | `weave-gitops-helmrepo` | HelmRepository with type `oci` | Diff | | `weave-gitops-ocirepo` | OCIRepository | Diff | diff --git a/flux-helm-diff.sh b/flux-helm-diff.sh index d2fc530..93360f0 100755 --- a/flux-helm-diff.sh +++ b/flux-helm-diff.sh @@ -2,23 +2,44 @@ set -eu -o pipefail helm_files=(${HELM_FILES[@]}) -if [ "${#helm_files[@]}" == "0" ]; then +if [[ "${#helm_files[@]}" == "0" ]]; then echo "No Helm files specified, nothing to do" exit fi echo "${#helm_files[@]} Helm file(s) to render: ${helm_files[*]}" +output_msg() { + if [[ -z "${2}" ]]; then + echo "Need severity and message text" >&2 + return 1 + fi + { + # Alert level - https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts + echo "> [!${1}]" + shift + + # Message line(s) + for msg in "$@"; do + echo "> ${msg}" + echo '>' + done + echo + } >> "$GITHUB_OUTPUT" +} + helm_template() { set -eu -o pipefail - if [ -z "${1}" ]; then - echo "Error: Need file name to template" >&2 - return 2 - fi # 'head' or 'base' ref - used for logging output ref="${1%%/*}" - # Set test = to run against Helm teplates under test/ + if [[ -z "${1}" ]]; then + echo "Error: Need ${ref} file name to template" >&2 + output_msg CAUTION "Error: Need \`${ref}\` file name to template" + return 1 + fi + + # Set test = to run against Helm teplates under test/ directory if [ -z "${TEST:-}" ]; then helm_file="${1}" else @@ -26,12 +47,22 @@ helm_template() { fi if [ ! -f "${helm_file}" ]; then - # echo "Warn: File \"${helm_file}\" not found, skipping" - echo "File \"${helm_file}\" not found, skipping" >&2 - return 1 + echo "${ref} file \"${helm_file}\" not found" >&2 + if [[ "${ref}" == "base" ]]; then + output_msg TIP "File \`${helm_file}\` not found in \`${ref}\` ref, looks like a new Helm file" + return + else + output_msg CAUTION "Error: File \`${helm_file}\` not found in \`${ref}\` ref, cannot produce diff" + return 1 + fi fi - # Determine repo type - HelmRepository or OCIRepository + # Extracting chart properties + release_name=$(yq '. | select(.kind == "HelmRelease").metadata.name' "${helm_file}") + release_namespace=$(yq '. | select(.kind == "HelmRelease").metadata.namespace' "${helm_file}") + chart_values=$(yq '. | select(.kind == "HelmRelease").spec.values' "${helm_file}") + + # Determine repo type # https://fluxcd.io/flux/components/source/helmrepositories/ # https://fluxcd.io/flux/components/source/ocirepositories/ # https://fluxcd.io/flux/components/source/gitrepositories/ @@ -39,8 +70,10 @@ helm_template() { repo_type=helm repo_name=$(yq '. | select(.kind == "HelmRelease").spec.chart.spec.sourceRef.name' "${helm_file}") repo_url=$(yq '. | select(.kind == "HelmRepository").spec.url' "${helm_file}") + chart_name=$(yq '. | select(.kind == "HelmRelease").spec.chart.spec.chart' "${helm_file}") chart_version=$(yq '. | select(.kind == "HelmRelease").spec.chart.spec.version' "${helm_file}") + if [[ "${repo_url}" = "oci://"* ]]; then url="${repo_url}/${chart_name}" # Syntax for chart repos is different from OCI repos (as HelmRepo kind) else @@ -50,8 +83,10 @@ helm_template() { elif [[ "OCIRepository" == "$(yq '. | select(.kind == "HelmRelease").spec.chartRef.kind' "${helm_file}")" ]]; then repo_type=oci repo_name=$(yq '. | select(.kind == "HelmRelease").spec.chartRef.name' "${helm_file}") + chart_name="${repo_name}" chart_version=$(yq '. | select(.kind == "OCIRepository").spec.ref.tag' "${helm_file}") + url=$(yq '. | select(.kind == "OCIRepository").spec.url' "${helm_file}") elif [[ "GitRepository" == "$(yq '. | select(.kind == "HelmRelease").spec.chart.spec.sourceRef.kind' "${helm_file}")" ]]; then @@ -59,31 +94,63 @@ helm_template() { repo_name=$(yq '. | select(.kind == "HelmRelease").spec.chart.spec.sourceRef.name' "${helm_file}") repo_url=$(yq '. | select(.kind == "GitRepository").spec.url' "${helm_file}") repo_tag=$(yq '. | select(.kind == "GitRepository").spec.ref.tag' "${helm_file}") + chart_name="${repo_name}" - chart_path=$(yq '. | select(.kind == "HelmRelease").spec.chart.spec.chart' "${helm_file}") + chart_version="${repo_tag}" + chart_rel_path=$(yq '. | select(.kind == "HelmRelease").spec.chart.spec.chart' "${helm_file}") - # Download and extract release artifact - may only work with GitHub? - release_id="${repo_name}_${repo_tag}" - mkdir -p "./tmp/${release_id}" - curl --no-progress-meter -Lo "${release_id}.tar.gz" "${repo_url}/archive/refs/tags/${repo_tag}.tar.gz" - tar -xzf "${release_id}.tar.gz" --directory "./tmp/${release_id}" - rm "./${release_id}.tar.gz" || true + url="${repo_url}/archive/refs/tags/${repo_tag}.tar.gz" - # Find the chart directory - find_chart_path=$(echo "${chart_path}" | sed -e 's|^./|/|' -e 's|/$||') - url=$(find "./tmp/${release_id}" -type d -path "*${find_chart_path}" | head -n 1) + else + echo "Unrecognised ${ref} repo type" >&2 + if [[ "${ref}" == "base" ]]; then + output_msg TIP "Unable to determine \`${ref}\` repo type, not rendering template" + return + else + output_msg CAUTION "Error: Unable to determine \`${ref}\` repo type, cannot produce diff" + return 1 + fi + fi + # Download chart + release_id="${chart_name}-${chart_version}" + chart_temp_path="./tmp/${release_name}-${release_id}-${ref}" + mkdir -p "${chart_temp_path}" + if [[ "${repo_type}" != "git" ]]; then + # Syntax for pull Helm charts is different for OCI repos + if [[ "${url}" = "https://"* ]]; then + helm_pull_args=("${chart_name}" --repo "${url}") # treat as array, to avoid adding single-quotes + elif [[ "${url}" = "oci://"* ]]; then + helm_pull_args=("${url}") # treat as array, to avoid adding single-quotes + else + echo "Unrecognised ${ref} repo type. Again. This should already be caught, so this should never happen.">&2 + return 1 + fi + chart_file="${chart_temp_path}/${release_id}.tgz" + helm pull ${helm_pull_args[@]} --version "${chart_version}" -d "${chart_temp_path}" || { + echo "Helm failed to pull \"${url}\" to \"${chart_temp_path}\"" >&2 + output_msg CAUTION "Helm failed to pull \`${url}\` to \`${chart_temp_path}\`" + return 1 + } else - echo "Unable to determine repo type, skipping" - echo "Unable to determine repo type, skipping" >&2 - return 2 + # Probably only works with GitHub + chart_file="${chart_temp_path}/asset.tgz" + curl --no-progress-meter -Lo "${chart_file}" "${url}" || { + echo "cURL failed to download \"${url}\" to \"${chart_file}\"" >&2 + output_msg CAUTION "cURL failed to download \`${url}\` to \`${chart_file}\`" + return 1 + } fi - # Extracting chart properties - release_name=$(yq '. | select(.kind == "HelmRelease").metadata.name' "${helm_file}") - release_namespace=$(yq '. | select(.kind == "HelmRelease").metadata.namespace' "${helm_file}") - chart_values=$(yq '. | select(.kind == "HelmRelease").spec.values' "${helm_file}") - chart_version="${chart_version:-N/A}" # not relevant for local charts, e.g. downloaded via GitRepository + # Extract chart + tar -xzf "${chart_file}" --directory "${chart_temp_path}" + rm "${chart_file}" || true + if [[ "${repo_type}" == "git" ]]; then + find_chart_path=$(echo "${chart_rel_path}" | sed -e 's|^./|/|' -e 's|/$||') + chart_path=$(find "${chart_temp_path}" -type d -path "*${find_chart_path}" | head -n 1) + else + chart_path="${chart_temp_path}/${chart_name}" + fi # Use Capabilities.APIVersions mapfile -t api_versions < <(yq '. | foot_comment' "${helm_file}" | yq '.helm-api-versions[]') @@ -98,22 +165,22 @@ helm_template() { echo "${ref} release namespace: ${release_namespace}" >&2 echo "${ref} API versions: $(IFS=,; echo "${api_versions[*]}")" >&2 - # Syntax for chart repos is different from OCI repos (as HelmRepo kind) - if [[ "${url}" = "https://"* ]]; then - chart_args=("${chart_name}" --repo "${url}" --version "${chart_version}") # treat as array, to avoid adding single-quotes - elif [[ "${url}" = "oci://"* ]]; then - chart_args=("${url}" --version "${chart_version}") # treat as array, to avoid adding single-quotes - else - # Assume local path (i.e. GitRepository) - chart_args=("${url}") # treat as array, to avoid adding single-quotes - fi + # Inspect rendered manifests + grep -R --include='*.yaml' --include='*.yml' --include='*.tpl' ".Capabilities.APIVersions" "${chart_temp_path}" > /dev/null && { + echo "Warning: Chart uses \".Capabilities.APIVersions\"" >&2 + output_msg WARNING "Chart in \`${ref}\` ref uses the \`.Capabilities.APIVersions\` [built-in template object](https://helm.sh/docs/chart_template_guide/builtin_objects/), which can affect rendered manifests." \ + "See [Flux Helm Diff read-me](https://github.com/marketplace/actions/flux-helm-diff#dry-runningemulating-api-capabilities) for details and workaround." + } # Render template - template_out=$(helm template "${release_name}" ${chart_args[@]} -n "${release_namespace}" -f <(echo "${chart_values}") --api-versions "$(IFS=,; echo "${api_versions[*]}")" 2>&1) || { - echo "$template_out" + return_code=0 + template_out=$(helm template "${release_name}" "${chart_path}" -n "${release_namespace}" -f <(echo "${chart_values}") --api-versions "$(IFS=,; echo "${api_versions[*]}")" 2>&1) || return_code=$? + rm -rf "${chart_temp_path}" + if [ $return_code -ne 0 ]; then echo "$template_out" >&2 - return 2 - } + output_msg CAUTION "Error rendering \`${ref}\` ref: \`${template_out}\`" + return 1 + fi # Cleanup template, removing comments, output template_clean=$(yq -P 'sort_keys(..) comments=""' <(echo "${template_out}")) @@ -126,48 +193,39 @@ echo "## Flux Helm diffs" >> "$GITHUB_OUTPUT" any_failed=0 for helm_file in "${helm_files[@]}"; do + # Begin output echo -e "\nProcessing file \"$helm_file\"" - echo >> "$GITHUB_OUTPUT" - echo "### ${helm_file}" >> "$GITHUB_OUTPUT" + { + echo + echo "### \`${helm_file}\`" + echo + } >> "$GITHUB_OUTPUT" # Template before - return_code=0 - base_out=$(helm_template "base/${helm_file}") || return_code=$? - if [ $return_code -eq 2 ]; then # Ignore files skipped - { - echo '```' - echo "Error rendering base ref:" - echo "${base_out}" - echo '```' - } >> "$GITHUB_OUTPUT" + base_out=$(helm_template "base/${helm_file}") || { any_failed=1 continue - fi + } # Template after - return_code=0 - head_out=$(helm_template "head/${helm_file}") || return_code=$? - if [ $return_code -ne 0 ]; then - { - echo '```' - echo "Error rendering head ref:" - echo "${head_out}" - echo '```' - } >> "$GITHUB_OUTPUT" + head_out=$(helm_template "head/${helm_file}") || { any_failed=1 continue - fi + } # Template diff diff_out=$(diff --unified=5 <(echo "${base_out}") <(echo "${head_out}")) || true echo "Diff has $(echo "$diff_out" | wc -l) line(s)" - [ -z "${diff_out}" ] && diff_out="No changes" - { + if [[ -z "${diff_out}" ]]; then + echo '> [!NOTE]' + echo '> No changes' + else echo '```diff' echo "${diff_out}" echo '```' - } >> "$GITHUB_OUTPUT" + fi >> "$GITHUB_OUTPUT" + done { diff --git a/test/base/infrastructure/base/podinfo/helm.yaml b/test/base/infrastructure/base/podinfo/helm.yaml new file mode 100644 index 0000000..abb922f --- /dev/null +++ b/test/base/infrastructure/base/podinfo/helm.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmTypoRepository +metadata: + name: podinfo + namespace: default +spec: + interval: 5m + url: https://stefanprodan.github.io/podinfo diff --git a/test/head/infrastructure/base/podinfo/helm.yaml b/test/head/infrastructure/base/podinfo/helm.yaml new file mode 100644 index 0000000..abb922f --- /dev/null +++ b/test/head/infrastructure/base/podinfo/helm.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmTypoRepository +metadata: + name: podinfo + namespace: default +spec: + interval: 5m + url: https://stefanprodan.github.io/podinfo