diff --git a/.github/actions/generate-platform/action.yaml b/.github/actions/generate-platform/action.yaml new file mode 100644 index 0000000..2b63b90 --- /dev/null +++ b/.github/actions/generate-platform/action.yaml @@ -0,0 +1,90 @@ +--- +name: Generate platform +description: Renders a kubara platform from the working-tree catalogs. + +inputs: + kubara: + description: Path to the kubara binary. + required: true + kubara-scripts: + description: Path to the kubara repository's .scripts directory. + required: true + work-dir: + description: Directory to generate into. + required: true + provider: + description: Terraform provider. + default: stackit + kubernetes-type: + description: Kubernetes flavour. + default: ske + dns-name: + default: kubara-tst.stackit.run + project-id: + default: 00000000-0000-0000-0000-000000000000 + velero-s3-url: + default: https://object.storage.eu01.onstackit.cloud + +runs: + using: composite + steps: + - name: Generate platform + shell: bash + env: + HOME: ${{ runner.temp }}/kubara-home + KUBARA: ${{ inputs.kubara }} + SCRIPTS: ${{ inputs.kubara-scripts }} + WORK_DIR: ${{ inputs.work-dir }} + KUBARA_PROJECT_NAME: kubara + KUBARA_PROJECT_STAGE: tst + KUBARA_DOCKERCONFIG_BASE64: "000000" + KUBARA_ARGOCD_WIZARD_ACCOUNT_PASSWORD: "000000" + KUBARA_ARGOCD_HELM_REPO_USERNAME: git + KUBARA_ARGOCD_HELM_REPO_PASSWORD: "000000" + KUBARA_ARGOCD_HELM_REPO_URL: https://kubara.io/kubara.git + KUBARA_ARGOCD_GIT_HTTPS_URL: https://kubara.io/kubara.git + KUBARA_ARGOCD_GIT_PAT_OR_PASSWORD: "000000" + KUBARA_ARGOCD_GIT_USERNAME: git + KUBARA_CLUSTER_TYPE: hub + KUBARA_SSO_ORG: Kubara + KUBARA_SSO_TEAM: Test + KUBARA_KUBERNETES_VERSION: 1.35.0 + KUBARA_TERRAFORM_PROVIDER: ${{ inputs.provider }} + KUBARA_KUBERNETES_TYPE: ${{ inputs.kubernetes-type }} + KUBARA_DNS_NAME: ${{ inputs.dns-name }} + KUBARA_STACKIT_PROJECT_ID: ${{ inputs.project-id }} + KUBARA_VELERO_S3_URL: ${{ inputs.velero-s3-url }} + run: | + set -euo pipefail + bootstrap="${GITHUB_WORKSPACE}/bootstrap" + general="${GITHUB_WORKSPACE}/general" + mkdir -p "$WORK_DIR" + + # Cache the working-tree catalogs locally so `init` resolves them + # without reaching for the published versions. + for catalog in bootstrap general; do + (cd "$catalog" && "$KUBARA" catalog package "oci://ghcr.io/${GITHUB_REPOSITORY_OWNER}/catalogs/") + done + + "$KUBARA" --work-dir "$WORK_DIR" init --prep + "$SCRIPTS/kubara-env-update.sh" "$WORK_DIR/.env" + "$KUBARA" --work-dir "$WORK_DIR" --catalog "$general" --catalog-overwrite init + "$SCRIPTS/kubara-config-update.sh" "$WORK_DIR/config.yaml" + + # `init` writes the CLI's pinned catalog refs (…/general:1.1.0) into + # config.yaml. Left alone, `generate` would render the PUBLISHED catalog + # instead of this working tree — silently wrong on any version bump. + BOOTSTRAP="$bootstrap" GENERAL="$general" yq eval ' + .bootstrapCatalog = strenv(BOOTSTRAP) | + .clusters[].catalogs = [strenv(GENERAL)] + ' -i "$WORK_DIR/config.yaml" + + yq eval ' + .clusters[0].services[].status = "enabled" | + .clusters[0].services.metallb.config.publicLoadBalancerIPs = "192.0.2.10" | + .clusters[0].services.metallb.config.loadBalancerAddressPool = ["192.0.2.10-192.0.2.20"] | + .clusters[0].services.velero.config.backupStorage.bucketName = "kubara-ci-velero" | + .clusters[0].services.velero.config.backupStorage.s3Url = strenv(KUBARA_VELERO_S3_URL) + ' -i "$WORK_DIR/config.yaml" + + "$KUBARA" --work-dir "$WORK_DIR" generate diff --git a/.github/workflows/pr-checks.yaml b/.github/workflows/pr-checks.yaml index f96ef6b..e19f0fb 100644 --- a/.github/workflows/pr-checks.yaml +++ b/.github/workflows/pr-checks.yaml @@ -90,8 +90,6 @@ jobs: - name: Checkout catalogs uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - # Needed in both acquisition modes: the job below runs kubara's helper - # scripts (.ci/kubara/.scripts/*.sh) regardless of how the CLI is obtained. - name: Checkout kubara uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -322,3 +320,61 @@ jobs: name: terraform-validation-${{ matrix.target }}-${{ github.run_attempt }}-${{ github.sha }}-${{ github.run_number }} retention-days: ${{ env.ARTIFACT_RETENTION_DAYS }} path: ${{ env.REPORT_DIR }}/terraform + + extract-images: + needs: integration-generate + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + target: [stackit-ske] + defaults: + run: + shell: bash + steps: + - name: Checkout CI scripts + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + sparse-checkout: | + .scripts + .github + + - name: Download generated kubara artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: kubara-generated-${{ matrix.target }}-${{ github.run_attempt }}-${{ github.sha }}-${{ github.run_number }} + + - name: Verify Helm + run: helm version --short + + - name: Extract container images + env: + IMAGE_OUTPUT_FILE: ${{ github.workspace }}/reports/images.txt + HELM_CHART_VERSION_FILE: ${{ github.workspace }}/reports/chart.txt + + run: | + mkdir -p reports + .scripts/image-version.sh + + - name: Write image summary + if: always() + run: | + [[ -f reports/images.txt ]] || exit 0 + echo "### Container Images" >> "$GITHUB_STEP_SUMMARY" + while read -r img; do + echo "- $img" >> "$GITHUB_STEP_SUMMARY" + done < reports/images.txt + [[ -f reports/chart.txt ]] || exit 0 + echo "### Helm Chart Image Versions" >> "$GITHUB_STEP_SUMMARY" + while read -r img; do + echo "- $img" >> "$GITHUB_STEP_SUMMARY" + done < reports/chart.txt + + - name: Upload image list + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + if: always() + with: + name: container-images-${{ matrix.target }}-${{ github.run_attempt }}-${{ github.sha }}-${{ github.run_number }} + retention-days: ${{ env.ARTIFACT_RETENTION_DAYS }} + path: reports/ + diff --git a/.github/workflows/publish-catalog.yaml b/.github/workflows/publish-catalog.yaml index c7a3d92..3e63dbf 100644 --- a/.github/workflows/publish-catalog.yaml +++ b/.github/workflows/publish-catalog.yaml @@ -17,11 +17,20 @@ concurrency: env: # renovate: datasource=github-releases depName=kubara-io/kubara KUBARA_REF: "v0.13.0" + OUTPUT_GENERATED_DIR: ${{ github.workspace }}/output-generated + # setup-oras requires version without "v" in "vX.X.X" + # renovate: datasource=github-releases depName=oras-project/oras extractVersion=^v(?.*)$ + ORAS_VERSION: "1.3.1" + REPORT_DIR: ${{ github.workspace }}/reports + ARTIFACT_RETENTION_DAYS: "15" jobs: publish: name: Publish catalog runs-on: ubuntu-latest + outputs: + catalog: ${{ steps.release.outputs.catalog }} + version: ${{ steps.release.outputs.version }} steps: - name: Checkout catalogs uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -108,3 +117,120 @@ jobs: "${RUNNER_TEMP}/kubara" catalog package \ "oci://ghcr.io/${GITHUB_REPOSITORY_OWNER}/catalogs/" "${RUNNER_TEMP}/kubara" catalog push "$CATALOG_REF" + + extract-images: + name: Extract container images + needs: publish + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + defaults: + run: + shell: bash + steps: + - name: Checkout catalogs + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Checkout kubara + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: kubara-io/kubara + ref: ${{ env.KUBARA_REF }} + path: .ci/kubara + + - name: Install kubara release + env: + KUBARA_VERSION: ${{ env.KUBARA_REF }} + run: | + set -euo pipefail + version="${KUBARA_VERSION#v}" + archive="kubara_${version}_linux_amd64.tar.gz" + checksums="kubara_${version}_checksums.txt" + base_url="https://github.com/kubara-io/kubara/releases/download/${KUBARA_VERSION}" + temp_dir="$(mktemp -d)" + trap 'rm -rf "$temp_dir"' EXIT + curl -fsSL "$base_url/$archive" -o "$temp_dir/$archive" + curl -fsSL "$base_url/$checksums" -o "$temp_dir/$checksums" + checksum="$(awk -v file="$archive" '$2 == file {print $1}' "$temp_dir/$checksums")" + [[ -n "$checksum" ]] + echo "$checksum $temp_dir/$archive" | sha256sum -c - + tar xzf "$temp_dir/$archive" -C "$RUNNER_TEMP" kubara + chmod +x "$RUNNER_TEMP/kubara" + + - name: Install oras + uses: oras-project/setup-oras@1d808f7d7f6995cc68b7bf507bfe5c5446e1dc9d # v2.0.1 + with: + version: ${{ env.ORAS_VERSION }} + + + - name: Generate platform + uses: ./.github/actions/generate-platform + with: + kubara: ${{ runner.temp }}/kubara + kubara-scripts: ${{ github.workspace }}/.ci/kubara/.scripts + work-dir: ${{ env.OUTPUT_GENERATED_DIR }} + + - name: Extract container images + working-directory: ${{ env.OUTPUT_GENERATED_DIR }} + env: + IMAGE_OUTPUT_FILE: ${{ env.REPORT_DIR }}/images.txt + HELM_CHART_VERSION_FILE: ${{ env.REPORT_DIR }}/chart.txt + run: | + mkdir -p "$REPORT_DIR" + "${GITHUB_WORKSPACE}/.scripts/image-version.sh" + + - name: Write image summary + if: always() + run: | + [[ -f "$REPORT_DIR/images.txt" ]] || exit 0 + echo "### Container Images" >> "$GITHUB_STEP_SUMMARY" + while read -r img; do + echo "- $img" >> "$GITHUB_STEP_SUMMARY" + done < "$REPORT_DIR/images.txt" + [[ -f "$REPORT_DIR/chart.txt" ]] || exit 0 + echo "### Helm Chart Image Versions" >> "$GITHUB_STEP_SUMMARY" + while read -r img; do + echo "- $img" >> "$GITHUB_STEP_SUMMARY" + done < "$REPORT_DIR/chart.txt" + + - name: Push image lists alongside catalog + env: + GHCR_TOKEN: ${{ github.token }} + CATALOG: ${{ needs.publish.outputs.catalog }} + VERSION: ${{ needs.publish.outputs.version }} + run: | + set -euo pipefail + cd "$REPORT_DIR" + + files=() + for report in images chart; do + if [[ -f "${report}.txt" ]]; then + files+=("${report}.txt:text/plain") + fi + done + if ((${#files[@]} == 0)); then + echo "::error::No image reports produced; nothing to push" + exit 1 + fi + + endpoint="ghcr.io/${GITHUB_REPOSITORY_OWNER}/catalogs/${CATALOG}-images:${VERSION},latest" + + printf '%s\n' "$GHCR_TOKEN" | + oras login ghcr.io --username "$GITHUB_ACTOR" --password-stdin + + oras push "$endpoint" \ + --artifact-type application/vnd.kubara.catalog.images.v1+text \ + "${files[@]}" + + echo "::notice::Attached image list to $endpoint" + + + + - name: Upload image list + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + if: always() + with: + name: container-images-${{ github.ref_name }}-${{ github.run_attempt }} + retention-days: ${{ env.ARTIFACT_RETENTION_DAYS }} + path: ${{ env.REPORT_DIR }}/ diff --git a/.scripts/image-version.sh b/.scripts/image-version.sh new file mode 100755 index 0000000..02b3979 --- /dev/null +++ b/.scripts/image-version.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash + + +# pipefail that pipes break +set -euo pipefail + +export PATH="$HOME/.local/bin/:$PATH" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +MANAGED="${MANAGED:-${PWD}/platform-components/helm}" +CONFIG_FILE="${CONFIG_FILE:-config.yaml}" + +[[ -f "$CONFIG_FILE" ]] || { echo "::error::Missing $CONFIG_FILE — run 'kubara generate' first (or cd into its output)"; exit 1; } + +CLUSTER_NAME="$(yq -r '.clusters[0].name' "$CONFIG_FILE")" +CONFIGS="${CONFIGS:-platform-configs/${CLUSTER_NAME}/helm}" +IMAGE_OUTPUT_FILE="${IMAGE_OUTPUT_FILE:-}" +HELM_CHART_VERSION_FILE="${HELM_CHART_VERSION_FILE:-}" + +[[ -d "$MANAGED" ]] || { echo "::error::Missing $MANAGED — run 'kubara generate' first"; exit 1; } +command -v helm >/dev/null 2>&1 || { echo "::error::helm not found on PATH"; exit 1; } +command -v yq >/dev/null 2>&1 || { echo "::error::yq not found on PATH"; exit 1; } + +KUBE_VERSION=$(yq -r '.clusters[0].terraform.kubernetesVersion' "$CONFIG_FILE") + +PROMETHEUS_STATUS="$(yq -r '.clusters[0].services."kube-prometheus-stack".status // "disabled"' "$CONFIG_FILE")" + +# helm template flags advertise the monitoring API only when +# kube-prometheus-stack is enabled, since some charts (eg. traefik) render +# ServiceMonitors guarded by a `fail` on monitoring.coreos.com/v1. +HELM_TEMPLATE_ARGS=(--kube-version "$KUBE_VERSION" --include-crds) +if [[ "$PROMETHEUS_STATUS" == enabled ]]; then + HELM_TEMPLATE_ARGS+=(--api-versions "monitoring.coreos.com/v1") +fi + + +echo "Rendering charts from $MANAGED (kube-version=$KUBE_VERSION)" >&2 + + +render_dir="$(mktemp -d)"; trap 'rm -rf "$render_dir"' EXIT +FAILED=() + +for chart_path in "$MANAGED"/*/; do + chart=$(basename "$chart_path") + [[ -f "$chart_path/Chart.yaml" ]] || continue + + # Don't render library charts + [[ "$(yq '.type // "application"' "$chart_path/Chart.yaml")" == library ]] && continue + + echo "Updating dependency for ${chart_path}" >&2 + + if ! dep_out=$(helm dependency update "$chart_path" 2>&1 >/dev/null ); then + echo "::error::helm dependency update failed for '$chart_path'"; echo "$dep_out" >&2 + FAILED+=("$chart:dependency-update"); continue + fi + + values_file="$CONFIGS/$chart/values.generated.yaml" + base_values=(); [[ -f "$values_file" ]] && base_values=(-f "$values_file") + + if ! helm template "${HELM_TEMPLATE_ARGS[@]}" \ + "$chart" "$chart_path" "${base_values[@]}" \ + > "$render_dir/$chart.yaml" 2> "$render_dir/$chart.err"; then + echo "::error::helm template for for '$chart':" + sed 's/^/ /' "$render_dir/$chart.err" >&2 + FAILED+=("$chart:template"); continue + fi +done + +# Note! This script does capture normal, init and sidecar containers, but by design +# misses out on runtime-injected images (e.g through webhooks, Kyverno etc.) +# and images refs passed via environment, this does not represent an exhaustive list of images +# captured +IMAGES="$( + shopt -s nullglob # prevents failing if nothing's matched + cat "$render_dir"/*.yaml | + { grep -E '^[[:space:]]*image:' || true; } | + sed -E "s/^[[:space:]]*image:[[:space:]]*//; s/[\"']//g" | + grep -vE '[*!]' | # drop kyverno wildcard/negation entries + grep -vE '^[[:space:]]*$' | + sort -u +)" + +echo "Done Rendering!" + + +# errors during templating are listed, but not critical for extraction +if ((${#FAILED[@]})); then + echo "::warning:: Errors during templating: " + for err in "${FAILED[@]}"; do + echo "- $err" + done +fi + + +echo "$IMAGES" + +### Helm dependency part +echo "Extracting Helm Dependencies" +HELM_CHART_VERSIONS="$(find "$MANAGED" -name Chart.yaml -exec yq '.dependencies[] | select(.name != "template-library") | .name + ": " + .version' {} \;)" +echo "$HELM_CHART_VERSIONS" + +# If either no images are found nor chart versions exit +[[ -n "$IMAGES" ]] || { echo "::warning::No image references found"; exit 0; } +[[ -n "$HELM_CHART_VERSIONS" ]] || { echo "::warning::No helm image references found"; exit 0; } + +if [[ -n "$IMAGE_OUTPUT_FILE" ]]; then + echo "$IMAGES" > "$IMAGE_OUTPUT_FILE" + echo "::notice::Image list written to $IMAGE_OUTPUT_FILE" +fi + +if [[ -n "$HELM_CHART_VERSION_FILE" ]]; then + echo "$HELM_CHART_VERSIONS" > "$HELM_CHART_VERSION_FILE" + echo "::notice:: Helm Image list written to $HELM_CHART_VERSION_FILE" +fi diff --git a/general/platform-components/helm/metallb/CHANGELOG.md b/general/platform-components/helm/metallb/CHANGELOG.md index 8df8f48..9213f4a 100644 --- a/general/platform-components/helm/metallb/CHANGELOG.md +++ b/general/platform-components/helm/metallb/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.1] - 2026-08-05 +### Fixed +- Disable `prometheus.rbacPrometheus` as permissions are already present in a seperate ClusterRole and it prevents rendering of the helm charts + ## [0.2.0] - 2026-07-10 ### Changed - Bump template-library dependency to 0.2.0 diff --git a/general/platform-components/helm/metallb/Chart.yaml b/general/platform-components/helm/metallb/Chart.yaml index 853022e..4ac2aed 100644 --- a/general/platform-components/helm/metallb/Chart.yaml +++ b/general/platform-components/helm/metallb/Chart.yaml @@ -3,7 +3,7 @@ apiVersion: v2 name: metallb description: Umbrella Chart for metalb type: application -version: 0.2.0 +version: 0.2.1 dependencies: - name: template-library repository: file://../template-library diff --git a/general/platform-components/helm/metallb/values.yaml b/general/platform-components/helm/metallb/values.yaml index f60606a..983282e 100644 --- a/general/platform-components/helm/metallb/values.yaml +++ b/general/platform-components/helm/metallb/values.yaml @@ -36,6 +36,10 @@ metallb: limits: memory: 100Mi prometheus: + # kube-prometheus-stack binds prometheus to a ClusterRole with the appopriate permissions already + # Setting it to true which is the default would not enable something new here + # Re-enable (and set serviceAccount/namespace) only if Prometheus loses cluster discover e.g metallb showing zero targets + rbacPrometheus: false serviceMonitor: enabled: true frr-k8s: