diff --git a/.github/workflows/pr-checks.yaml b/.github/workflows/pr-checks.yaml index e008c0ec..2e4de4a2 100644 --- a/.github/workflows/pr-checks.yaml +++ b/.github/workflows/pr-checks.yaml @@ -32,6 +32,8 @@ env: # renovate: datasource=github-releases depName=terraform-linters/tflint TFLINT_VERSION: "v0.63.1" ARTIFACT_RETENTION_DAYS: "15" + # renovate: datasource=github-releases depName=aquasecurity/trivy + TRIVY_VERSION: "v0.70.0" permissions: contents: read @@ -426,3 +428,136 @@ jobs: name: terraform-validation-${{ github.run_attempt }}-${{ github.sha }}-${{ github.run_number }} retention-days: ${{ env.ARTIFACT_RETENTION_DAYS }} path: ${{ env.REPORT_DIR }}/terraform + + + extract-images: + needs: run-kubara-init-and-generate + runs-on: ubuntu-latest + outputs: + images: ${{ steps.matrix.outputs.images }} + has_images: ${{ steps.matrix.outputs.has_images }} + defaults: + run: + shell: bash + steps: + - name: Checkout CI scripts + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + sparse-checkout: | + .scripts + .github/helm-profiles + + - name: Download generated kubara artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: kubara-generated-files-${{ github.run_attempt }}-${{ github.sha }}-${{ github.run_number }} + + - name: Verify Helm + run: helm version --short + + - name: Extract container images + run: | + mkdir -p reports + OUTPUT_FILE="$PWD/reports/images.txt" .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 + + - name: Upload image list + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + if: always() + with: + name: container-images-${{ github.run_attempt }}-${{ github.sha }}-${{ github.run_number }} + retention-days: ${{ env.ARTIFACT_RETENTION_DAYS }} + path: reports/images.txt + + - name: Build image matrix + id: matrix + run: | + set -euo pipefail + images=$(jq -R -s -c 'split("\n") | map(select(length > 0))' reports/images.txt) + echo "images=$images" >> "$GITHUB_OUTPUT" + if [[ "$images" == "[]" ]]; then + echo "has_images=false" >> "$GITHUB_OUTPUT" + else + echo "has_images=true" >> "$GITHUB_OUTPUT" + fi + + trivy-db: + needs: extract-images + if: needs.extract-images.outputs.has_images == 'true' + runs-on: ubuntu-latest + steps: + - name: Cache Trivy DB + id: cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ github.workspace }}/.cache/trivy + key: trivy-db-${{ github.run_id }} + restore-keys: trivy-db- + + - name: Setup Trivy + if: steps.cache.outputs.cache-hit != 'true' + uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1 + with: + version: ${{ env.TRIVY_VERSION }} + + - name: Download DB only + if: steps.cache.outputs.cache-hit != 'true' + run: trivy image --download-db-only --cache-dir "${{ github.workspace }}"/.cache/trivy + + trivy-scan: + needs: [extract-images, trivy-db] + if: needs.extract-images.outputs.has_images == 'true' + runs-on: ubuntu-latest + env: + TRIVY_CACHE_DIR: ${{ github.workspace }}/.cache/trivy + TRIVY_SKIP_DB_UPDATE: true # assumes db is present, can't silently redownload db + TRIVY_SKIP_JAVA_DB_UPDATE: true + + strategy: + fail-fast: false + max-parallel: 10 + matrix: + image: ${{ fromJSON(needs.extract-images.outputs.images) }} + steps: + - name: Checkout # for fetching .git to upload sarif + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + sparse-checkout: .github + fetch-depth: 1 + + - name: Restore Trivy DB + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ github.workspace }}/.cache/trivy + key: trivy-db-${{ github.run_id }} + restore-keys: trivy-db- + + + - name: Scan ${{ matrix.image }} + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + cache: false # uses restored db + image-ref: ${{ matrix.image }} + version: ${{ env.TRIVY_VERSION }} + scanners: vuln + severity: HIGH + limit-severities-for-sarif: true + ignore-unfixed: true + format: sarif + output: trivy-results.sarif + exit-code: '0' + + - name: Upload SARIF + if: always() + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + with: + sarif_file: trivy-results.sarif + category: trivy-${{ matrix.image }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 2efc2ac5..a57b732d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -57,6 +57,52 @@ jobs: - name: Install go-licenses run: go install github.com/google/go-licenses/v2@3e084b0caf710f7bfead967567539214f598c0a2 #v2.0.1 + - name: Verify preinstalled Helm + run: | + set -euo pipefail + helm version --short + + - name: Run kubara generate + env: + 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_DNS_NAME: kubara-tst.stackit.run + KUBARA_STACKIT_PROJECT_ID: "00000000-0000-0000-0000-000000000000" + KUBARA_TERRAFORM_PROVIDER: stackit + KUBARA_CLUSTER_TYPE: hub + KUBARA_SSO_ORG: Kubara + KUBARA_SSO_TEAM: Test + KUBARA_KUBERNETES_TYPE: ske + KUBARA_KUBERNETES_VERSION: "1.35.0" + run: | + set -euo pipefail + mkdir -p /tmp/gen + (cd src && go run main.go --work-dir /tmp/gen init --prep) + .scripts/kubara-env-update.sh /tmp/gen/.env + (cd src && go run main.go --work-dir /tmp/gen init) + .scripts/kubara-config-update.sh /tmp/gen/config.yaml + (cd src && go run main.go --work-dir /tmp/gen generate) + + - name: Extract images and set goreleaser header + run: | + MANAGED=/tmp/gen/managed-service-catalog/helm \ + OUTPUT_FILE=/tmp/images.txt \ + "$GITHUB_WORKSPACE"/.scripts/image-version.sh + { + echo "IMAGE_LIST<> "$GITHUB_ENV" + + - name: Run GoReleaser if: github.event_name == 'push' uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 diff --git a/.scripts/image-version.sh b/.scripts/image-version.sh new file mode 100755 index 00000000..b8d71559 --- /dev/null +++ b/.scripts/image-version.sh @@ -0,0 +1,84 @@ +#!/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}" +CLUSTER_NAME="$(yq -r '.clusters[0].name' "$CONFIG_FILE")" +CONFIGS="${CONFIGS:-platform-configs/${CLUSTER_NAME}/helm}" +OUTPUT_FILE="${OUTPUT_FILE:-}" + +[[ -f "$CONFIG_FILE" ]] || { echo "::error::Missing $CONFIG_FILE — run 'kubara generate' first (or cd into its output)"; exit 1; } +[[ -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" >/dev/null 2>&1); 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 + +IMAGES="$( + cat "$render_dir"/*.yaml | + grep -E '^[[:space:]]*image:' | + sed -E "s/^[[:space:]]*image:[[:space:]]*//; s/[\"']//g" | + grep -vE '[*!]' | # drop kyverno wildcard/negation entries + grep -vE '^[[:space:]]*$' | + sort -u +)" + +echo "Done Rendering!" + +[[ -n "$IMAGES" ]] || { echo "::warning::No image references found"; exit 0; } + +echo "$IMAGES" + +if [[ -n "$OUTPUT_FILE" ]]; then + echo "$IMAGES" > "$OUTPUT_FILE" + echo "::notice::Image list written to $OUTPUT_FILE" +fi diff --git a/.scripts/kubara-config-update.sh b/.scripts/kubara-config-update.sh index 90fddaa5..d7a85f8a 100755 --- a/.scripts/kubara-config-update.sh +++ b/.scripts/kubara-config-update.sh @@ -49,4 +49,14 @@ apply_yaml_if_set KUBARA_KUBERNETES_VERSION ".clusters[0].terraform.kubernetesV apply_yaml_if_set KUBARA_DNS_NAME ".clusters[0].dnsName" apply_yaml_if_set KUBARA_DNS_NAME ".clusters[0].terraform.dns.name" +# Enable every catalog service to the image/vuln report and the release header cover all charts +yq eval '(.clusters[0].services[] | .status) = "enabled"' -i "$CFG" + +# metalb, loki and velero need custom configs +yq eval '.clusters[0].services.metallb.config.publicLoadBalancerIPs = "203.0.113.10"' -i "$CFG" +yq eval '.clusters[0].services.metallb.config.loadBalancerAddressPool = ["203.0.113.0/24"]' -i "$CFG" +yq eval '.clusters[0].storage.bucketNames.chunks = "loki"' -i "$CFG" + +yq eval '.clusters[0].services.velero.config.backupStorage.s3Url = "https://bucket.example.com"' -i "$CFG" + log "✅ config.yaml updated" diff --git a/src/.goreleaser.yaml b/src/.goreleaser.yaml index b996b83a..85ae7443 100644 --- a/src/.goreleaser.yaml +++ b/src/.goreleaser.yaml @@ -13,6 +13,9 @@ release: replace_existing_artifacts: true extra_files: - glob: licenses.csv + header: | + ## Container Images + {{ .Env.IMAGE_LIST }} changelog: use: git sort: asc diff --git a/src/internal/catalog/built-in/platform-configs/terraform/stackit/infrastructure/env.auto.tfvars.tplt b/src/internal/catalog/built-in/platform-configs/terraform/stackit/infrastructure/env.auto.tfvars.tplt index efc68d36..05c7a953 100644 --- a/src/internal/catalog/built-in/platform-configs/terraform/stackit/infrastructure/env.auto.tfvars.tplt +++ b/src/internal/catalog/built-in/platform-configs/terraform/stackit/infrastructure/env.auto.tfvars.tplt @@ -11,7 +11,7 @@ name = "{{ .cluster.name }}" ### Velero Object Storage velero_bucket_region = "{{ index .cluster.services "velero" "config" "backupStorage" "region" }}" velero_bucket_name = "bucket-velero-{{ .cluster.name }}-{{ .cluster.stage }}" -{{/* Terraform has complained that the credentials group name might be too long, this trims it to 32 characters */}} +{{- /* Terraform has complained that the credentials group name might be too long, this trims it to 32 characters */}} velero_credentials_group_name = "{{ printf "cg-velero-%s-%s" .cluster.name .cluster.stage | trunc 32 | trimSuffix "-" }}" {{ end -}}