commit b6460c4ea3b8e249e2bc38f20a874a76b592878a Author: Scaffolder Date: Wed Apr 15 15:41:22 2026 +0000 initial commit Change-Id: I2e2564a72b6be9af536235fc3795fd788fd9257b diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..87fd972 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,67 @@ +version: 2 +jobs: + lint-scripts: + docker: + - image: koalaman/shellcheck-alpine + steps: + - checkout + - run: + command: | + shellcheck -x .circleci/install_charts.sh + shellcheck -x .circleci/install_tools.sh + shellcheck -x .circleci/release.sh + lint-charts: + docker: + - image: quay.io/helmpack/chart-testing:latest + steps: + - checkout + - run: + command: | + ct lint --all --validate-maintainers=false --config .circleci/ct.yaml + install-charts: + machine: true + steps: + - checkout + - run: + no_output_timeout: 20m + command: .circleci/install_charts.sh + release-charts: + machine: true + steps: + - checkout + - add_ssh_keys: + fingerprints: + - "7f:84:3b:70:a1:c8:63:8e:dc:5f:51:51:b8:f4:7c:76" + - run: + command: | + echo "export GIT_REPOSITORY_URL=$CIRCLE_REPOSITORY_URL" >> $BASH_ENV + echo "export GIT_USERNAME=$CIRCLE_PROJECT_USERNAME" >> $BASH_ENV + .circleci/install_tools.sh + .circleci/release.sh +workflows: + version: 2 + lint-test-release: + jobs: + - lint-scripts: + filters: + branches: + ignore: gh-pages + - lint-charts: + filters: + branches: + ignore: gh-pages + - install-charts: + requires: + - lint-charts + filters: + branches: + ignore: gh-pages + - release-charts: + requires: + - lint-charts + - install-charts + filters: + tags: + ignore: /.*/ + branches: + only: main diff --git a/.circleci/ct.yaml b/.circleci/ct.yaml new file mode 100644 index 0000000..cc80da0 --- /dev/null +++ b/.circleci/ct.yaml @@ -0,0 +1 @@ +chart-dirs: . diff --git a/.circleci/install_charts.sh b/.circleci/install_charts.sh new file mode 100755 index 0000000..69b4c6c --- /dev/null +++ b/.circleci/install_charts.sh @@ -0,0 +1,126 @@ +#!/bin/bash + +set -o errexit +set -o nounset +set -o pipefail + +readonly CT_VERSION=latest +readonly KIND_VERSION=v0.31.0 +readonly CLUSTER_NAME=chart-testing +readonly REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel)}" +readonly KEDA_VERSION=2.19.0 + +find_latest_tag() { + if ! git describe --tags --abbrev=0 2>/dev/null; then + git rev-list --max-parents=0 --first-parent HEAD + fi +} + +create_ct_container() { + echo "Starting Chart Testing container" + docker run --rm --interactive --detach --network host --name ct \ + --volume "$(pwd)/.circleci/ct.yaml:/etc/ct/ct.yaml" \ + --volume "$(pwd):/workdir" \ + --workdir /workdir \ + "quay.io/helmpack/chart-testing:${CT_VERSION}" \ + cat +} + +cleanup() { + echo "Removing ct container" + docker kill ct >/dev/null 2>&1 || true +} + +docker_exec() { + docker exec --interactive --tty ct "$@" +} + +create_kind_cluster() { + echo "Installing kind" + curl -sSLo kind "https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/kind-linux-amd64" + chmod +x kind + sudo mv kind /usr/local/bin/kind + + echo "Creating cluster" + kind create cluster --name "${CLUSTER_NAME}" --wait 5m -q + + echo "Copying kubeconfig to container" + local kubeconfig + kubeconfig="$(pwd)/kube-config" + kind get kubeconfig --name "${CLUSTER_NAME}" | tee "${kubeconfig}" + docker_exec mkdir -p /root/.kube + docker cp "${kubeconfig}" ct:/root/.kube/config + + docker_exec kubectl cluster-info + docker_exec kubectl get nodes +} + +install_local_path_provisioner() { + docker_exec kubectl delete storageclass standard + docker_exec kubectl apply -f "https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml" +} + +install_keda() { + docker_exec kubectl apply -f "https://github.com/kedacore/keda/releases/download/v${KEDA_VERSION}/keda-${KEDA_VERSION}-core.yaml" || true +} + +install_charts() { + docker_exec ct install --all + echo +} + +main() { + pushd "${REPO_ROOT}" >/dev/null + + echo "Fetching tags" + git fetch --tags + + local latest_tag + latest_tag=$(find_latest_tag) + + local latest_tag_rev + latest_tag_rev=$(git rev-parse --verify "${latest_tag}") + echo "${latest_tag_rev} ${latest_tag} (latest tag)" + + local head_rev + head_rev=$(git rev-parse --verify HEAD) + echo "${head_rev} HEAD" + + if [[ "${latest_tag_rev}" == "${head_rev}" ]]; then + echo "No code changes. Nothing to release." + exit + fi + + echo "Identifying changed charts since tag ${latest_tag}" + + local changed_charts=() + readarray -t changed_charts <<< "$(git diff --find-renames --name-only "${latest_tag_rev}" | grep '\.yaml$' | cut -d '/' -f 1 | sort -u)" + + if [[ -n "${changed_charts[*]}" ]]; then + local changes_pending=no + for chart in "${changed_charts[@]}"; do + if [[ -f "${chart}/Chart.yaml" ]]; then + changes_pending=yes + break + fi + done + + if [[ "${changes_pending}" == "yes" ]]; then + create_ct_container + trap cleanup EXIT + + create_kind_cluster + install_local_path_provisioner + install_keda + install_charts + else + echo "Nothing to do. No chart changes detected." + fi + else + echo "Nothing to do. No chart changes detected." + fi + + popd >/dev/null +} + +main diff --git a/.circleci/install_tools.sh b/.circleci/install_tools.sh new file mode 100755 index 0000000..f6038bb --- /dev/null +++ b/.circleci/install_tools.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +set -o errexit + +readonly HELM_VERSION=3.19.4 +readonly CHART_RELEASER_VERSION=1.8.1 + +install_helm() { + echo "Installing Helm" + curl -sSLO "https://get.helm.sh/helm-v${HELM_VERSION}-linux-amd64.tar.gz" + sudo mkdir -p "/usr/local/helm-v${HELM_VERSION}" + sudo tar -xzf "helm-v${HELM_VERSION}-linux-amd64.tar.gz" -C "/usr/local/helm-v${HELM_VERSION}" + sudo ln -s "/usr/local/helm-v${HELM_VERSION}/linux-amd64/helm" /usr/local/bin/helm + sudo chmod +x /usr/local/helm-v${HELM_VERSION}/linux-amd64/helm + rm -f "helm-v${HELM_VERSION}-linux-amd64.tar.gz" +} + +install_cr() { + echo "Installing Chart Releaser" + curl -sSLO "https://github.com/helm/chart-releaser/releases/download/v${CHART_RELEASER_VERSION}/chart-releaser_${CHART_RELEASER_VERSION}_linux_amd64.tar.gz" + sudo mkdir -p "/usr/local/chart-releaser-v${CHART_RELEASER_VERSION}" + sudo tar -xzf "chart-releaser_${CHART_RELEASER_VERSION}_linux_amd64.tar.gz" -C "/usr/local/chart-releaser-v${CHART_RELEASER_VERSION}" + sudo ln -s "/usr/local/chart-releaser-v${CHART_RELEASER_VERSION}/cr" /usr/local/bin/cr + sudo chmod +x "/usr/local/chart-releaser-v${CHART_RELEASER_VERSION}/cr" + rm -f "chart-releaser_${CHART_RELEASER_VERSION}_linux_amd64.tar.gz" +} + +main() { + install_helm + install_cr +} + +main diff --git a/.circleci/release.sh b/.circleci/release.sh new file mode 100755 index 0000000..336e5bc --- /dev/null +++ b/.circleci/release.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +set -o nounset + +: "${CR_TOKEN:?Environment variable CR_TOKEN must be set}" +: "${GIT_REPOSITORY_URL:?Environment variable GIT_REPOSITORY_URL must be set}" +: "${GIT_USERNAME:?Environment variable GIT_USERNAME must be set}" +: "${GIT_EMAIL:?Environment variable GIT_EMAIL must be set}" + +readonly OWNER=haproxytech +readonly GIT_REPO=helm-charts +readonly PACKAGE_PATH=.deploy +readonly CHARTS_URL=https://haproxytech.github.io/helm-charts +readonly REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel)}" + +find_latest_tag() { + if ! git describe --tags --abbrev=0 2>/dev/null; then + git rev-list --max-parents=0 --first-parent HEAD + fi +} + +package_chart() { + local chart="$1" + helm dependency build "${chart}" + helm package "${chart}" --destination "${PACKAGE_PATH}" +} + +release_charts() { + echo "Upload Helm chart packages to GitHub" + cr upload -o "${OWNER}" -r "${GIT_REPO}" -p "${PACKAGE_PATH}" + + echo "Upload Helm chart packages to GHCR OCI" + echo "${HELM_GH_TOKEN}" | helm registry login ghcr.io --username "${OWNER}" --password-stdin + + for chart in "${PACKAGE_PATH}"/*.tgz; do + helm push "${chart}" "oci://ghcr.io/${OWNER}/${GIT_REPO}" + done +} + +update_index() { + echo "Generating Helm chart index" + git config user.email "${GIT_EMAIL}" + git config user.name "${GIT_USERNAME}" + git checkout gh-pages + + cr index -i index.yaml -o "${OWNER}" -r "${GIT_REPO}" -c "${CHARTS_URL}" -p "${PACKAGE_PATH}" + + git add index.yaml + git commit --message="Update index.yaml" --signoff + git push "${GIT_REPOSITORY_URL}" gh-pages +} + +main() { + pushd "${REPO_ROOT}" >/dev/null || exit 1 + + echo "Fetching tags" + git fetch --tags + + local latest_tag + latest_tag=$(find_latest_tag) + + local latest_tag_rev + latest_tag_rev=$(git rev-parse --verify "${latest_tag}") + echo "${latest_tag_rev} ${latest_tag} (latest tag)" + + local head_rev + head_rev=$(git rev-parse --verify HEAD) + echo "${head_rev} HEAD" + + if [[ "${latest_tag_rev}" == "${head_rev}" ]]; then + echo "No code changes. Nothing to release." + exit + fi + + mkdir -p "${PACKAGE_PATH}" + + echo "Identifying changed charts since tag ${latest_tag}" + + local changed_charts=() + readarray -t changed_charts <<< "$(git diff --find-renames --name-only "${latest_tag_rev}" | grep 'Chart.yaml$' | cut -d '/' -f 1 | sort -u)" + + if [[ -n "${changed_charts[*]}" ]]; then + local release_pending=no + for chart in "${changed_charts[@]}"; do + if [[ -f "${chart}/Chart.yaml" ]]; then + release_pending=yes + echo "Packaging chart ${chart}" + package_chart "${chart}" + fi + done + + if [[ "${release_pending}" == "yes" ]]; then + release_charts + update_index + else + echo "Nothing to do. No chart changes detected." + fi + else + echo "Nothing to do. No chart changes detected." + fi + + popd >/dev/null || exit 1 +} + +main diff --git a/.gitea/workflows/build-push.yml b/.gitea/workflows/build-push.yml new file mode 100644 index 0000000..0ac53e3 --- /dev/null +++ b/.gitea/workflows/build-push.yml @@ -0,0 +1,84 @@ +name: Build and Push to ACR + +on: + push: + branches: [ dev ] + workflow_dispatch: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + AZURE_FEDERATED_TOKEN_FILE: /var/run/secrets/azure/tokens/azure-identity-token + +jobs: + build: + name: Build and Push + runs-on: ubuntu-latest + if: >- + github.ref != 'refs/heads/main' && ( + github.event_name == 'workflow_dispatch' || + (github.event_name == 'push' && github.event.before != '0000000000000000000000000000000000000000') + ) + permissions: + contents: read + id-token: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Azure CLI + run: | + command -v az &>/dev/null || curl -sL https://aka.ms/InstallAzureCLIDeb | bash + + - name: Install Docker CLI + run: | + command -v docker &>/dev/null || (apt-get update -qq && apt-get install -y docker.io) + docker --version + + - name: Azure login (OIDC) + run: | + az login \ + --service-principal \ + --username "$AZURE_CLIENT_ID" \ + --tenant "$AZURE_TENANT_ID" \ + --federated-token "$(cat $AZURE_FEDERATED_TOKEN_FILE)" + echo "✓ Azure login successful" + + - name: Get ACR details + run: | + ACR_NAME=$(az acr list --query "[0].name" -o tsv) + ACR_NAME="${ACR_NAME:-bstagecjotdevacr}" + echo "ACR_NAME=$ACR_NAME" >> $GITHUB_ENV + echo "ACR_LOGIN_SERVER=${ACR_NAME}.azurecr.io" >> $GITHUB_ENV + echo "✓ Using ACR: ${ACR_NAME}.azurecr.io" + + - name: ACR Login + run: | + ACR_TOKEN=$(az acr login --name "$ACR_NAME" --expose-token --output tsv --query accessToken) + docker login "$ACR_LOGIN_SERVER" \ + --username 00000000-0000-0000-0000-000000000000 \ + --password "$ACR_TOKEN" + echo "✓ ACR login successful" + + - name: Build and Push Docker image + run: | + IMAGE_TAG="${{ gitea.sha }}" + IMAGE_FULL="${ACR_LOGIN_SERVER}/haproxy-unified:${IMAGE_TAG}" + IMAGE_LATEST="${ACR_LOGIN_SERVER}/haproxy-unified:latest" + docker build -t "$IMAGE_FULL" -t "$IMAGE_LATEST" . + docker push "$IMAGE_FULL" + docker push "$IMAGE_LATEST" + echo "IMAGE_FULL=$IMAGE_FULL" >> $GITHUB_ENV + echo "✓ Pushed: $IMAGE_FULL" + + - name: Build Summary + run: | + echo "### ✅ Build Successful" >> $GITHUB_STEP_SUMMARY + echo "| | |" >> $GITHUB_STEP_SUMMARY + echo "|---|---|" >> $GITHUB_STEP_SUMMARY + echo "| **Service** | haproxy-unified |" >> $GITHUB_STEP_SUMMARY + echo "| **Commit** | ${{ gitea.sha }} |" >> $GITHUB_STEP_SUMMARY + echo "| **Image** | $IMAGE_FULL |" >> $GITHUB_STEP_SUMMARY diff --git a/.gitea/workflows/integration-test.yml b/.gitea/workflows/integration-test.yml new file mode 100644 index 0000000..757b895 --- /dev/null +++ b/.gitea/workflows/integration-test.yml @@ -0,0 +1,138 @@ + +name: Integration Test + +on: + pull_request: + branches: [ "main" ] + workflow_dispatch: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ── Job 1: Platform Conformance ─────────────────────────────────────────── + platform-check: + name: Platform Conformance + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate catalog-info.yaml + run: | + if [ ! -f catalog-info.yaml ]; then + echo "✗ catalog-info.yaml not found" + exit 1 + fi + python3 -c "import yaml; list(yaml.safe_load_all(open('catalog-info.yaml')))" 2>/dev/null \ + || (pip install pyyaml -q && python3 -c "import yaml; list(yaml.safe_load_all(open('catalog-info.yaml')))") + echo "✓ catalog-info.yaml is valid YAML" + + - name: Check platform files + run: | + ERRORS=0 + for f in catalog-info.yaml; do + if [ ! -f "$f" ]; then + echo "✗ Missing: $f" + ERRORS=$((ERRORS+1)) + else + echo "✓ Found: $f" + fi + done + if [ $ERRORS -gt 0 ]; then exit 1; fi + + # ── Job 2: Unit Tests + Container Smoke ─────────────────────────────────── + smoke-test: + name: Unit Tests + Container Smoke + runs-on: ubuntu-latest + needs: platform-check + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Free disk space + run: | + docker system prune -f 2>/dev/null || true + df -h / 2>/dev/null || true + + - name: Install Docker CLI + run: command -v docker &>/dev/null || (apt-get update -qq && apt-get install -y docker.io) + + - name: Build container image + run: | + if [ -f Dockerfile ]; then + docker build -t ci-image:test . + else + echo "No Dockerfile found — skipping container smoke test" + echo "SKIP_SMOKE=true" >> $GITHUB_ENV + fi + + - name: Start service and wait for health + if: env.SKIP_SMOKE != 'true' + run: | + CONTAINER_NAME="ci-${GITHUB_RUN_ID}" + PORT="${CONTAINER_PORT:-8080}" + HEALTH_PATH="${HEALTH_ENDPOINT:-/health}" + echo "CONTAINER_NAME=${CONTAINER_NAME}" >> $GITHUB_ENV + echo "CONTAINER_PORT=${PORT}" >> $GITHUB_ENV + echo "HEALTH_PATH=${HEALTH_PATH}" >> $GITHUB_ENV + + docker run -d --name "${CONTAINER_NAME}" -e OTEL_SDK_DISABLED=true ci-image:test + + for i in $(seq 1 10); do + CONTAINER_IP=$(docker inspect "${CONTAINER_NAME}" --format '{{.NetworkSettings.IPAddress}}' 2>/dev/null) + [ -n "${CONTAINER_IP}" ] && break + sleep 1 + done + echo "CONTAINER_IP=${CONTAINER_IP}" >> $GITHUB_ENV + + echo "Waiting for ${HEALTH_PATH} on ${CONTAINER_IP}:${PORT} (up to 180s)..." + DEADLINE=$(($(date +%s) + 180)) + while true; do + if curl -sf "http://${CONTAINER_IP}:${PORT}${HEALTH_PATH}" >/dev/null 2>&1; then + break + fi + if [ $(date +%s) -ge $DEADLINE ]; then + echo "Timeout waiting for health check" + docker logs "${CONTAINER_NAME}" 2>&1 | tail -30 + exit 1 + fi + if ! docker ps --filter "name=${CONTAINER_NAME}" --format '{{.Status}}' | grep -q Up; then + echo "Container exited:" + docker logs "${CONTAINER_NAME}" 2>&1 | tail -30 + exit 1 + fi + echo " still waiting..."; sleep 3 + done + echo "✓ Service healthy at ${CONTAINER_IP}:${PORT}${HEALTH_PATH}" + + - name: Validate health response + if: env.SKIP_SMOKE != 'true' + run: | + curl -sf "http://${CONTAINER_IP}:${CONTAINER_PORT}${HEALTH_PATH}" > /tmp/health.json + echo "Health response:" + cat /tmp/health.json + echo "" + echo "✓ Container smoke test: PASSED" + + - name: Cleanup + if: always() + run: docker rm -f "ci-${GITHUB_RUN_ID}" 2>/dev/null || true + + - name: Post commit status + if: always() + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + JOB_STATUS: ${{ job.status }} + run: | + STATE=$([[ "$JOB_STATUS" == "success" ]] && echo "success" || echo "failure") + DESC=$([[ "$STATE" == "success" ]] && echo "All checks passed" || echo "Some checks failed") + curl -sf -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/statuses/${GITHUB_SHA}" \ + -d "{\"state\":\"${STATE}\",\"context\":\"Integration Test / Unit Tests + Container Smoke (workflow_dispatch)\",\"description\":\"${DESC}\"}" \ + || true diff --git a/.gitea/workflows/security-scan.yml b/.gitea/workflows/security-scan.yml new file mode 100644 index 0000000..4703104 --- /dev/null +++ b/.gitea/workflows/security-scan.yml @@ -0,0 +1,149 @@ +name: Security Scanning + +on: + pull_request: + branches: [ "main" ] + workflow_dispatch: {} + +env: + TRIVY_VERSION: "0.51.1" + GITLEAKS_VERSION: "8.18.4" + COMPONENT_ID: haproxy-unified + +jobs: + # ───────────────────────────────────────────── + # 1. FILESYSTEM & DEPENDENCY SCAN + # Trivy auto-detects lockfiles (pom.xml, + # package-lock.json, go.sum, requirements.txt, etc.) + # and scans for vulns, secrets, and misconfigs. + # ───────────────────────────────────────────── + trivy-scan: + name: Trivy — Filesystem & Dependency Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Install Trivy + run: | + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \ + | sh -s -- -b /usr/local/bin v + + - name: Run Trivy filesystem scan + run: | + trivy fs \ + --exit-code 0 \ + --severity HIGH,CRITICAL \ + --format sarif \ + --output trivy-results.sarif \ + --scanners vuln,secret,misconfig \ + --dependency-tree \ + . + + - name: Upload SARIF report + uses: actions/upload-artifact@v4 + if: always() + with: + name: trivy-sarif + path: trivy-results.sarif + retention-days: 30 + + - name: Print human-readable summary + run: | + trivy fs \ + --exit-code 0 \ + --severity MEDIUM,HIGH,CRITICAL \ + --format table \ + --scanners vuln,secret,misconfig \ + . + + - name: Enforce quality gate (CRITICAL fails build) + run: | + trivy fs \ + --exit-code 1 \ + --severity CRITICAL \ + --scanners vuln,misconfig \ + . + + # ───────────────────────────────────────────── + # 2. SECRET SCAN — detect leaked credentials + # across full git history. + # ───────────────────────────────────────────── + gitleaks-scan: + name: Gitleaks — Secret Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout source (full history) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Install Gitleaks binary directly — the GitHub Action + # relies on GITHUB_TOKEN which is unavailable on Gitea Act runners. + - name: Install Gitleaks + run: | + curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v/gitleaks__linux_x64.tar.gz" \ + | tar -xz -C /usr/local/bin gitleaks + + - name: Run Gitleaks + run: | + gitleaks detect \ + --source . \ + --report-format sarif \ + --report-path gitleaks-results.sarif \ + --exit-code 1 \ + --log-level warn + + - name: Upload SARIF report + uses: actions/upload-artifact@v4 + if: always() + with: + name: gitleaks-sarif + path: gitleaks-results.sarif + retention-days: 30 + + # ───────────────────────────────────────────── + # 3. SUMMARY — aggregate all SARIF reports + # ───────────────────────────────────────────── + security-summary: + name: Security Summary + needs: + - trivy-scan + - gitleaks-scan + runs-on: ubuntu-latest + if: always() + + steps: + - name: Download all SARIF artefacts + uses: actions/download-artifact@v4 + with: + pattern: "*-sarif" + merge-multiple: true + path: sarif-reports/ + + - name: List collected reports + run: ls -lh sarif-reports/ + + - name: Generate summary + run: | + echo "## Security Scan Results — " >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Report | Size |" >> $GITHUB_STEP_SUMMARY + echo "|--------|------|" >> $GITHUB_STEP_SUMMARY + for f in sarif-reports/*.sarif; do + name=$(basename "$f") + size=$(du -sh "$f" | cut -f1) + echo "| $name | $size |" >> $GITHUB_STEP_SUMMARY + done + echo "" >> $GITHUB_STEP_SUMMARY + echo "Commit: \`\`" >> $GITHUB_STEP_SUMMARY + echo "Branch: \`\`" >> $GITHUB_STEP_SUMMARY + + - name: Bundle all SARIF reports + uses: actions/upload-artifact@v4 + with: + name: all-sarif-reports + path: sarif-reports/ + retention-days: 90 diff --git a/.gitea/workflows/techdocs.yml b/.gitea/workflows/techdocs.yml new file mode 100644 index 0000000..3b0cd36 --- /dev/null +++ b/.gitea/workflows/techdocs.yml @@ -0,0 +1,108 @@ +name: Build and Publish TechDocs + +on: + push: + branches: [main] + paths: + - "docs/**" + - "mkdocs.yml" + - "catalog-info.yaml" + workflow_dispatch: {} + +env: + TECHDOCS_AZURE_BLOB_CONTAINER_NAME: + AZURE_FEDERATED_TOKEN_FILE: /var/run/secrets/azure/tokens/azure-identity-token + AZURE_ACCOUNT_NAME: "bstagecjotdevsttechdocs" + ENTITY_NAMESPACE: default + ENTITY_KIND: component + ENTITY_NAME: haproxy-unified +jobs: + build-and-publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: read and set output + id: read_env + run: | + echo "$AZURE_FEDERATED_TOKEN_FILE" + env | grep AZURE + echo "$(cat $AZURE_FEDERATED_TOKEN_FILE)" + + # act-based Gitea runners run as root — sudo is not available. + # apt-get is called directly; works whether root or not. + - name: Bootstrap pip + run: | + python3 --version + if python3 -m pip --version 2>/dev/null; then + echo "pip already available" + elif python3 -m ensurepip --version 2>/dev/null; then + python3 -m ensurepip --upgrade + else + apt-get update -qq + apt-get install -y python3-pip + fi + python3 -m pip install --upgrade pip + python3 -m pip --version + + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + python3 -m pip install \ + mkdocs-techdocs-core==1.* \ + mkdocs-git-revision-date-localized-plugin \ + mkdocs-awesome-pages-plugin + + npm install -g @techdocs/cli + npm cache clean --force + + # mkdocs has no dry-run flag — build into a temp dir to validate config + # and catch any broken links or missing pages early. + - name: Validate MkDocs config + run: mkdocs build --strict --site-dir /tmp/mkdocs-validate + + - name: Build TechDocs site + run: | + techdocs-cli generate \ + --source-dir . \ + --output-dir ./site \ + --no-docker \ + --verbose + + # act runners don't include az by default — install via Microsoft's + # official script which works on Debian/Ubuntu without sudo. + - name: Install Azure CLI + run: | + if command -v az &>/dev/null; then + echo "Azure CLI already installed: $(az version --query '"azure-cli"' -o tsv)" + else + curl -sL https://aka.ms/InstallAzureCLIDeb | bash + fi + + - name: Azure login (OIDC) + run: | + az login \ + --service-principal \ + --username "$AZURE_CLIENT_ID" \ + --tenant "$AZURE_TENANT_ID" \ + --federated-token "$(cat $AZURE_FEDERATED_TOKEN_FILE)" + + echo "✓ Azure login successful" + + - name: Publish TechDocs site + run: | + echo "$AZURE_ACCOUNT_NAME" + echo "$ENTITY_NAMESPACE" + echo "$ENTITY_KIND" + echo "$ENTITY_NAME" + techdocs-cli publish \ + --publisher-type azureBlobStorage \ + --storage-name "techdocs" \ + --azureAccountName "$AZURE_ACCOUNT_NAME" \ + --entity "$ENTITY_NAMESPACE/$ENTITY_KIND/$ENTITY_NAME" diff --git a/.github/workflows/inactive.yml b/.github/workflows/inactive.yml new file mode 100644 index 0000000..163e519 --- /dev/null +++ b/.github/workflows/inactive.yml @@ -0,0 +1,22 @@ +name: Close inactive issues +on: + schedule: + - cron: "30 1 * * *" + +jobs: + close-issues: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v9 + with: + days-before-issue-stale: 30 + days-before-issue-close: 30 + stale-issue-label: "stale" + stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." + close-issue-message: "This issue was closed because it has been inactive for 30 days since being marked as stale." + days-before-pr-stale: -1 + days-before-pr-close: -1 + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..abdfbe5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# General files +*.tgz +.project +.deploy + +# MacOS +._* +.DS_Store + +# JetBrains +.idea/ +*.iml + +# VSC +.vscode + +# Emacs +*~ +\#*\# +.\#* + +# Vim +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +*.un~ +Session.vim +.netrwhist + +# ct config files (auto-downloaded by test/ct-test.sh) +test/chart_schema.yaml +test/lintconf.yaml + +# TODO +TODO diff --git a/.pages b/.pages new file mode 100644 index 0000000..7ef485f --- /dev/null +++ b/.pages @@ -0,0 +1,3 @@ +nav: + - docs + - ... diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..08857ee --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,254 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +### Linting and Template Validation (no cluster needed) + +```bash +# Test all charts +./test/local-test.sh + +# Test a single chart +./test/local-test.sh haproxy-unified-gateway +./test/local-test.sh kubernetes-ingress + +# Helm lint a chart directly +helm lint kubernetes-ingress/ +helm lint haproxy-unified-gateway/ -f haproxy-unified-gateway/ci/deployment-default-values.yaml + +# Render templates to stdout +helm template test-release kubernetes-ingress/ +helm template test-release haproxy-unified-gateway/ --set controller.kind=DaemonSet +helm template test-release haproxy-unified-gateway/ --api-versions monitoring.coreos.com/v1 +``` + +### Chart-testing (ct) — matches CircleCI + +```bash +# Lint all charts +./test/ct-test.sh lint + +# Lint a single chart +./test/ct-test.sh lint haproxy-unified-gateway + +# Install (creates a Kind cluster automatically) +./test/ct-test.sh install haproxy-unified-gateway + +# Keep the Kind cluster after install tests +KIND_KEEP_CLUSTER=1 ./test/ct-test.sh install +``` + +### Integration Tests (real Kind cluster) + +```bash +# Test all charts +./test/integration-test.sh + +# Test a single chart +./test/integration-test.sh haproxy-unified-gateway + +# Run a specific test scenario (defaults, daemonset, hpa, pdb, metrics-port, monitoring, hugconf-cleanup, ci) +TEST_FILTER=monitoring ./test/integration-test.sh haproxy-unified-gateway + +# Keep namespaces after failure for debugging +KEEP_NS=1 ./test/integration-test.sh +``` + +## Contribution Requirements + +- All commits require a `Signed-off-by` line (DCO): `git commit -s -m "message"` +- Any change to a chart requires a chart **version bump** in `Chart.yaml` following semver +- Submit changes to multiple charts in **separate PRs** + +## Project Structure + +This repo contains Helm charts for HAProxy products: + +- `kubernetes-ingress/` - HAProxy Kubernetes Ingress Controller chart (mature, feature-rich) +- `haproxy/` - HAProxy community chart +- `haproxy-unified-gateway/` - HAProxy Unified Gateway (HUG) chart + +## kubernetes-ingress Chart + +- **Chart version**: follows its own semver (currently 1.49.x) +- **Image**: `haproxytech/kubernetes-ingress` +- **Supports**: Deployment + DaemonSet modes, IngressClass, Gateway API, HPA, KEDA, ServiceMonitor/PodMonitor, PDB, proxy service (fetch sync mode), ConfigMap-based HAProxy config, publish-service, default TLS cert generation +- **Templates**: 25 files in `templates/` +- **CI tests**: 38 test value files in `ci/` +- **Kubernetes**: >=1.23 +- **Maintainer**: Dinko Korunic + +## haproxy-unified-gateway Chart + +- **Chart version**: 0.1.0 (appVersion 0.9.1) +- **Image**: `haproxytech/haproxy-unified-gateway` +- **Purpose**: Kubernetes Gateway API controller powered by HAProxy +- **Kubernetes**: >=1.26 +- **Binary**: `/usr/local/sbin/hug` inside the container +- **Entry point**: `/start.sh` +- **Key flag**: `--hugconf-crd=/` for HugConf CRD reference + +### Templates + +| Template | Purpose | +|---|---| +| `_helpers.tpl` | Name, labels, image, serviceAccount, hugconfCrd, serviceMonitorName, podMonitorName helpers | +| `controller-deployment.yaml` | Deployment (when controller.kind=Deployment) | +| `controller-daemonset.yaml` | DaemonSet with hostNetwork/hostPort support (when controller.kind=DaemonSet) | +| `controller-service.yaml` | NodePort Service (HTTP 31080, HTTPS 31443, Stats 31024) | +| `controller-serviceaccount.yaml` | ServiceAccount | +| `clusterrole.yaml` | RBAC: Gateway API resources, HUG CRDs (gate.v3.haproxy.org incl. globals/defaults), core K8s resources, auth/authz for kube-rbac metrics | +| `clusterrolebinding.yaml` | ClusterRoleBinding | +| `controller-hugconf.yaml` | HugConf CR (logging, globalRef, defaultsRef configuration) — post-install hook (weight 5) | +| `controller-hugconf-cleanup.yaml` | Pre-delete hook Job that deletes the HugConf CR on `helm uninstall` | +| `controller-crdjob.yaml` | Helm hook Job: installs HUG CRDs (`--job-check-crd`) — post-install hook (weight 0) | +| `controller-crdjob-rbac.yaml` | SA + ClusterRole + Binding for CRD/GWAPI jobs | +| `controller-gwapijob.yaml` | Helm hook Job: installs Gateway API CRDs (`--job-gwapi=VERSION`) | +| `controller-hpa.yaml` | HPA (disabled by default, mutually exclusive with KEDA) | +| `controller-keda.yaml` | KEDA ScaledObject (disabled by default, Deployment only) | +| `controller-servicemonitor.yaml` | ServiceMonitor for Prometheus Operator (disabled by default, gated behind `.Capabilities.APIVersions`) | +| `controller-podmonitor.yaml` | PodMonitor for Prometheus Operator (disabled by default, gated behind `.Capabilities.APIVersions`) | +| `controller-service-metrics.yaml` | ClusterIP metrics Service with stat + metrics ports (created when serviceMonitor is enabled) | +| `controller-poddisruptionbudget.yaml` | PDB (disabled by default) | +| `controller-podsecuritypolicy.yaml` | PSP (disabled by default, K8s <1.25 only) | +| `controller-role.yaml` | Role for PSP usage | +| `controller-rolebinding.yaml` | RoleBinding for PSP Role | +| `namespace.yaml` | Optional namespace creation (pre-install hook) | +| `NOTES.txt` | Post-install instructions | + +### Values Structure + +- `rbac.create` - RBAC resources +- `namespace.create` - optional namespace +- `serviceAccount` - create, name, annotations +- `controller` - kind (Deployment/DaemonSet), image, replicaCount, hugconfCrd, metricsAuth, extraArgs, containerPort, resources, securityContext, probes, scheduling (nodeSelector/tolerations/affinity/topologySpreadConstraints), extraEnvs/Volumes/Containers, daemonset (useHostNetwork/useHostPort/hostPorts/hostIP), service config (incl. metrics service), serviceMonitor, podMonitor, autoscaling, keda (ScaledObject), PDB +- `hugconf` - create, name, logging (defaultLevel, categoryLevelList), globalRef, defaultsRef +- `crdjob` - enabled, podAnnotations, ttl, scheduling, resources, image override +- `gwapijob` - enabled, version (Gateway API CRD version), same options as crdjob + +### Metrics + +HUG exposes two separate metrics endpoints: + +| Port | Name | Source | Default | +|---|---|---|---| +| 31024 | `stat` | HAProxy stats (via `--stats-port`) | Always exposed | +| 31060 | `metrics` | Controller metrics (via `--controller-port`) | Always exposed | + +- `controller.metricsAuth` controls `--metrics-auth` flag; default is `kube-rbac` +- Supported values: `none`, `kube-rbac`, `basic` +- When `kube-rbac`: controller serves HTTPS, validates bearer tokens via TokenReview API +- When `none`: plain HTTP, no authentication +- ClusterRole includes `tokenreviews` and `subjectaccessreviews` for kube-rbac auth +- The metrics Service (`controller-service-metrics.yaml`) exposes both `stat` and `metrics` ports, created only when ServiceMonitor is enabled + +### What HUG chart intentionally does NOT have (compared to kubernetes-ingress) + +- No IngressClass +- No ConfigMap-based HAProxy configuration +- No proxy service / fetch sync mode +- No publish-service +- No default TLS cert generation + +### HUG Source Project + +Source code lives at: `/home/zlatko/src/gitlab.int.haproxy.com/zbratkovic/unified-k8s-gateway` + +Key paths in source: +- `cmd/controller/main.go` - controller entry point +- `hug/configuration/configuration.go` - CLI flags definition +- `build/Dockerfile` - container image build +- `api/definition/` - CRD definitions +- `example/dev-init/` - example Gateway/HTTPRoute manifests +- `documentation/metrics*.md` - metrics documentation + +### HUG Controller Flags + +All flags (for `extraArgs`): + +| Flag | Default | Description | +|---|---|---| +| `--hugconf-crd` | | `namespace/name` of the HugConf CRD | +| `--controller-name` | `gate.haproxy.org/hug` | `spec.controllerName` GatewayClass selector | +| `--ipv4-bind-address` | | IPv4 address to bind to | +| `--ipv6-bind-address` | | IPv6 address to bind to | +| `--log-type` | `json` | Log output type (`text` or `json`) | +| `--job-gwapi` | | Install Gateway API experimental CRDs for given version (e.g. `1.3.0`) and exit | +| `--namespaces` | | Comma-separated list of namespaces to monitor | +| `--stats-port` | `1024` | Port for HAProxy stats | +| `--controller-port` | `31060` | Port for controller metrics (prometheus) | +| `--sync-period` | `0` | Period for HAProxy config computation (e.g. `5s`, `1m`) | +| `--startup-sync-period` | `0` | Startup period for HAProxy config computation | +| `--cache-resync-period` | `0` | Controller-runtime manager cache SyncPeriod (default: 10 hours) | +| `--add-stats-port` | `true` | Add stats port bind to existing stats frontend | +| `--force-restart-haproxy` | `false` | Force HAProxy restart at controller startup | +| `--leader-election-enabled` | `false` | Enable leader election | +| `--with-s6-overlay` | `false` | Use s6 overlay to start/stop/restart HAProxy | +| `--with-pebble` | `false` | Use pebble to start/stop/restart HAProxy | +| `--disable-ipv4` | `false` | Disable IPv4 support | +| `--disable-ipv6` | `false` | Disable IPv6 support | +| `--job-check-crd` | `false` | Run CRD refresh job and exit | +| `-e` / `--external` | `false` | Use as external controller (out of k8s cluster) | +| `--external-config-dir` | | Path to HAProxy configuration directory | +| `--external-haproxy-binary` | | Path to HAProxy binary | +| `--external-runtime-dir` | | Path to HAProxy runtime directory | +| `--external-state-dir` | | Path to HAProxy state directory | +| `--external-aux-dir` | | Path to HAProxy aux directory | +| `--metrics-auth` | `none` | Metrics endpoint auth mode: `none`, `kube-rbac`, `basic` | +| `--metrics-basic-auth-user` | | Basic auth username (when `--metrics-auth=basic`) | +| `--metrics-basic-auth-password` | | Basic auth password (when `--metrics-auth=basic`) | +| `-t` | `false` | Simulate running HAProxy (test mode) | + +Note: The HUG binary default for `--metrics-auth` is `none`, but the Helm chart overrides this to `kube-rbac` via `controller.metricsAuth`. + +## CI Values Files + +23 test value files in `haproxy-unified-gateway/ci/`: +- 8 DaemonSet variants (default, customnodeport, extraargs, extraenvs, extraports, hostport, serviceannotation, strategy) +- 15 Deployment variants (default, customnodeport, disabled-jobs, extraargs, extraenvs, extraports, hpa, hugconf, keda, keda-advanced, metrics-none, pdb, podmonitor, servicemonitor, strategy) + +Naming convention: `--values.yaml` + +## Testing + +Three test scripts in `test/`: + +| Script | Purpose | +|---|---| +| `test/local-test.sh` | Offline lint + template validation (no cluster needed) | +| `test/integration-test.sh` | Deploy to a real Kind cluster and verify resources | +| `test/ct-test.sh` | Wrapper around `ct` (chart-testing), matches CircleCI pipeline | + +### local-test.sh + +Tests: Chart.yaml metadata, helm lint, helm template, Deployment vs DaemonSet switching, HugConf cleanup hooks, metrics port rendering + `--metrics-auth` flag, ServiceMonitor/PodMonitor rendering, all ci/ values files. + +### integration-test.sh + +Tests on a real Kind cluster: default install, DaemonSet mode, HPA, PDB, metrics port (container port 31060, `--metrics-auth=kube-rbac` arg), ServiceMonitor/PodMonitor with metrics Service port verification, HugConf cleanup on uninstall, all ci/ values files. + +`TEST_FILTER` values: `defaults`, `daemonset`, `hpa`, `pdb`, `metrics-port`, `monitoring`, `hugconf-cleanup`, `ci` + +### ct-test.sh + +Runs `ct lint` and `ct install` locally, same as CircleCI. Auto-downloads `chart_schema.yaml` and `lintconf.yaml` from the ct release on first run (files are gitignored). + +Modes: `lint`, `install`, `all` + +### CircleCI Pipeline + +`.circleci/config.yml` workflow: `lint-scripts` (shellcheck) -> `lint-charts` (ct lint) -> `install-charts` (ct install on Kind) -> `release-charts` (helm package + push to GitHub Releases, GHCR OCI, update gh-pages index). + +## Conventions + +- Template names prefixed with `controller-` for controller-specific resources +- All templates use `include ".fullname"` for resource naming +- All templates use `include ".namespace"` for namespace +- Helm hooks used for CRD installation jobs (post-install, pre-upgrade) +- ArgoCD hook annotations included alongside Helm hooks +- HugConf CR is a post-install hook (weight 5) that runs after the CRD job (weight 0) +- HugConf cleanup is a pre-delete hook Job +- Security context: non-root (UID 1000), CAP_NET_BIND_SERVICE, seccomp RuntimeDefault +- ServiceMonitor/PodMonitor gated behind `.Capabilities.APIVersions "monitoring.coreos.com/v1"` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e041169 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,89 @@ +# Contributing Guidelines + +Contributions are welcome via GitHub pull requests. +This document outlines the process to help get your contribution accepted. + +## Sign off Your Work + +The Developer Certificate of Origin (DCO) is a lightweight way for contributors to certify that they wrote or otherwise have the right to submit the code they are contributing to the project. +Here is the full text of the [DCO](http://developercertificate.org/): + +``` +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +1 Letterman Drive +Suite D4700 +San Francisco, CA, 94129 + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +``` + +Contributors must sign-off that they adhere to these requirements by adding a `Signed-off-by` line to commit messages. + +``` +This is my commit message + +Signed-off-by: Random J Developer +``` + +Git has a `-s` command line option to append this automatically to your commit message: + +```console +$ git commit -s -m 'This is my commit message' +``` + +## How to Contribute + +1. Fork this repository, develop, and test your changes. +1. Remember to sign off your commits as described above. +1. Submit a pull request. + +**_NOTE_**: In order to make testing and merging of PRs easier, please submit changes to multiple charts in separate PRs. + +### Technical Requirements + +- Must pass linting and installing with the [chart-testing](https://github.com/helm/chart-testing) tool +- Must follow [best practices](https://helm.sh/docs/chart_best_practices/) and [review guidelines](https://github.com/helm/charts/blob/master/REVIEW_GUIDELINES.md) + +### Documentation Requirements + +- A chart's `README.md` must include configuration options +- A chart's `NOTES.txt` must include relevant post-installation information + +### Merge Approval and Release Process + +- Must pass DCO check +- Must pass CI jobs for linting and installing changed charts +- Any change to a chart requires a version bump following [semver](https://semver.org/) principles + +Once changes have been merged, the release job will automatically run to package and release changed charts. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d9a10c0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f9cea5 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# ![HAProxy](https://github.com/haproxytech/kubernetes-ingress/raw/master/assets/images/haproxy-weblogo-210x49.png "HAProxy") + +## HAProxy Helm Charts + +![GitHub](https://img.shields.io/github/license/haproxytech/helm-charts) +[![CircleCI](https://circleci.com/gh/haproxytech/helm-charts/tree/main.svg?style=svg)](https://circleci.com/gh/haproxytech/helm-charts/tree/main) +[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/haproxytech)](https://artifacthub.io/packages/search?repo=haproxytech) + +This repository hosts official [HAProxy Technologies](https://www.haproxy.com/) Helm Charts for deploying [HAProxy Load Balancer](https://github.com/haproxy/haproxy) and [Ingress controller](https://github.com/haproxytech/kubernetes-ingress) on [Kubernetes](https://kubernetes.io/). + +### Changelogs + +Changelog for **Helm charts** in this repository are maintained automatically at ArtifactHub separately for [HAProxy](https://artifacthub.io/packages/helm/haproxytech/haproxy?modal=changelog) and [Ingress controller](https://artifacthub.io/packages/helm/haproxytech/kubernetes-ingress?modal=changelog) + +Changelog for the packaged projects are available separately for [HAProxy](https://github.com/haproxy/haproxy/blob/master/CHANGELOG) and [HAProxy Technologies Ingress controller](https://github.com/haproxytech/kubernetes-ingress/releases/), with release notes and other documentation available at their respective project pages. + +## Before you begin + +### Setup a Kubernetes Cluster + +The quickest way to setup a Kubernetes cluster is with [Azure Kubernetes Service](https://azure.microsoft.com/en-us/services/kubernetes-service/), [AWS Elastic Kubernetes Service](https://aws.amazon.com/eks/) or [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/) using their respective quick-start guides. + +For setting up Kubernetes on other cloud platforms or bare-metal servers refer to the Kubernetes [getting started guide](http://kubernetes.io/docs/getting-started-guides/). + +### Install Helm + +Get the latest [Helm release](https://github.com/helm/helm#install). + +### Add Helm chart repo + +Once you have Helm installed, add the repo as follows: + +```console +helm repo add haproxytech https://haproxytech.github.io/helm-charts +helm repo update +``` + +HAProxy Helm charts can be also found on [ArtifactHub](https://artifacthub.io/packages/search?repo=haproxytech). + +## Search and install charts + +```console +helm search repo haproxytech/ +helm install my-release haproxytech/ +``` + +**_NOTE_**: For instructions on how to install a chart follow instructions in its `README.md`. + +## Contributing + +We welcome all contributions. Please refer to [guidelines](CONTRIBUTING.md) on how to make a contribution. diff --git a/catalog-info.yaml b/catalog-info.yaml new file mode 100644 index 0000000..7c71770 --- /dev/null +++ b/catalog-info.yaml @@ -0,0 +1,101 @@ +# ─── System: groups all per-service Components for this application ─── +apiVersion: backstage.io/v1alpha1 +kind: System +metadata: + name: haproxy-unified + description: "haproxy-unified — deployed via ArgoCD into demo-apps" + labels: + backstage.io/environment: "dev" + app.kubernetes.io/managed-by: "backstage" + tags: + - deployment + - argocd + + annotations: + argocd/app-name: "haproxy-unified" + argocd/app-namespace: "argocd" + argocd/instance-name: "" + backstage.io/techdocs-ref: dir:. + backstage.io/source-location: "url:https://gitea.kyndemo.live/validate/haproxy-unified/src/branch/main" + backstage.io/kubernetes-namespace: "demo-apps" + backstage.io/kubernetes-label-selector: "app.kubernetes.io/managed-by=backstage" + gitea.kyndemo.live/repo-slug: "validate/haproxy-unified" + + links: + - url: https://haproxy-unified.kyndemo.live + title: Live Application + icon: web + - url: https://gitea.kyndemo.live/validate/haproxy-unified + title: Repository + icon: github + - url: https://argocd.kyndemo.live/applications/haproxy-unified + title: ArgoCD App + icon: dashboard + +spec: + owner: "platform-engineering" + domain: platform + dependsOn: + - component:default/argocd-service + + + - resource:default/k6-operator + +# ─── Primary Component (always present, matches component_id) ─────── +--- +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: haproxy-unified + description: "haproxy-unified — deployed via ArgoCD into demo-apps" + labels: + backstage.io/environment: "dev" + app.kubernetes.io/managed-by: "backstage" + tags: + - deployment + - argocd + + - load-testing + - k6 + + + annotations: + argocd/app-name: "haproxy-unified" + argocd/app-namespace: "argocd" + argocd/instance-name: "" + backstage.io/techdocs-ref: dir:. + backstage.io/source-location: "url:https://gitea.kyndemo.live/validate/haproxy-unified/src/branch/main" + backstage.io/kubernetes-namespace: "demo-apps" + backstage.io/kubernetes-label-selector: "app=haproxy-unified" + gitea.kyndemo.live/repo-slug: "validate/haproxy-unified" + + k6/enabled: "true" + k6/test-configmap: "k6-test-haproxy-unified" + k6/test-namespace: "demo-apps" + k6/target-service: "frontend" + + links: + - url: https://haproxy-unified.kyndemo.live + title: Live Application + icon: web + - url: https://gitea.kyndemo.live/validate/haproxy-unified + title: Repository + icon: github + - url: https://argocd.kyndemo.live/applications/haproxy-unified + title: ArgoCD App + icon: dashboard + +spec: + type: service + owner: "platform-engineering" + lifecycle: experimental + + system: haproxy-unified + dependsOn: + - component:default/argocd-service + + + - resource:default/k6-operator + +# ─── Per-service Components (from Watcher discovery) ───────────────── + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..39dd111 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,65 @@ +# haproxy-unified + +Deployed from **custom** via the Backstage Hello Demo template. + +| Property | Value | +|---|---| +| **Environment** | `dev` | +| **Namespace** | `demo-apps` | +| **ArgoCD Project** | `` | +| **Branch** | `main` | +| **Observability** | Disabled | + +## Quick Links + +- **Repository**: [https://gitea.kyndemo.live/validate/haproxy-unified](https://gitea.kyndemo.live/validate/haproxy-unified) +- **ArgoCD**: [https://argocd.kyndemo.live/applications/haproxy-unified](https://argocd.kyndemo.live/applications/haproxy-unified) +- **Live App**: [https://haproxy-unified.kyndemo.live](https://haproxy-unified.kyndemo.live) + + +## Architecture + +This service was scaffolded using the **Application Migration Factory** Backstage template. + +**Deployment flow:** + +1. Source cloned from `custom` +2. Catalog entity and CI workflows overlaid by Backstage + +4. ArgoCD Application created targeting the `demo-apps` namespace +5. ArgoCD continuously syncs from the `main` branch + +**ArgoCD sync path:** `kubernetes-manifests` + +## Development Workflow + +```bash +git clone https://gitea.kyndemo.live/validate/haproxy-unified.git +cd haproxy-unified +# make changes, then: +git add . && git commit -m "your change" && git push origin main +``` + +ArgoCD monitors the repository and automatically syncs changes to the `demo-apps` namespace. + +## Rollback + +To roll back to a previous version: + +1. Open the [ArgoCD UI](https://argocd.kyndemo.live/applications/haproxy-unified) +2. Click **History and Rollback** +3. Select the desired revision and click **Rollback** + +Alternatively, revert the commit in Git and push — ArgoCD will auto-sync the rollback. + + + +## SLOs and Monitoring + +Define your service level objectives here once the service is stable: + +| SLI | Target | Dashboard | +|---|---|---| +| Availability | 99.9% | [Grafana](https://grafana.kyndemo.live) | +| Latency (p99) | < 500ms | [Grafana](https://grafana.kyndemo.live) | +| Error rate | < 1% | [Grafana](https://grafana.kyndemo.live) | diff --git a/haproxy-unified-gateway/.helmignore b/haproxy-unified-gateway/.helmignore new file mode 100644 index 0000000..5206f4e --- /dev/null +++ b/haproxy-unified-gateway/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/haproxy-unified-gateway/Chart.yaml b/haproxy-unified-gateway/Chart.yaml new file mode 100644 index 0000000..7243adc --- /dev/null +++ b/haproxy-unified-gateway/Chart.yaml @@ -0,0 +1,39 @@ +# Copyright 2026 HAProxy Technologies LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v2 +name: haproxy-unified-gateway +description: A Helm chart for HAProxy Unified Gateway - Kubernetes Gateway API Controller +type: application +version: 1.0.0 +appVersion: 1.0.1 +kubeVersion: ">=1.26.0-0" +keywords: + - haproxy + - gateway-api + - gateway + - load-balancer +home: https://github.com/haproxytech/helm-charts/tree/main/haproxy-unified-gateway +sources: + - https://github.com/haproxytech/haproxy-unified-gateway +icon: https://raw.githubusercontent.com/haproxytech/helm-charts/main/kubernetes-ingress/chart-icon.png +maintainers: + - name: Zlatko Bratkovic + email: zbratkovic@haproxy.com + - name: Dinko Korunic + email: dkorunic@haproxy.com +annotations: + artifacthub.io/license: Apache-2.0 + artifacthub.io/changes: |- + - Use HAProxy Unified Gateway 1.0.1 version for base image diff --git a/haproxy-unified-gateway/README.md b/haproxy-unified-gateway/README.md new file mode 100644 index 0000000..86d362e --- /dev/null +++ b/haproxy-unified-gateway/README.md @@ -0,0 +1,495 @@ +# ![HAProxy](https://github.com/haproxytech/kubernetes-ingress/raw/master/assets/images/haproxy-weblogo-210x49.png "HAProxy") + +## HAProxy Unified Gateway + +A Kubernetes Gateway API controller powered by HAProxy. HAProxy Unified Gateway (HUG) implements the [Gateway API](https://gateway-api.sigs.k8s.io/) specification to provide advanced traffic management capabilities. + +## Introduction + +This chart bootstraps a HAProxy Unified Gateway deployment/daemonset on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager. + +### Prerequisites + +- Kubernetes 1.26+ +- Helm 3.6+ (recommended 3.7+) + +## Before you begin + +### Setting up a Kubernetes Cluster + +The quickest way to setup a Kubernetes cluster is with [Azure Kubernetes Service](https://azure.microsoft.com/en-us/services/kubernetes-service/), [AWS Elastic Kubernetes Service](https://aws.amazon.com/eks/) or [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/) using their respective quick-start guides. + +For setting up Kubernetes on other cloud platforms or bare-metal servers refer to the Kubernetes [getting started guide](http://kubernetes.io/docs/getting-started-guides/). + +### Install Helm + +Get the latest [Helm release](https://github.com/helm/helm#install). + +### Adding Helm chart repo + +Once you have Helm installed, add the haproxytech Chart Repository as follows: + +```console +helm repo add haproxytech https://haproxytech.github.io/helm-charts + +helm repo update +``` + +## Installing the chart + +To install the chart with Helm v3 as _my-release_ deployment: + +```console +helm install my-release haproxytech/haproxy-unified-gateway +``` + +### Installing with unique name + +To auto-generate controller and its resources names when installing, use the following: + +```console +helm install haproxytech/haproxy-unified-gateway \ + --generate-name +``` + +### Installing from a private registry + +To install the chart using a private registry for controller into a separate namespace _prod_. + +**_NOTE_**: Helm v3 requires namespace to be precreated (eg. with `kubectl create namespace prod`) + +```console +helm install my-release haproxytech/haproxy-unified-gateway \ + --namespace prod \ + --set controller.image.tag=SOMETAG \ + --set controller.imagePullSecrets[0].name=my-pull-secret +``` + +### Using values from YAML file + +As opposed to using many `--set` invocations, much simpler approach is to define value overrides in a separate YAML file and specify them when invoking Helm: + +_myhug.yaml_: + +```yaml +controller: + kind: DaemonSet + service: + type: LoadBalancer + annotations: + service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true" + service.beta.kubernetes.io/aws-load-balancer-internal: 0.0.0.0/0 +``` + +And invoking Helm becomes: + +```console +helm install my-release -f myhug.yaml haproxytech/haproxy-unified-gateway +``` + +### Installing as DaemonSet + +Default controller mode is [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/), but it is possible to use [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) as well: + +```console +helm install my-release haproxytech/haproxy-unified-gateway \ + --set controller.kind=DaemonSet +``` + +### Installing with host networking (DaemonSet) + +When using DaemonSet mode, you can enable host networking and host ports: + +```console +helm install my-release haproxytech/haproxy-unified-gateway \ + --set controller.kind=DaemonSet \ + --set controller.daemonset.useHostNetwork=true \ + --set controller.dnsPolicy=ClusterFirstWithHostNet +``` + +### Installing with service annotations + +On some environments like EKS and GKE there might be a need to pass service annotations. Syntax can become a little tedious however: + +```console +helm install my-release haproxytech/haproxy-unified-gateway \ + --set controller.service.type=LoadBalancer \ + --set controller.service.annotations."service\.beta\.kubernetes\.io/aws-load-balancer-internal"="0.0.0.0/0" \ + --set controller.service.annotations."service\.beta\.kubernetes\.io/aws-load-balancer-cross-zone-load-balancing-enabled"="true" +``` + +**_NOTE_**: With helm `--set` it is needed to put quotes and escape dots in the annotation key and commas in the value string. + +### Installing with Horizontal Pod Autoscaler (HPA) + +[HPA](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/) automatically scales number of replicas in Deployment and adjusts replica count for the controller: + +```console +helm install my-release haproxytech/haproxy-unified-gateway \ + --set controller.autoscaling.enabled=true +``` + +### Enabling Prometheus monitoring + +HUG exposes two separate metrics endpoints: + +- **`stat`** (port 31024) — HAProxy native metrics (`haproxy_*` prefix): connections, request rates, backend health, latency, error codes +- **`metrics`** (port 31060) — HUG controller metrics (`hug_*` prefix): event batch processing, config generation, cert/map operations, HAProxy reloads + +The chart supports both [ServiceMonitor](https://github.com/prometheus-operator/prometheus-operator/blob/master/Documentation/user-guides/getting-started.md) and [PodMonitor](https://github.com/prometheus-operator/prometheus-operator/blob/master/Documentation/design.md#podmonitor) for Prometheus Operator integration. These are mutually exclusive — enable only one. By default, both endpoints are scraped. + +**Note:** Requires Prometheus Operator installed in the cluster. The `monitoring.coreos.com/v1` API must be available. + +#### Metrics authentication + +The controller metrics endpoint (`metrics` port) supports three authentication modes via `controller.metricsAuth`: + +| Mode | Default | Protocol | Description | +| --- | --- | --- | --- | +| `kube-rbac` | **yes** | HTTPS | Kubernetes TokenReview/SubjectAccessReview — Prometheus authenticates with its ServiceAccount token | +| `none` | | HTTP | No authentication | +| `basic` | | HTTPS | HTTP Basic Authentication with username/password | + +#### Default setup (kube-rbac) + +By default the chart uses `kube-rbac` authentication. The controller serves metrics over HTTPS and validates bearer tokens via the Kubernetes API. To set it up: + +**Step 1.** Create a ClusterRole that grants access to the `/metrics` endpoint and bind it to the Prometheus ServiceAccount: + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: hug-metrics-reader +rules: + - nonResourceURLs: ["/metrics"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: hug-metrics-reader +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: hug-metrics-reader +subjects: + - kind: ServiceAccount + name: prometheus # adjust to your Prometheus SA name + namespace: monitoring # adjust to your Prometheus namespace +``` + +**Step 2.** Enable the ServiceMonitor (or PodMonitor). The default endpoints are pre-configured for kube-rbac — `stat` uses plain HTTP, `metrics` uses HTTPS with the Prometheus pod's ServiceAccount token: + +```yaml +controller: + serviceMonitor: + enabled: true + extraLabels: + release: prometheus # match your Prometheus serviceMonitorSelector +``` + +That's it. The default `values.yaml` endpoints already include the correct HTTPS + bearer token configuration for the `metrics` port: + +```yaml +# Default endpoints (already set in values.yaml): +endpoints: + - port: stat + path: /metrics + scheme: http + interval: 30s + - port: metrics + path: /metrics + scheme: https + interval: 30s + tlsConfig: + insecureSkipVerify: true + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token +``` + +#### Using no authentication + +To disable metrics authentication: + +```yaml +controller: + metricsAuth: none + serviceMonitor: + enabled: true + endpoints: + - port: stat + path: /metrics + scheme: http + interval: 30s + - port: metrics + path: /metrics + scheme: http + interval: 30s +``` + +#### Using basic authentication + +```yaml +controller: + metricsAuth: basic + extraArgs: + - --metrics-basic-auth-user=prometheus + - --metrics-basic-auth-password=secret + serviceMonitor: + enabled: true + endpoints: + - port: stat + path: /metrics + scheme: http + interval: 30s + - port: metrics + path: /metrics + scheme: https + interval: 30s + tlsConfig: + insecureSkipVerify: true + basicAuth: + username: + name: hug-metrics-basic-auth + key: username + password: + name: hug-metrics-basic-auth + key: password +``` + +#### Using PodMonitor instead of ServiceMonitor + +PodMonitor scrapes pods directly without creating an extra metrics Service. Replace `serviceMonitor` with `podMonitor` in any of the examples above: + +```yaml +controller: + podMonitor: + enabled: true + extraLabels: + release: prometheus +``` + +### Configuring HugConf + +The chart creates a HugConf custom resource for controller configuration. You can customize logging, and optionally reference Global and Defaults custom resources: + +```yaml +hugconf: + logging: + defaultLevel: Debug + categoryLevelList: + - category: "gate" + level: "Debug" + - category: "k8s" + level: "Info" + # Reference a Global CR for HAProxy global section customization + globalRef: + group: gate.v3.haproxy.org + kind: Global + name: global + # Reference a Defaults CR for HAProxy defaults section customization + defaultsRef: + group: gate.v3.haproxy.org + kind: Defaults + name: haproxytech +``` + +The Global and Defaults CRDs are automatically installed by the CRD job. When a `globalRef` or `defaultsRef` is set, the controller uses the referenced CR to configure the HAProxy global/defaults sections. When removed, built-in defaults are restored. + +### Adding extra ports + +By default the chart exposes four container ports: `http` (31080), `https` (31443), `stat` (31024) and `metrics` (31060). Additional ports can be added in two places: + +1. **`controller.containerPort`** — exposes the port on the container (pod spec) +2. **`controller.service.extraPorts`** — exposes the port on the Service + +#### Using `--set` flags + +To add a container port only (e.g. for a sidecar or internal use): + +```console +helm install my-release haproxytech/haproxy-unified-gateway \ + --set controller.containerPort.custom=8080 +``` + +To also expose it on the Service: + +```console +helm install my-release haproxytech/haproxy-unified-gateway \ + --set controller.containerPort.custom=8080 \ + --set controller.service.extraPorts[0].name=custom \ + --set controller.service.extraPorts[0].port=8080 \ + --set controller.service.extraPorts[0].targetPort=8080 \ + --set controller.service.extraPorts[0].protocol=TCP +``` + +#### Using a values file + +For multiple extra ports, a values file is cleaner: + +```yaml +controller: + containerPort: + http: 31080 + https: 31443 + stat: 31024 + custom: 8080 + grpc: 9090 + service: + extraPorts: + - name: custom + port: 8080 + targetPort: 8080 + protocol: TCP + - name: grpc + port: 9090 + targetPort: 9090 + protocol: TCP +``` + +#### DaemonSet with host ports + +For DaemonSet mode with host ports, also add matching entries in `controller.daemonset.hostPorts`: + +```yaml +controller: + kind: DaemonSet + containerPort: + http: 31080 + https: 31443 + stat: 31024 + custom: 8080 + daemonset: + useHostPort: true + hostPorts: + http: 80 + https: 443 + stat: 1024 + custom: 8080 + service: + extraPorts: + - name: custom + port: 8080 + targetPort: 8080 + protocol: TCP +``` + +### Passing extra arguments + +Additional controller flags can be passed via `extraArgs`: + +```yaml +controller: + extraArgs: + - --controller-name=gate.haproxy.org/hug + - --namespaces=default,production + - --leader-election-enabled +``` + +#### Available controller flags + +| Flag | Default | Description | +| --- | --- | --- | +| `--controller-name` | `gate.haproxy.org/hug` | `spec.controllerName` GatewayClass selector | +| `--namespaces` | | Comma-separated list of namespaces to monitor | +| `--ipv4-bind-address` | | IPv4 address to bind to | +| `--ipv6-bind-address` | | IPv6 address to bind to | +| `--disable-ipv4` | `false` | Disable IPv4 support | +| `--disable-ipv6` | `false` | Disable IPv6 support | +| `--stats-port` | `1024` | Port for HAProxy stats | +| `--controller-port` | `31060` | Port for controller metrics (prometheus) | +| `--log-type` | `json` | Log output type (`text` or `json`) | +| `--sync-period` | `0` | Period at which the controller computes HAProxy configuration (e.g. `5s`, `1m`) | +| `--startup-sync-period` | `0` | Startup period for HAProxy config computation | +| `--cache-resync-period` | `0` | Controller-runtime manager cache SyncPeriod (defaults to 10 hours if not set) | +| `--leader-election-enabled` | `false` | Enable leader election | +| `--add-stats-port` | `true` | Add stats port bind to existing stats frontend | +| `--metrics-auth` | `none` | Metrics endpoint auth mode: `none`, `kube-rbac`, `basic` | +| `--metrics-basic-auth-user` | | Basic auth username (when `--metrics-auth=basic`) | +| `--metrics-basic-auth-password` | | Basic auth password (when `--metrics-auth=basic`) | + +**Note:** The `--hugconf-crd` flag is set automatically by the chart via the `hugconfCrd` helper. The `--job-check-crd` and `--job-gwapi` flags are used internally by the CRD/Gateway API installation jobs. + +### Installing with KEDA autoscaling + +[KEDA](https://keda.sh/) provides event-driven autoscaling. It is mutually exclusive with HPA — when KEDA is enabled, HPA is automatically disabled even if `autoscaling.enabled` is set to `true`. + +```yaml +controller: + keda: + enabled: true + minReplicas: 2 + maxReplicas: 20 + pollingInterval: 30 + cooldownPeriod: 300 + restoreToOriginalReplicaCount: false + scaledObject: + annotations: {} + triggers: + - type: prometheus + metadata: + serverAddress: http://:9090 + metricName: haproxy_process_idle_time_percent + threshold: '50' + query: avg(100-avg_over_time(haproxy_process_idle_time_percent{job="haproxy-unified-gateway"}[2m])) +``` + +Optional advanced configuration: + +```yaml +controller: + keda: + enabled: true + # ...triggers, minReplicas, maxReplicas... + fallback: + failureThreshold: 3 + replicas: 5 + horizontalPodAutoscalerConfig: + behavior: + scaleDown: + stabilizationWindowSeconds: 300 + policies: + - type: Pods + value: 1 + periodSeconds: 300 +``` + +### Disabling CRD/Gateway API installation jobs + +By default, the chart includes Helm hook jobs that install HUG CRDs and Gateway API CRDs. To disable them: + +```console +helm install my-release haproxytech/haproxy-unified-gateway \ + --set crdjob.enabled=false \ + --set gwapijob.enabled=false +``` + +## Upgrading the chart + +To upgrade the _my-release_ deployment: + +```console +helm upgrade my-release haproxytech/haproxy-unified-gateway +``` + +## Uninstalling the chart + +To uninstall/delete the _my-release_ deployment: + +```console +helm delete my-release +``` + +## Debugging + +It is possible to generate a set of YAML files for testing/debugging: + +```console +helm install my-release haproxytech/haproxy-unified-gateway \ + --debug \ + --dry-run +``` + +## Contributing + +We welcome all contributions. Please refer to [guidelines](../CONTRIBUTING.md) on how to make a contribution. diff --git a/haproxy-unified-gateway/ci/daemonset-customnodeport-values.yaml b/haproxy-unified-gateway/ci/daemonset-customnodeport-values.yaml new file mode 100644 index 0000000..c5381db --- /dev/null +++ b/haproxy-unified-gateway/ci/daemonset-customnodeport-values.yaml @@ -0,0 +1,12 @@ +controller: + kind: DaemonSet + service: + type: NodePort + http: + port: 8080 + targetPort: 8080 + nodePort: 30080 + https: + port: 8443 + targetPort: 8443 + nodePort: 30443 diff --git a/haproxy-unified-gateway/ci/daemonset-default-values.yaml b/haproxy-unified-gateway/ci/daemonset-default-values.yaml new file mode 100644 index 0000000..ddb2562 --- /dev/null +++ b/haproxy-unified-gateway/ci/daemonset-default-values.yaml @@ -0,0 +1,2 @@ +controller: + kind: DaemonSet diff --git a/haproxy-unified-gateway/ci/daemonset-extraargs-values.yaml b/haproxy-unified-gateway/ci/daemonset-extraargs-values.yaml new file mode 100644 index 0000000..986afa6 --- /dev/null +++ b/haproxy-unified-gateway/ci/daemonset-extraargs-values.yaml @@ -0,0 +1,5 @@ +controller: + kind: DaemonSet + extraArgs: + - --controller-name=gate.haproxy.org/hug + - --namespaces=default diff --git a/haproxy-unified-gateway/ci/daemonset-extraenvs-values.yaml b/haproxy-unified-gateway/ci/daemonset-extraenvs-values.yaml new file mode 100644 index 0000000..35294fa --- /dev/null +++ b/haproxy-unified-gateway/ci/daemonset-extraenvs-values.yaml @@ -0,0 +1,7 @@ +controller: + kind: DaemonSet + extraEnvs: + - name: TEST_STR1 + value: foo + - name: TEST_STR2 + value: baz diff --git a/haproxy-unified-gateway/ci/daemonset-extraports-values.yaml b/haproxy-unified-gateway/ci/daemonset-extraports-values.yaml new file mode 100644 index 0000000..3b5f316 --- /dev/null +++ b/haproxy-unified-gateway/ci/daemonset-extraports-values.yaml @@ -0,0 +1,21 @@ +## Test: DaemonSet with extra container and service ports, including host ports +controller: + kind: DaemonSet + containerPort: + http: 31080 + https: 31443 + stat: 31024 + custom: 8080 + daemonset: + useHostPort: true + hostPorts: + http: 80 + https: 443 + stat: 1024 + custom: 8080 + service: + extraPorts: + - name: custom + port: 8080 + targetPort: 8080 + protocol: TCP diff --git a/haproxy-unified-gateway/ci/daemonset-hostport-values.yaml b/haproxy-unified-gateway/ci/daemonset-hostport-values.yaml new file mode 100644 index 0000000..45042ea --- /dev/null +++ b/haproxy-unified-gateway/ci/daemonset-hostport-values.yaml @@ -0,0 +1,8 @@ +controller: + kind: DaemonSet + daemonset: + useHostPort: true + hostPorts: + http: 80 + https: 443 + stat: 1024 diff --git a/haproxy-unified-gateway/ci/daemonset-serviceannotation-values.yaml b/haproxy-unified-gateway/ci/daemonset-serviceannotation-values.yaml new file mode 100644 index 0000000..b538cb5 --- /dev/null +++ b/haproxy-unified-gateway/ci/daemonset-serviceannotation-values.yaml @@ -0,0 +1,5 @@ +controller: + kind: DaemonSet + service: + annotations: + service.beta.kubernetes.io/aws-load-balancer-internal: 0.0.0.0/0 diff --git a/haproxy-unified-gateway/ci/daemonset-strategy-values.yaml b/haproxy-unified-gateway/ci/daemonset-strategy-values.yaml new file mode 100644 index 0000000..ed45d7a --- /dev/null +++ b/haproxy-unified-gateway/ci/daemonset-strategy-values.yaml @@ -0,0 +1,7 @@ +controller: + kind: DaemonSet + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 diff --git a/haproxy-unified-gateway/ci/deployment-customnodeport-values.yaml b/haproxy-unified-gateway/ci/deployment-customnodeport-values.yaml new file mode 100644 index 0000000..cb0025c --- /dev/null +++ b/haproxy-unified-gateway/ci/deployment-customnodeport-values.yaml @@ -0,0 +1,11 @@ +controller: + service: + type: NodePort + http: + port: 8080 + targetPort: 8080 + nodePort: 30080 + https: + port: 8443 + targetPort: 8443 + nodePort: 30443 diff --git a/haproxy-unified-gateway/ci/deployment-default-values.yaml b/haproxy-unified-gateway/ci/deployment-default-values.yaml new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/haproxy-unified-gateway/ci/deployment-default-values.yaml @@ -0,0 +1 @@ +# diff --git a/haproxy-unified-gateway/ci/deployment-disabled-jobs-values.yaml b/haproxy-unified-gateway/ci/deployment-disabled-jobs-values.yaml new file mode 100644 index 0000000..b909770 --- /dev/null +++ b/haproxy-unified-gateway/ci/deployment-disabled-jobs-values.yaml @@ -0,0 +1,4 @@ +crdjob: + enabled: false +gwapijob: + enabled: false diff --git a/haproxy-unified-gateway/ci/deployment-extraargs-values.yaml b/haproxy-unified-gateway/ci/deployment-extraargs-values.yaml new file mode 100644 index 0000000..df17758 --- /dev/null +++ b/haproxy-unified-gateway/ci/deployment-extraargs-values.yaml @@ -0,0 +1,4 @@ +controller: + extraArgs: + - --controller-name=gate.haproxy.org/hug + - --namespaces=default diff --git a/haproxy-unified-gateway/ci/deployment-extraenvs-values.yaml b/haproxy-unified-gateway/ci/deployment-extraenvs-values.yaml new file mode 100644 index 0000000..1f9e30c --- /dev/null +++ b/haproxy-unified-gateway/ci/deployment-extraenvs-values.yaml @@ -0,0 +1,6 @@ +controller: + extraEnvs: + - name: TEST_STR1 + value: foo + - name: TEST_STR2 + value: baz diff --git a/haproxy-unified-gateway/ci/deployment-extraports-values.yaml b/haproxy-unified-gateway/ci/deployment-extraports-values.yaml new file mode 100644 index 0000000..8064008 --- /dev/null +++ b/haproxy-unified-gateway/ci/deployment-extraports-values.yaml @@ -0,0 +1,14 @@ +## Test: Deployment with extra container and service ports +controller: + kind: Deployment + containerPort: + http: 31080 + https: 31443 + stat: 31024 + custom: 8080 + service: + extraPorts: + - name: custom + port: 8080 + targetPort: 8080 + protocol: TCP diff --git a/haproxy-unified-gateway/ci/deployment-hpa-values.yaml b/haproxy-unified-gateway/ci/deployment-hpa-values.yaml new file mode 100644 index 0000000..3b55066 --- /dev/null +++ b/haproxy-unified-gateway/ci/deployment-hpa-values.yaml @@ -0,0 +1,6 @@ +controller: + autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 80 diff --git a/haproxy-unified-gateway/ci/deployment-hugconf-values.yaml b/haproxy-unified-gateway/ci/deployment-hugconf-values.yaml new file mode 100644 index 0000000..844d2a3 --- /dev/null +++ b/haproxy-unified-gateway/ci/deployment-hugconf-values.yaml @@ -0,0 +1,8 @@ +hugconf: + logging: + defaultLevel: Debug + categoryLevelList: + - category: "gate" + level: "Debug" + - category: "k8s" + level: "Info" diff --git a/haproxy-unified-gateway/ci/deployment-keda-advanced-values.yaml b/haproxy-unified-gateway/ci/deployment-keda-advanced-values.yaml new file mode 100644 index 0000000..c96586b --- /dev/null +++ b/haproxy-unified-gateway/ci/deployment-keda-advanced-values.yaml @@ -0,0 +1,34 @@ +controller: + keda: + enabled: true + minReplicas: 1 + maxReplicas: 10 + pollingInterval: 15 + cooldownPeriod: 600 + restoreToOriginalReplicaCount: true + scaledObject: + annotations: {} + fallback: + failureThreshold: 3 + replicas: 5 + horizontalPodAutoscalerConfig: + behavior: + scaleDown: + stabilizationWindowSeconds: 300 + policies: + - type: Pods + value: 1 + periodSeconds: 300 + triggers: + - type: prometheus + metadata: + serverAddress: http://prometheus:9090 + metricName: haproxy_process_idle_time_percent + threshold: '50' + query: avg(100-avg_over_time(haproxy_process_idle_time_percent{job="haproxy-unified-gateway"}[2m])) + # HPA should be ignored when KEDA is enabled + autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 80 diff --git a/haproxy-unified-gateway/ci/deployment-keda-values.yaml b/haproxy-unified-gateway/ci/deployment-keda-values.yaml new file mode 100644 index 0000000..c5de8de --- /dev/null +++ b/haproxy-unified-gateway/ci/deployment-keda-values.yaml @@ -0,0 +1,18 @@ +controller: + keda: + enabled: true + minReplicas: 2 + maxReplicas: 20 + pollingInterval: 30 + cooldownPeriod: 300 + restoreToOriginalReplicaCount: false + scaledObject: + annotations: + test-annotation: "test-value" + triggers: + - type: prometheus + metadata: + serverAddress: http://prometheus:9090 + metricName: haproxy_process_idle_time_percent + threshold: '50' + query: avg(100-avg_over_time(haproxy_process_idle_time_percent{job="haproxy-unified-gateway"}[2m])) diff --git a/haproxy-unified-gateway/ci/deployment-metrics-none-values.yaml b/haproxy-unified-gateway/ci/deployment-metrics-none-values.yaml new file mode 100644 index 0000000..e9f76bd --- /dev/null +++ b/haproxy-unified-gateway/ci/deployment-metrics-none-values.yaml @@ -0,0 +1,14 @@ +## Test: Deployment with metricsAuth=none (plain HTTP controller metrics) +controller: + metricsAuth: none + serviceMonitor: + enabled: true + endpoints: + - port: stat + path: /metrics + scheme: http + interval: 30s + - port: metrics + path: /metrics + scheme: http + interval: 30s diff --git a/haproxy-unified-gateway/ci/deployment-pdb-values.yaml b/haproxy-unified-gateway/ci/deployment-pdb-values.yaml new file mode 100644 index 0000000..408479b --- /dev/null +++ b/haproxy-unified-gateway/ci/deployment-pdb-values.yaml @@ -0,0 +1,5 @@ +controller: + replicaCount: 2 + podDisruptionBudget: + enabled: true + minAvailable: 1 diff --git a/haproxy-unified-gateway/ci/deployment-podmonitor-values.yaml b/haproxy-unified-gateway/ci/deployment-podmonitor-values.yaml new file mode 100644 index 0000000..74c0dd7 --- /dev/null +++ b/haproxy-unified-gateway/ci/deployment-podmonitor-values.yaml @@ -0,0 +1,10 @@ +controller: + podMonitor: + enabled: true + extraLabels: + release: prometheus + endpoints: + - port: stat + path: /metrics + scheme: http + interval: 30s diff --git a/haproxy-unified-gateway/ci/deployment-servicemonitor-values.yaml b/haproxy-unified-gateway/ci/deployment-servicemonitor-values.yaml new file mode 100644 index 0000000..e11578b --- /dev/null +++ b/haproxy-unified-gateway/ci/deployment-servicemonitor-values.yaml @@ -0,0 +1,18 @@ +## Test: Deployment with ServiceMonitor (both stat and controller metrics endpoints) +controller: + serviceMonitor: + enabled: true + extraLabels: + release: prometheus + endpoints: + - port: stat + path: /metrics + scheme: http + interval: 30s + - port: metrics + path: /metrics + scheme: https + interval: 30s + tlsConfig: + insecureSkipVerify: true + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token diff --git a/haproxy-unified-gateway/ci/deployment-strategy-values.yaml b/haproxy-unified-gateway/ci/deployment-strategy-values.yaml new file mode 100644 index 0000000..939312a --- /dev/null +++ b/haproxy-unified-gateway/ci/deployment-strategy-values.yaml @@ -0,0 +1,6 @@ +controller: + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 diff --git a/haproxy-unified-gateway/templates/NOTES.txt b/haproxy-unified-gateway/templates/NOTES.txt new file mode 100644 index 0000000..59b75d3 --- /dev/null +++ b/haproxy-unified-gateway/templates/NOTES.txt @@ -0,0 +1,51 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +HAProxy Unified Gateway has been installed. + +{{- if .Values.controller.service.enabled }} + +The controller is exposed via a {{ .Values.controller.service.type }} Service: + - HTTP: {{ .Values.controller.service.http.port }} + - HTTPS: {{ .Values.controller.service.https.port }} + - Stats: {{ .Values.controller.service.stat.port }} + +{{- if eq .Values.controller.service.type "NodePort" }} +Access the gateway using your node IP and the configured NodePort: + export NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="ExternalIP")].address}') + echo "HTTP: http://$NODE_IP:{{ .Values.controller.service.http.nodePort }}" + echo "HTTPS: https://$NODE_IP:{{ .Values.controller.service.https.nodePort }}" +{{- else if eq .Values.controller.service.type "LoadBalancer" }} +It may take a few minutes for the LoadBalancer IP to be available. +Watch the status with: + kubectl get svc {{ include "haproxy-unified-gateway.fullname" . }} -n {{ include "haproxy-unified-gateway.namespace" . }} -w +{{- end }} +{{- end }} + +{{- if .Values.crdjob.enabled }} + +A post-install/pre-upgrade Job will install/update the HUG CRDs automatically. +{{- end }} + +{{- if .Values.gwapijob.enabled }} + +A post-install/pre-upgrade Job will install Gateway API CRDs (v{{ .Values.gwapijob.version }}). +{{- end }} + +To check the controller status: + kubectl get pods -n {{ include "haproxy-unified-gateway.namespace" . }} -l "{{ include "haproxy-unified-gateway.selectorLabels" . | replace "\n" "," }}" + +For more information, visit: https://github.com/haproxytech/haproxy-unified-gateway diff --git a/haproxy-unified-gateway/templates/_helpers.tpl b/haproxy-unified-gateway/templates/_helpers.tpl new file mode 100644 index 0000000..ac6ba1f --- /dev/null +++ b/haproxy-unified-gateway/templates/_helpers.tpl @@ -0,0 +1,174 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{/* +Expand the name of the chart. +*/}} +{{- define "haproxy-unified-gateway.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "haproxy-unified-gateway.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "haproxy-unified-gateway.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Namespace to use. +*/}} +{{- define "haproxy-unified-gateway.namespace" -}} +{{- default .Release.Namespace .Values.namespaceOverride }} +{{- end }} + +{{/* +Selector labels. +*/}} +{{- define "haproxy-unified-gateway.selectorLabels" -}} +app.kubernetes.io/name: {{ include "haproxy-unified-gateway.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Helm chart metadata labels. +*/}} +{{- define "haproxy-unified-gateway.helmChartLabels" -}} +helm.sh/chart: {{ include "haproxy-unified-gateway.chart" . }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Common labels (selector + chart metadata). +*/}} +{{- define "haproxy-unified-gateway.labels" -}} +{{ include "haproxy-unified-gateway.selectorLabels" . }} +{{ include "haproxy-unified-gateway.helmChartLabels" . }} +{{- end }} + +{{/* +ServiceAccount name. +*/}} +{{- define "haproxy-unified-gateway.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "haproxy-unified-gateway.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Controller image. +*/}} +{{- define "haproxy-unified-gateway.image" -}} +{{- $tag := default .Chart.AppVersion .Values.controller.image.tag -}} +{{- printf "%s:%s" .Values.controller.image.repository $tag }} +{{- end }} + +{{/* +CRD Job ServiceAccount/RBAC name. +*/}} +{{- define "haproxy-unified-gateway.crdjob.saName" -}} +{{- printf "%s-crdjob" (include "haproxy-unified-gateway.fullname" .) | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Metrics Service name. +*/}} +{{- define "haproxy-unified-gateway.metricsServiceName" -}} +{{- printf "%s-metrics" (include "haproxy-unified-gateway.fullname" .) | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +HugConf cleanup Job name. +*/}} +{{- define "haproxy-unified-gateway.hugconfCleanup.fullname" -}} +{{- printf "%s-hugconf-cleanup" (include "haproxy-unified-gateway.fullname" .) | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +CRD Job labels. +*/}} +{{- define "haproxy-unified-gateway.crdjobLabels" -}} +{{ include "haproxy-unified-gateway.helmChartLabels" . }} +app.kubernetes.io/name: {{ include "haproxy-unified-gateway.name" . }}-crdjob +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +CRD Job fullname (includes revision for uniqueness). +*/}} +{{- define "haproxy-unified-gateway.crdjob.fullname" -}} +{{- printf "%s-crdjob-%d" (include "haproxy-unified-gateway.fullname" .) .Release.Revision | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Gateway API Job labels. +*/}} +{{- define "haproxy-unified-gateway.gwapijobLabels" -}} +{{ include "haproxy-unified-gateway.helmChartLabels" . }} +app.kubernetes.io/name: {{ include "haproxy-unified-gateway.name" . }}-gwapijob +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Gateway API Job fullname (includes revision for uniqueness). +*/}} +{{- define "haproxy-unified-gateway.gwapijob.fullname" -}} +{{- printf "%s-gwapijob-%d" (include "haproxy-unified-gateway.fullname" .) .Release.Revision | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +ServiceMonitor name. +*/}} +{{- define "haproxy-unified-gateway.serviceMonitorName" -}} +{{- default (include "haproxy-unified-gateway.fullname" .) .Values.controller.serviceMonitor.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +PodMonitor name. +*/}} +{{- define "haproxy-unified-gateway.podMonitorName" -}} +{{- default (include "haproxy-unified-gateway.fullname" .) .Values.controller.podMonitor.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +HugConf CRD reference path (namespace/name). +*/}} +{{- define "haproxy-unified-gateway.hugconfCrd" -}} +{{- if .Values.controller.hugconfCrd }} +{{- .Values.controller.hugconfCrd }} +{{- else }} +{{- printf "%s/%s" (include "haproxy-unified-gateway.namespace" .) .Values.hugconf.name }} +{{- end }} +{{- end }} diff --git a/haproxy-unified-gateway/templates/clusterrole.yaml b/haproxy-unified-gateway/templates/clusterrole.yaml new file mode 100644 index 0000000..916c42a --- /dev/null +++ b/haproxy-unified-gateway/templates/clusterrole.yaml @@ -0,0 +1,138 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.rbac.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "haproxy-unified-gateway.fullname" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} +rules: + - apiGroups: + - "apiextensions.k8s.io" + resources: + - customresourcedefinitions + verbs: + - get + - list + - watch + - update + - apiGroups: + - "" + resources: + - configmaps + - endpoints + - nodes + - pods + - services + - namespaces + - events + - serviceaccounts + verbs: + - get + - list + - watch + - create + - patch + - update + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch + - create + - patch + - update + - apiGroups: + - "discovery.k8s.io" + resources: + - endpointslices + verbs: + - get + - list + - watch + - apiGroups: + - "apps" + resources: + - replicasets + - deployments + - daemonsets + verbs: + - get + - list + - apiGroups: + - gateway.networking.k8s.io + resources: + - gatewayclasses + - gateways + - httproutes + - referencegrants + - grpcroutes + - tlsroutes + verbs: + - list + - watch + - apiGroups: + - gateway.networking.k8s.io + resources: + - gatewayclasses/status + - gateways/status + - httproutes/status + - referencegrants/status + - grpcroutes/status + - tlsroutes/status + verbs: + - get + - update + - patch + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - get + - update + - apiGroups: + - gate.v3.haproxy.org + resources: + - huggates + - hugconfs + - backends + - globals + - defaults + verbs: + - get + - list + - watch + # Required for kube-rbac metrics auth (TokenReview + SubjectAccessReview) + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +{{- end }} diff --git a/haproxy-unified-gateway/templates/clusterrolebinding.yaml b/haproxy-unified-gateway/templates/clusterrolebinding.yaml new file mode 100644 index 0000000..c71a4f6 --- /dev/null +++ b/haproxy-unified-gateway/templates/clusterrolebinding.yaml @@ -0,0 +1,32 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.rbac.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "haproxy-unified-gateway.fullname" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "haproxy-unified-gateway.fullname" . }} +subjects: + - kind: ServiceAccount + name: {{ include "haproxy-unified-gateway.serviceAccountName" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} +{{- end }} diff --git a/haproxy-unified-gateway/templates/controller-crdjob-rbac.yaml b/haproxy-unified-gateway/templates/controller-crdjob-rbac.yaml new file mode 100644 index 0000000..850ce53 --- /dev/null +++ b/haproxy-unified-gateway/templates/controller-crdjob-rbac.yaml @@ -0,0 +1,72 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if or .Values.crdjob.enabled .Values.gwapijob.enabled }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "haproxy-unified-gateway.crdjob.saName" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} + labels: + {{- include "haproxy-unified-gateway.crdjobLabels" . | nindent 4 }} + annotations: + "helm.sh/hook": post-install,pre-upgrade + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "haproxy-unified-gateway.crdjob.saName" . }} + labels: + {{- include "haproxy-unified-gateway.crdjobLabels" . | nindent 4 }} + annotations: + "helm.sh/hook": post-install,pre-upgrade + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +rules: + - apiGroups: + - "apiextensions.k8s.io" + resources: + - customresourcedefinitions + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "haproxy-unified-gateway.crdjob.saName" . }} + labels: + {{- include "haproxy-unified-gateway.crdjobLabels" . | nindent 4 }} + annotations: + "helm.sh/hook": post-install,pre-upgrade + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "haproxy-unified-gateway.crdjob.saName" . }} +subjects: + - kind: ServiceAccount + name: {{ include "haproxy-unified-gateway.crdjob.saName" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} +{{- end }} diff --git a/haproxy-unified-gateway/templates/controller-crdjob.yaml b/haproxy-unified-gateway/templates/controller-crdjob.yaml new file mode 100644 index 0000000..6011c40 --- /dev/null +++ b/haproxy-unified-gateway/templates/controller-crdjob.yaml @@ -0,0 +1,96 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.crdjob.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "haproxy-unified-gateway.crdjob.fullname" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} + labels: + {{- include "haproxy-unified-gateway.crdjobLabels" . | nindent 4 }} + annotations: + "helm.sh/hook": post-install,pre-upgrade + "helm.sh/hook-weight": "0" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + "argocd.argoproj.io/hook": PostSync + {{- with .Values.crdjob.podAnnotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.crdjob.ttlSecondsAfterFinished }} + ttlSecondsAfterFinished: {{ . }} + {{- end }} + template: + metadata: + labels: + {{- include "haproxy-unified-gateway.crdjobLabels" . | nindent 8 }} + {{- with .Values.crdjob.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ include "haproxy-unified-gateway.crdjob.saName" . }} + restartPolicy: Never + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + seccompProfile: + type: RuntimeDefault + {{- with .Values.controller.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: crdjob + {{- if .Values.crdjob.image.repository }} + image: {{ printf "%s:%s" .Values.crdjob.image.repository (default .Chart.AppVersion .Values.crdjob.image.tag) }} + {{- else }} + image: {{ include "haproxy-unified-gateway.image" . }} + {{- end }} + imagePullPolicy: {{ .Values.controller.image.pullPolicy }} + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + command: + - /usr/local/sbin/hug + - --job-check-crd + {{- with .Values.crdjob.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.crdjob.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.crdjob.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.crdjob.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + backoffLimit: 0 +{{- end }} diff --git a/haproxy-unified-gateway/templates/controller-daemonset.yaml b/haproxy-unified-gateway/templates/controller-daemonset.yaml new file mode 100644 index 0000000..756d2ed --- /dev/null +++ b/haproxy-unified-gateway/templates/controller-daemonset.yaml @@ -0,0 +1,185 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if eq .Values.controller.kind "DaemonSet" }} +{{- $useHostNetwork := .Values.controller.daemonset.useHostNetwork -}} +{{- $useHostPort := .Values.controller.daemonset.useHostPort -}} +{{- $hostPorts := .Values.controller.daemonset.hostPorts -}} +{{- $hostIP := .Values.controller.daemonset.hostIP -}} +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: {{ include "haproxy-unified-gateway.fullname" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} + {{- with .Values.controller.extraLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + selector: + matchLabels: + {{- include "haproxy-unified-gateway.selectorLabels" . | nindent 6 }} + {{- with .Values.controller.strategy }} + updateStrategy: + {{- toYaml . | nindent 4 }} + {{- end }} + template: + metadata: + labels: + {{- include "haproxy-unified-gateway.selectorLabels" . | nindent 8 }} + {{- with .Values.controller.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ include "haproxy-unified-gateway.serviceAccountName" . }} + {{- with .Values.controller.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- if .Values.controller.unprivileged }} + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 +{{- end }} + {{- with .Values.controller.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.priorityClassName }} + priorityClassName: {{ . }} + {{- end }} + {{- if $useHostNetwork }} + hostNetwork: true + {{- end }} + {{- with .Values.controller.dnsPolicy }} + dnsPolicy: {{ . }} + {{- end }} + {{- with .Values.controller.dnsConfig }} + dnsConfig: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.initContainers }} + initContainers: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ include "haproxy-unified-gateway.name" . }} + image: {{ include "haproxy-unified-gateway.image" . }} + imagePullPolicy: {{ .Values.controller.image.pullPolicy }} + args: + {{- if or .Values.hugconf.create .Values.controller.hugconfCrd }} + - --hugconf-crd={{ include "haproxy-unified-gateway.hugconfCrd" . }} + {{- end }} + {{- with .Values.controller.metricsAuth }} + - --metrics-auth={{ . }} + {{- end }} + {{- range .Values.controller.extraArgs }} + - {{ . }} + {{- end }} + ports: + {{- range $key, $value := .Values.controller.containerPort }} + - name: {{ $key }} + containerPort: {{ $value }} + protocol: TCP + {{- if and $useHostPort (index $hostPorts $key) }} + hostPort: {{ index $hostPorts $key }} + {{- end }} + {{- if $hostIP }} + hostIP: {{ $hostIP }} + {{- end }} + {{- end }} + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + {{- with .Values.controller.extraEnvs }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.controller.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.controller.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.controller.startupProbe }} + startupProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.controller.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.controller.unprivileged }} + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: {{ .Values.controller.allowPrivilegeEscalation }} + capabilities: + drop: + - ALL + add: + - NET_BIND_SERVICE + {{- with .Values.controller.seccompProfile }} + seccompProfile: + {{- toYaml . | nindent 14 }} + {{- end }} + {{- end }} + {{- with .Values.controller.extraVolumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.controller.extraContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.extraVolumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/haproxy-unified-gateway/templates/controller-deployment.yaml b/haproxy-unified-gateway/templates/controller-deployment.yaml new file mode 100644 index 0000000..3ae6cd2 --- /dev/null +++ b/haproxy-unified-gateway/templates/controller-deployment.yaml @@ -0,0 +1,175 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if eq .Values.controller.kind "Deployment" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "haproxy-unified-gateway.fullname" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} + {{- with .Values.controller.extraLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if not .Values.controller.autoscaling.enabled }} + replicas: {{ .Values.controller.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "haproxy-unified-gateway.selectorLabels" . | nindent 6 }} + {{- with .Values.controller.strategy }} + strategy: + {{- toYaml . | nindent 4 }} + {{- end }} + template: + metadata: + labels: + {{- include "haproxy-unified-gateway.selectorLabels" . | nindent 8 }} + {{- with .Values.controller.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ include "haproxy-unified-gateway.serviceAccountName" . }} + {{- with .Values.controller.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- if .Values.controller.unprivileged }} + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 +{{- end }} + {{- with .Values.controller.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.priorityClassName }} + priorityClassName: {{ . }} + {{- end }} + {{- with .Values.controller.dnsPolicy }} + dnsPolicy: {{ . }} + {{- end }} + {{- with .Values.controller.dnsConfig }} + dnsConfig: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.initContainers }} + initContainers: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ include "haproxy-unified-gateway.name" . }} + image: {{ include "haproxy-unified-gateway.image" . }} + imagePullPolicy: {{ .Values.controller.image.pullPolicy }} + args: + {{- if or .Values.hugconf.create .Values.controller.hugconfCrd }} + - --hugconf-crd={{ include "haproxy-unified-gateway.hugconfCrd" . }} + {{- end }} + {{- with .Values.controller.metricsAuth }} + - --metrics-auth={{ . }} + {{- end }} + {{- range .Values.controller.extraArgs }} + - {{ . }} + {{- end }} + ports: + {{- range $key, $value := .Values.controller.containerPort }} + - name: {{ $key }} + containerPort: {{ $value }} + protocol: TCP + {{- end }} + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + {{- with .Values.controller.extraEnvs }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.controller.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.controller.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.controller.startupProbe }} + startupProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.controller.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if .Values.controller.unprivileged }} + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: {{ .Values.controller.allowPrivilegeEscalation }} + capabilities: + drop: + - ALL + add: + - NET_BIND_SERVICE + {{- with .Values.controller.seccompProfile }} + seccompProfile: + {{- toYaml . | nindent 14 }} + {{- end }} + {{- end }} + {{- with .Values.controller.extraVolumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.controller.extraContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.extraVolumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/haproxy-unified-gateway/templates/controller-gwapijob.yaml b/haproxy-unified-gateway/templates/controller-gwapijob.yaml new file mode 100644 index 0000000..9dafad2 --- /dev/null +++ b/haproxy-unified-gateway/templates/controller-gwapijob.yaml @@ -0,0 +1,96 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.gwapijob.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "haproxy-unified-gateway.gwapijob.fullname" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} + labels: + {{- include "haproxy-unified-gateway.gwapijobLabels" . | nindent 4 }} + annotations: + "helm.sh/hook": post-install,pre-upgrade + "helm.sh/hook-weight": "0" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded + "argocd.argoproj.io/hook": PostSync + {{- with .Values.gwapijob.podAnnotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.gwapijob.ttlSecondsAfterFinished }} + ttlSecondsAfterFinished: {{ . }} + {{- end }} + template: + metadata: + labels: + {{- include "haproxy-unified-gateway.gwapijobLabels" . | nindent 8 }} + {{- with .Values.gwapijob.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ include "haproxy-unified-gateway.crdjob.saName" . }} + restartPolicy: Never + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + seccompProfile: + type: RuntimeDefault + {{- with .Values.controller.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: gwapijob + {{- if .Values.gwapijob.image.repository }} + image: {{ printf "%s:%s" .Values.gwapijob.image.repository (default .Chart.AppVersion .Values.gwapijob.image.tag) }} + {{- else }} + image: {{ include "haproxy-unified-gateway.image" . }} + {{- end }} + imagePullPolicy: {{ .Values.controller.image.pullPolicy }} + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + command: + - /usr/local/sbin/hug + - --job-gwapi={{ .Values.gwapijob.version }} + {{- with .Values.gwapijob.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.gwapijob.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.gwapijob.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.gwapijob.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + backoffLimit: 0 +{{- end }} diff --git a/haproxy-unified-gateway/templates/controller-hpa.yaml b/haproxy-unified-gateway/templates/controller-hpa.yaml new file mode 100644 index 0000000..b614ba7 --- /dev/null +++ b/haproxy-unified-gateway/templates/controller-hpa.yaml @@ -0,0 +1,53 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and .Values.controller.autoscaling.enabled (not .Values.controller.keda.enabled) }} +{{- if semverCompare ">=1.23-0" .Capabilities.KubeVersion.GitVersion }} +apiVersion: autoscaling/v2 +{{- else }} +apiVersion: autoscaling/v2beta2 +{{- end }} +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "haproxy-unified-gateway.fullname" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "haproxy-unified-gateway.fullname" . }} + minReplicas: {{ .Values.controller.autoscaling.minReplicas }} + maxReplicas: {{ .Values.controller.autoscaling.maxReplicas }} + metrics: + {{- if .Values.controller.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.controller.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.controller.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.controller.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/haproxy-unified-gateway/templates/controller-hugconf-cleanup.yaml b/haproxy-unified-gateway/templates/controller-hugconf-cleanup.yaml new file mode 100644 index 0000000..b7e50ae --- /dev/null +++ b/haproxy-unified-gateway/templates/controller-hugconf-cleanup.yaml @@ -0,0 +1,125 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.hugconf.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "haproxy-unified-gateway.hugconfCleanup.fullname" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-delete + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "haproxy-unified-gateway.hugconfCleanup.fullname" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-delete + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +rules: + - apiGroups: + - "gate.v3.haproxy.org" + resources: + - hugconfs + verbs: + - get + - delete +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "haproxy-unified-gateway.hugconfCleanup.fullname" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-delete + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "haproxy-unified-gateway.hugconfCleanup.fullname" . }} +subjects: + - kind: ServiceAccount + name: {{ include "haproxy-unified-gateway.hugconfCleanup.fullname" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "haproxy-unified-gateway.hugconfCleanup.fullname" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-delete + "helm.sh/hook-weight": "0" + "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded +spec: + ttlSecondsAfterFinished: 60 + template: + metadata: + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 8 }} + spec: + serviceAccountName: {{ include "haproxy-unified-gateway.hugconfCleanup.fullname" . }} + restartPolicy: Never + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + seccompProfile: + type: RuntimeDefault + {{- with .Values.controller.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: hugconf-cleanup + image: {{ include "haproxy-unified-gateway.image" . }} + imagePullPolicy: {{ .Values.controller.image.pullPolicy }} + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + command: + - /bin/sh + - -c + - | + APISERVER="https://kubernetes.default.svc" + TOKEN="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" + CACERT="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + wget --header="Authorization: Bearer ${TOKEN}" \ + --ca-certificate="${CACERT}" \ + --method=DELETE \ + -q -O /dev/null \ + "${APISERVER}/apis/gate.v3.haproxy.org/v3/namespaces/{{ include "haproxy-unified-gateway.namespace" . }}/hugconfs/{{ .Values.hugconf.name }}" 2>/dev/null || true + backoffLimit: 1 +{{- end }} diff --git a/haproxy-unified-gateway/templates/controller-hugconf.yaml b/haproxy-unified-gateway/templates/controller-hugconf.yaml new file mode 100644 index 0000000..7603ad0 --- /dev/null +++ b/haproxy-unified-gateway/templates/controller-hugconf.yaml @@ -0,0 +1,45 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.hugconf.create }} +apiVersion: gate.v3.haproxy.org/v3 +kind: HugConf +metadata: + name: {{ .Values.hugconf.name }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": post-install,pre-upgrade + "helm.sh/hook-weight": "5" + "helm.sh/hook-delete-policy": before-hook-creation + "argocd.argoproj.io/hook": PostSync +spec: + logging: + defaultLevel: {{ .Values.hugconf.logging.defaultLevel | quote }} + {{- with .Values.hugconf.logging.categoryLevelList }} + categoryLevelList: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- with .Values.hugconf.globalRef }} + globalRef: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.hugconf.defaultsRef }} + defaultsRef: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/haproxy-unified-gateway/templates/controller-keda.yaml b/haproxy-unified-gateway/templates/controller-keda.yaml new file mode 100644 index 0000000..5a33624 --- /dev/null +++ b/haproxy-unified-gateway/templates/controller-keda.yaml @@ -0,0 +1,57 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and (eq .Values.controller.kind "Deployment") .Values.controller.keda.enabled }} +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{ include "haproxy-unified-gateway.fullname" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} + {{- if .Values.controller.keda.scaledObject.annotations }} + annotations: {{ toYaml .Values.controller.keda.scaledObject.annotations | nindent 4 }} + {{- end }} +spec: + scaleTargetRef: + name: {{ include "haproxy-unified-gateway.fullname" . }} + pollingInterval: {{ .Values.controller.keda.pollingInterval }} + cooldownPeriod: {{ .Values.controller.keda.cooldownPeriod }} + minReplicaCount: {{ .Values.controller.keda.minReplicas }} + maxReplicaCount: {{ .Values.controller.keda.maxReplicas }} + triggers: +{{- with .Values.controller.keda.triggers }} +{{ toYaml . | indent 2 }} +{{ end }} +{{- with .Values.controller.keda.fallback }} + fallback: +{{ toYaml . | indent 4 }} +{{- end }} + advanced: + restoreToOriginalReplicaCount: {{ .Values.controller.keda.restoreToOriginalReplicaCount }} +{{- if .Values.controller.keda.horizontalPodAutoscalerConfig }} + horizontalPodAutoscalerConfig: +{{- if .Values.controller.keda.horizontalPodAutoscalerConfig.name }} + name: {{ .Values.controller.keda.horizontalPodAutoscalerConfig.name }} +{{- end }} +{{- if .Values.controller.keda.horizontalPodAutoscalerConfig.behavior }} + behavior: +{{ with .Values.controller.keda.horizontalPodAutoscalerConfig.behavior -}} +{{ toYaml . | indent 8 }} +{{ end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/haproxy-unified-gateway/templates/controller-poddisruptionbudget.yaml b/haproxy-unified-gateway/templates/controller-poddisruptionbudget.yaml new file mode 100644 index 0000000..6644b2e --- /dev/null +++ b/haproxy-unified-gateway/templates/controller-poddisruptionbudget.yaml @@ -0,0 +1,35 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.controller.podDisruptionBudget.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "haproxy-unified-gateway.fullname" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "haproxy-unified-gateway.selectorLabels" . | nindent 6 }} + {{- if .Values.controller.podDisruptionBudget.minAvailable }} + minAvailable: {{ .Values.controller.podDisruptionBudget.minAvailable }} + {{- end }} + {{- if .Values.controller.podDisruptionBudget.maxUnavailable }} + maxUnavailable: {{ .Values.controller.podDisruptionBudget.maxUnavailable }} + {{- end }} +{{- end }} diff --git a/haproxy-unified-gateway/templates/controller-podmonitor.yaml b/haproxy-unified-gateway/templates/controller-podmonitor.yaml new file mode 100644 index 0000000..1d6d06e --- /dev/null +++ b/haproxy-unified-gateway/templates/controller-podmonitor.yaml @@ -0,0 +1,37 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and (.Capabilities.APIVersions.Has "monitoring.coreos.com/v1") .Values.controller.podMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: {{ include "haproxy-unified-gateway.podMonitorName" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} + {{- with .Values.controller.podMonitor.extraLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + podMetricsEndpoints: + {{- toYaml .Values.controller.podMonitor.endpoints | nindent 4 }} + namespaceSelector: + matchNames: + - {{ include "haproxy-unified-gateway.namespace" . }} + selector: + matchLabels: + {{- include "haproxy-unified-gateway.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/haproxy-unified-gateway/templates/controller-podsecuritypolicy.yaml b/haproxy-unified-gateway/templates/controller-podsecuritypolicy.yaml new file mode 100644 index 0000000..dec3ba2 --- /dev/null +++ b/haproxy-unified-gateway/templates/controller-podsecuritypolicy.yaml @@ -0,0 +1,81 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if (semverCompare "<1.25.0-0" .Capabilities.KubeVersion.Version) }} +{{- if and .Values.rbac.create .Values.podSecurityPolicy.enabled }} +{{- $useHostNetwork := false }} +{{- $useHostPort := false }} +{{- if eq .Values.controller.kind "DaemonSet" }} +{{- $useHostNetwork = .Values.controller.daemonset.useHostNetwork }} +{{- $useHostPort = .Values.controller.daemonset.useHostPort }} +{{- end }} +{{- if or (.Capabilities.APIVersions.Has "policy/v1/PodSecurityPolicy") (semverCompare ">=1.21.0-0" .Capabilities.KubeVersion.Version) }} +apiVersion: policy/v1 +{{- else }} +apiVersion: policy/v1beta1 +{{- end }} +kind: PodSecurityPolicy +metadata: +{{- if .Values.podSecurityPolicy.annotations }} + annotations: +{{ toYaml .Values.podSecurityPolicy.annotations | indent 4 }} +{{- end }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} + name: {{ include "haproxy-unified-gateway.fullname" . }} + annotations: + seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'docker/default,runtime/default' + apparmor.security.beta.kubernetes.io/allowedProfileNames: 'runtime/default' + seccomp.security.alpha.kubernetes.io/defaultProfileName: 'runtime/default' + apparmor.security.beta.kubernetes.io/defaultProfileName: 'runtime/default' +spec: + allowPrivilegeEscalation: {{ .Values.controller.allowPrivilegeEscalation }} + allowedCapabilities: + - NET_BIND_SERVICE + defaultAllowPrivilegeEscalation: false + fsGroup: + rule: MustRunAs + ranges: + - max: 65535 + min: 1 + hostIPC: false +{{- if $useHostNetwork }} + hostNetwork: true +{{- end }} +{{- if or $useHostPort $useHostNetwork }} + hostPorts: +{{- range $key, $value := .Values.controller.containerPort }} + - min: {{ $value }} + max: {{ $value }} +{{- end }} +{{- end }} + hostPID: false + privileged: false + runAsUser: + rule: RunAsAny + seLinux: + rule: RunAsAny + supplementalGroups: + rule: MustRunAs + ranges: + - max: 65535 + min: 1 + volumes: + - configMap + - downwardAPI + - secret +{{- end }} +{{- end }} diff --git a/haproxy-unified-gateway/templates/controller-role.yaml b/haproxy-unified-gateway/templates/controller-role.yaml new file mode 100644 index 0000000..388b4a9 --- /dev/null +++ b/haproxy-unified-gateway/templates/controller-role.yaml @@ -0,0 +1,34 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and .Values.rbac.create .Values.podSecurityPolicy.enabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "haproxy-unified-gateway.fullname" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} +rules: +- apiGroups: + - "policy" + resources: + - podsecuritypolicies + verbs: + - use + resourceNames: + - {{ include "haproxy-unified-gateway.fullname" . }} +{{- end -}} diff --git a/haproxy-unified-gateway/templates/controller-rolebinding.yaml b/haproxy-unified-gateway/templates/controller-rolebinding.yaml new file mode 100644 index 0000000..e7ef98f --- /dev/null +++ b/haproxy-unified-gateway/templates/controller-rolebinding.yaml @@ -0,0 +1,33 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and .Values.rbac.create .Values.podSecurityPolicy.enabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "haproxy-unified-gateway.fullname" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "haproxy-unified-gateway.fullname" . }} +subjects: +- kind: ServiceAccount + name: {{ include "haproxy-unified-gateway.serviceAccountName" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} +{{- end -}} diff --git a/haproxy-unified-gateway/templates/controller-service-metrics.yaml b/haproxy-unified-gateway/templates/controller-service-metrics.yaml new file mode 100644 index 0000000..4c22386 --- /dev/null +++ b/haproxy-unified-gateway/templates/controller-service-metrics.yaml @@ -0,0 +1,45 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and .Values.controller.serviceMonitor.enabled .Values.controller.service.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "haproxy-unified-gateway.metricsServiceName" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} + {{- with .Values.controller.service.metrics.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.controller.service.metrics.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.controller.service.metrics.type }} + selector: + {{- include "haproxy-unified-gateway.selectorLabels" . | nindent 4 }} + ports: + - name: stat + port: {{ .Values.controller.service.stat.port }} + targetPort: {{ .Values.controller.service.stat.targetPort }} + protocol: TCP + - name: metrics + port: {{ index .Values.controller.service "controller-metrics" "port" }} + targetPort: {{ index .Values.controller.service "controller-metrics" "targetPort" }} + protocol: TCP +{{- end }} diff --git a/haproxy-unified-gateway/templates/controller-service.yaml b/haproxy-unified-gateway/templates/controller-service.yaml new file mode 100644 index 0000000..3907b68 --- /dev/null +++ b/haproxy-unified-gateway/templates/controller-service.yaml @@ -0,0 +1,64 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.controller.service.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "haproxy-unified-gateway.fullname" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} + {{- with .Values.controller.service.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.controller.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.controller.service.type }} + {{- with .Values.controller.service.externalTrafficPolicy }} + externalTrafficPolicy: {{ . }} + {{- end }} + selector: + {{- include "haproxy-unified-gateway.selectorLabels" . | nindent 4 }} + ports: + - name: http + port: {{ .Values.controller.service.http.port }} + targetPort: {{ .Values.controller.service.http.targetPort }} + protocol: TCP + {{- if and (eq .Values.controller.service.type "NodePort") .Values.controller.service.http.nodePort }} + nodePort: {{ .Values.controller.service.http.nodePort }} + {{- end }} + - name: https + port: {{ .Values.controller.service.https.port }} + targetPort: {{ .Values.controller.service.https.targetPort }} + protocol: TCP + {{- if and (eq .Values.controller.service.type "NodePort") .Values.controller.service.https.nodePort }} + nodePort: {{ .Values.controller.service.https.nodePort }} + {{- end }} + - name: stat + port: {{ .Values.controller.service.stat.port }} + targetPort: {{ .Values.controller.service.stat.targetPort }} + protocol: TCP + {{- if and (eq .Values.controller.service.type "NodePort") .Values.controller.service.stat.nodePort }} + nodePort: {{ .Values.controller.service.stat.nodePort }} + {{- end }} + {{- with .Values.controller.service.extraPorts }} + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/haproxy-unified-gateway/templates/controller-serviceaccount.yaml b/haproxy-unified-gateway/templates/controller-serviceaccount.yaml new file mode 100644 index 0000000..328401e --- /dev/null +++ b/haproxy-unified-gateway/templates/controller-serviceaccount.yaml @@ -0,0 +1,29 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "haproxy-unified-gateway.serviceAccountName" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/haproxy-unified-gateway/templates/controller-servicemonitor.yaml b/haproxy-unified-gateway/templates/controller-servicemonitor.yaml new file mode 100644 index 0000000..6c32f8c --- /dev/null +++ b/haproxy-unified-gateway/templates/controller-servicemonitor.yaml @@ -0,0 +1,37 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and (.Capabilities.APIVersions.Has "monitoring.coreos.com/v1") .Values.controller.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "haproxy-unified-gateway.serviceMonitorName" . }} + namespace: {{ include "haproxy-unified-gateway.namespace" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} + {{- with .Values.controller.serviceMonitor.extraLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + endpoints: + {{- toYaml .Values.controller.serviceMonitor.endpoints | nindent 4 }} + namespaceSelector: + matchNames: + - {{ include "haproxy-unified-gateway.namespace" . }} + selector: + matchLabels: + {{- include "haproxy-unified-gateway.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/haproxy-unified-gateway/templates/namespace.yaml b/haproxy-unified-gateway/templates/namespace.yaml new file mode 100644 index 0000000..6630972 --- /dev/null +++ b/haproxy-unified-gateway/templates/namespace.yaml @@ -0,0 +1,27 @@ +{{/* +Copyright 2026 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.namespace.create }} +apiVersion: v1 +kind: Namespace +metadata: + name: {{ include "haproxy-unified-gateway.namespace" . }} + labels: + {{- include "haproxy-unified-gateway.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-install + "helm.sh/hook-weight": "-1" +{{- end }} diff --git a/haproxy-unified-gateway/values.yaml b/haproxy-unified-gateway/values.yaml new file mode 100644 index 0000000..f5b5bbc --- /dev/null +++ b/haproxy-unified-gateway/values.yaml @@ -0,0 +1,389 @@ +# Copyright 2026 HAProxy Technologies LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## HAProxy Unified Gateway Helm Chart - values.yaml + +# -- PodSecurityPolicy configuration (deprecated in K8s 1.21, removed in 1.25) +podSecurityPolicy: + ## Specify pod annotations + ## ref: https://kubernetes.io/docs/concepts/policy/pod-security-policy/#apparmor + ## ref: https://kubernetes.io/docs/concepts/policy/pod-security-policy/#seccomp + ## ref: https://kubernetes.io/docs/concepts/policy/pod-security-policy/#sysctl + annotations: {} + # apparmor.security.beta.kubernetes.io/allowedProfileNames: runtime/default + # apparmor.security.beta.kubernetes.io/defaultProfileName: runtime/default + # seccomp.security.alpha.kubernetes.io/allowedProfileNames: runtime/default + # seccomp.security.alpha.kubernetes.io/defaultProfileName: runtime/default + enabled: false + +# -- Create RBAC resources +rbac: + create: true + +# -- Create a namespace (uses Helm pre-install hook) +namespace: + create: false + +# -- ServiceAccount configuration +serviceAccount: + # -- Create a ServiceAccount + create: true + # -- ServiceAccount name (generated if not set) + name: "" + # -- Annotations to add to the ServiceAccount + annotations: {} + +# -- Controller configuration +controller: + # -- Controller name + name: controller + + # -- Container image configuration + image: + repository: docker.io/haproxytech/haproxy-unified-gateway + tag: "" # defaults to appVersion + pullPolicy: IfNotPresent + + # -- Image pull secrets + imagePullSecrets: [] + + # -- Deployment or DaemonSet pod mode + # ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/ + # ref: https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/ + kind: Deployment # can be 'Deployment' or 'DaemonSet' + + # -- Number of replicas (only for Deployment mode) + replicaCount: 1 + + # -- HugConf CRD reference (namespace/name) + # If empty, defaults to "/hugconf" + hugconfCrd: "" + + # -- Metrics authentication mode for the controller metrics endpoint (port 31060) + # Supported values: "none", "kube-rbac", "basic" + # - none: HTTP, no authentication + # - kube-rbac: HTTPS with Kubernetes TokenReview/SubjectAccessReview + # - basic: HTTPS with HTTP Basic Authentication (set credentials via extraArgs) + metricsAuth: kube-rbac + + # -- Extra arguments to pass to the controller + extraArgs: [] + + # -- Container ports + containerPort: + http: 31080 + https: 31443 + stat: 31024 + metrics: 31060 + + # -- Resource requests and limits + resources: + limits: + memory: 2560Mi + requests: + memory: 2048Mi + + ## Running container without root privileges + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ + unprivileged: true + + ## Privilege escalation + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ + allowPrivilegeEscalation: false + + ## Restricts container syscalls + ## ref: https://kubernetes.io/docs/tutorials/security/seccomp/ + ## Supported types: RuntimeDefault, Localhost, Unconfined + ## Set to empty ({}) to disable seccomp profile + seccompProfile: + type: RuntimeDefault + # localhostProfile: my-profiles/profile.json # only for type: Localhost + + # -- Pod-level security context + podSecurityContext: {} + + # -- Liveness probe configuration + livenessProbe: {} + + # -- Readiness probe configuration + readinessProbe: {} + + # -- Startup probe configuration + startupProbe: {} + + # -- Node selector for pod scheduling + nodeSelector: {} + + # -- Tolerations for pod scheduling + tolerations: [] + + # -- Affinity rules for pod scheduling + affinity: {} + + # -- Topology spread constraints + topologySpreadConstraints: [] + + # -- Extra environment variables + extraEnvs: [] + + # -- Extra volume mounts + extraVolumeMounts: [] + + # -- Extra volumes + extraVolumes: [] + + # -- Extra init containers + initContainers: [] + + # -- Extra sidecar containers + extraContainers: [] + + # -- Pod annotations + podAnnotations: {} + + # -- Pod labels + podLabels: {} + + # -- Extra labels for the Deployment + extraLabels: {} + + # -- Update strategy + strategy: + type: RollingUpdate + + # -- Priority class name + priorityClassName: "" + + # -- DNS policy + dnsPolicy: "" + + # -- DNS config + dnsConfig: {} + + # -- DaemonSet-specific configuration (only used when kind is 'DaemonSet') + daemonset: + useHostNetwork: false # also modify dnsPolicy accordingly + useHostPort: false + hostIP: null + hostPorts: + http: 80 + https: 443 + stat: 1024 + metrics: 31060 + + # -- Service configuration + service: + # -- Enable the Service + enabled: true + # -- Service type + type: NodePort + # -- Service annotations + annotations: {} + # -- Service labels + labels: {} + # -- External traffic policy + externalTrafficPolicy: "" + # -- HTTP port configuration + http: + port: 31080 + targetPort: 31080 + nodePort: 31080 + # -- HTTPS port configuration + https: + port: 31443 + targetPort: 31443 + nodePort: 31443 + # -- Stats port configuration + stat: + port: 31024 + targetPort: 31024 + nodePort: "" + # -- Extra ports to expose + extraPorts: [] + # -- Controller metrics port configuration (hug_* prometheus metrics) + controller-metrics: + port: 31060 + targetPort: 31060 + # -- Metrics service configuration (created when serviceMonitor is enabled) + metrics: + type: ClusterIP + annotations: {} + labels: {} + + # -- ServiceMonitor configuration (requires Prometheus Operator) + ## ref: https://github.com/prometheus-operator/prometheus-operator/blob/master/Documentation/user-guides/getting-started.md + ## Note: requires Prometheus Operator to be able to work, for example: + ## helm install prometheus prometheus-community/kube-prometheus-stack \ + ## --set prometheus.prometheusSpec.podMonitorSelectorNilUsesHelmValues=false \ + ## --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false + serviceMonitor: + # -- Enable ServiceMonitor (should not be enabled together with podMonitor) + enabled: false + # -- Extra labels for ServiceMonitor target discovery + extraLabels: {} + # -- ServiceMonitor endpoints configuration + endpoints: + - port: stat + path: /metrics + scheme: http + interval: 30s + - port: metrics + path: /metrics + scheme: https + interval: 30s + tlsConfig: + insecureSkipVerify: true + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + + # -- PodMonitor configuration (requires Prometheus Operator) + ## ref: https://github.com/prometheus-operator/prometheus-operator/blob/master/Documentation/user-guides/getting-started.md + podMonitor: + # -- Enable PodMonitor (should not be enabled together with serviceMonitor) + enabled: false + # -- Extra labels for PodMonitor target discovery + extraLabels: {} + # -- PodMonitor endpoints configuration + endpoints: + - port: stat + path: /metrics + scheme: http + interval: 30s + - port: metrics + path: /metrics + scheme: https + interval: 30s + tlsConfig: + insecureSkipVerify: true + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + + # -- HorizontalPodAutoscaler configuration + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 10 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + + # -- KEDA ScaledObject configuration (mutually exclusive with autoscaling) + keda: + enabled: false + minReplicas: 2 + maxReplicas: 20 + pollingInterval: 30 + cooldownPeriod: 300 + restoreToOriginalReplicaCount: false + # fallback: + # failureThreshold: 3 + # replicas: 11 + scaledObject: + annotations: {} + horizontalPodAutoscalerConfig: {} + # name: "" + # behavior: + # scaleDown: + # stabilizationWindowSeconds: 300 + # policies: + # - type: Pods + # value: 1 + # periodSeconds: 300 + triggers: [] + # - type: prometheus + # metadata: + # serverAddress: http://:9090 + # metricName: haproxy_process_idle_time_percent + # threshold: '50' + # query: avg(100-avg_over_time(haproxy_process_idle_time_percent{job="haproxy-unified-gateway"}[2m])) + + # -- PodDisruptionBudget configuration + podDisruptionBudget: + enabled: false + # minAvailable: 1 + # maxUnavailable: 1 + +# -- HugConf custom resource configuration +hugconf: + # -- Create a HugConf resource + create: true + # -- HugConf name + name: hugconf + # -- Logging configuration + logging: + # -- Default log level + defaultLevel: Info + # -- Per-category log level overrides + categoryLevelList: + - category: "k8s" + level: "Error" + - category: "gate" + level: "Info" + - category: "status" + level: "Info" + - category: "batch" + level: "Error" + - category: "app" + level: "Info" + - category: "certs-storage" + level: "Info" + # -- Global configuration reference (group, kind, name, namespace) + globalRef: {} + # group: gate.v3.haproxy.org + # kind: Global + # name: global + # namespace: haproxy-unified-gateway + # -- Defaults configuration reference (group, kind, name, namespace) + defaultsRef: {} + # group: gate.v3.haproxy.org + # kind: Defaults + # name: haproxytech + # namespace: haproxy-unified-gateway + +# -- CRD Job configuration +crdjob: + # -- Enable the CRD installation Job (Helm hook) + enabled: true + # -- Additional pod annotations + podAnnotations: {} + # -- TTL for completed jobs (seconds) + ttlSecondsAfterFinished: 60 + # -- Node selector + nodeSelector: {} + # -- Tolerations + tolerations: [] + # -- Affinity + affinity: {} + # -- Resources for CRD job + resources: {} + # -- Image override (defaults to controller image) + image: {} + +# -- Gateway API Job configuration +gwapijob: + # -- Enable the Gateway API CRD installation Job (Helm hook) + enabled: true + # -- Gateway API version to install + version: "1.3.0" + # -- Additional pod annotations + podAnnotations: {} + # -- TTL for completed jobs (seconds) + ttlSecondsAfterFinished: 60 + # -- Node selector + nodeSelector: {} + # -- Tolerations + tolerations: [] + # -- Affinity + affinity: {} + # -- Resources for Gateway API job + resources: {} + # -- Image override (defaults to controller image) + image: {} diff --git a/haproxy/.helmignore b/haproxy/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/haproxy/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/haproxy/Chart.yaml b/haproxy/Chart.yaml new file mode 100644 index 0000000..c51bd9d --- /dev/null +++ b/haproxy/Chart.yaml @@ -0,0 +1,35 @@ +# Copyright 2020 HAProxy Technologies LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v2 +name: haproxy +description: A Helm chart for HAProxy on Kubernetes +type: application +version: 1.28.1 +appVersion: 3.3.6 +kubeVersion: ">=1.17.0-0" +keywords: + - haproxy +home: https://github.com/haproxytech/helm-charts/tree/main/haproxy +sources: + - http://www.haproxy.org/ +icon: https://raw.githubusercontent.com/haproxytech/helm-charts/main/haproxy/chart-icon.png +maintainers: + - name: Dinko Korunic + email: dkorunic@haproxy.com +engine: gotpl +annotations: + artifacthub.io/changes: | + - Update base image to HAProxy 3.3.6 + - Fix newline issue with YAML normalisation (#344) diff --git a/haproxy/README.md b/haproxy/README.md new file mode 100644 index 0000000..6e88c96 --- /dev/null +++ b/haproxy/README.md @@ -0,0 +1,378 @@ +# ![HAProxy](https://github.com/haproxytech/kubernetes-ingress/raw/master/assets/images/haproxy-weblogo-210x49.png "HAProxy") + +## HAProxy Helm Chart + +## Introduction + +This chart bootstraps an HAProxy load balancer as deployment/daemonset on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager. As oposed to [HAProxy Kubernetes Ingress Controller](https://github.com/haproxytech/helm-charts/tree/main/kubernetes-ingress) Chart, HAProxy is installed as a regular application and not as an Ingress Controller. + +### Prerequisites + +- Kubernetes 1.17+ (recommended 1.20+) +- Helm 3.6+ (recommended 3.7+) + +## Before you begin + +### Setup a Kubernetes Cluster + +The quickest way to setup a Kubernetes cluster is with [Azure Kubernetes Service](https://azure.microsoft.com/en-us/services/kubernetes-service/), [AWS Elastic Kubernetes Service](https://aws.amazon.com/eks/) or [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/) using their respective quick-start guides. + +For setting up Kubernetes on other cloud platforms or bare-metal servers refer to the Kubernetes [getting started guide](http://kubernetes.io/docs/getting-started-guides/). + +### Install Helm + +Get the latest [Helm release](https://github.com/helm/helm#install). + +### Add Helm chart repo + +Once you have Helm installed, add the haproxytech Chat Repository as follows: + +```console +helm repo add haproxytech https://haproxytech.github.io/helm-charts +helm repo update +``` + +Alternatively if you want to proceed with just OCI-based repository, skip this step and follow the installation with OCI. + +## Install the chart + +To install the chart with Helm v3 as _my-release_ deployment: + +```console +helm install my-release haproxytech/haproxy +``` + +**_NOTE_**: To install the chart with Helm v2 (legacy Helm) the syntax requires adding deployment name to `--name` parameter: + +```console +helm install haproxytech/haproxy \ + --name my-release +``` + +Alternatively also have OCI-based repository available for simplified access: + +```console +helm install oci://ghcr.io/haproxytech/helm-charts/haproxy --version 1.24.0 +``` + +### Installing with unique name + +To auto-generate resource names when installing, use the following: + +```console +helm install haproxytech/haproxy \ + --generate-name +``` + +### Installing from a private registry + +To install the chart using a private registry for HAProxy (for instance to use a HAProxy Enterprise image) into a separate namespace _prod_. + +**_NOTE_**: Helm v3 requires namespace to be precreated (eg. with `kubectl create namespace prod`) + +```console +helm install my-haproxy haproxytech/haproxy \ + --namespace prod \ + --set image.tag=latest \ + --set image.repository=myregistry.domain.com/imagename \ + --set imageCredentials.registry=myregistry.domain.com \ + --set imageCredentials.username=MYUSERNAME \ + --set imageCredentials.password=MYPASSWORD +``` + +Alternatively, use a pre-configured (existing) imagePullSecret in the same namespace: + +```console +helm install my-ingress haproxytech/haproxy \ + --namespace prod \ + --set image.tag=SOMETAG \ + --set existingImagePullSecret name-of-existing-image-pull-secret +``` + +**_NOTE_**: Enterprise images using S6 overlay need default CMD arguments disabled (more about YAML configuration file for Helm can be found in a separate paragraph below): + +```yaml +args: + enabled: false +``` + +### Installing as DaemonSet + +Default image mode is [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/), but it is possible to use [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) as well: + +```console +helm install my-haproxy2 haproxytech/haproxy \ + --set kind=DaemonSet +``` + +**_NOTE_**: With helm `--set` it is needed to put quotes and escape dots in the annotation key and commas in the value string. + +### Installing with Horizontal Pod Autoscaler + +[HPA](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/) automatically scales number of replicas in Deployment or Replication Controller and adjusts replica count. Therefore we want to unset default replicaCount by setting corresponding key value to null and enable autoscaling: + +```console +helm install my-haproxy3 haproxytech/haproxy \ + --set kind=Deployment \ + --set replicaCount=null \ + --set autoscaling.enabled=true \ + --set autoscaling.targetCPUUtilizationPercentage=80 +``` + +**_NOTE_**: Make sure to look into other tunable values for HPA documented in [values.yaml](values.yaml). + +### Installing with service annotations + +On some environments like EKS and GKE there might be a need to pass service annotations. Syntax can become a little tedious however: + +```console +helm install my-haproxy4 haproxytech/haproxy \ + --set kind=DaemonSet \ + --set service.type=LoadBalancer \ + --set service.annotations."service\.beta\.kubernetes\.io/aws-load-balancer-internal"="0.0.0.0/0" \ + --set service.annotations."service\.beta\.kubernetes\.io/aws-load-balancer-cross-zone-load-balancing-enabled"="true" +``` + +**_NOTE_**: With helm `--set` it is needed to put quotes and escape dots in the annotation key and commas in the value string. + +### Using values from YAML file + +As opposed to using many `--set` invocations, much simpler approach is to define value overrides in a separate YAML file and specify them when invoking Helm. +The `config` block can also support using helm templates to populate dynamic values, e.g. `{{ .Release.Name }}`. + +_mylb.yaml_: + +```yaml +kind: DaemonSet +config: | + global + log stdout format raw local0 + daemon + maxconn 1024 + defaults + log global + timeout client 60s + timeout connect 60s + timeout server {{ .Values.global.serverTimeout }} + frontend fe_main + bind :80 + default_backend be_main + backend be_main + server web1 10.0.0.1:8080 check + server web2 {{ .Release.Name }}-web:8080 check +service: + type: LoadBalancer + annotations: + service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true" + service.beta.kubernetes.io/aws-load-balancer-internal: 0.0.0.0/0 +``` + +And invoking Helm becomes (compare to the previous example): + +```console +helm install my-haproxy5 -f mylb.yml haproxytech/haproxy +``` + +### Using secrets in additional volume mounts + +In order to e.g. support SSL certificates, you can mount additional volumes from secrets: + +_mylb.yaml_: + +```yaml +service: + type: LoadBalancer +config: | + global + log stdout format raw local0 + daemon + maxconn 1024 + defaults + log global + timeout client 60s + timeout connect 60s + timeout server 60s + frontend fe_main + mode http + bind :80 + bind :443 ssl crt /usr/local/etc/ssl/tls.crt + http-request redirect scheme https code 301 unless { ssl_fc } + default_backend be_main + backend be_main + mode http + server web1 10.0.0.1:8080 check +mountedSecrets: + - volumeName: ssl-certificate + secretName: star-example-com + mountPath: /usr/local/etc/ssl +``` + +The above example assumes that there is a certificate in key `tls.crt` of a secret called `star-example-com`. + +### Using additional volumes and volumeMounts + +In order to load data from other sources (e.g. to preload something inside an init-container) you can mount additional volumes to the container: + +```yaml +extraVolumes: + - name: tls + emptyDir: {} + - name: tmp + emptyDir: + medium: Memory + +extraVolumeMounts: + - name: tls + mountPath: /etc/tls + - name: tmp + mountPath: /tmp +``` + +### Using additional environment variables + +In order to expose extra data (e.g. node and pod IP addresses) to haproxy, you can populate extra environment variables on the container: + +```yaml +extraEnvs: + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP +``` + +### Automatic configuration reloading + +In some cases, configuration changes are frequent and constantly restarting HAProxy is not optimal. For those cases the HAProxy hot-reload feature +can be used. + +In master-worker mode, sending a USR2 signal to the HAProxy process will trigger a configuration reload. + +```yaml +config: | + global + log stdout format raw local0 + master-worker + daemon + maxconn 1024 + defaults + log global + timeout client 60s + timeout connect 60s + timeout server 60s + frontend fe_main + mode http + bind :80 + http-request redirect scheme https code 301 unless { ssl_fc } + default_backend be_main + backend be_main + mode http + server web1 10.0.0.1:8080 check +``` + +Make sure you are not specifying subPath for any of your volumeMounts so that Kubernetes will automatically update the volumes created from +ConfigMaps. + +And finally, use some sidecar container which will be delivering the signal to the process. The shareProcessNamespace Pod property is required +for the sidecars to be able to access other containers' processes. + +```yaml +shareProcessNamespace: + enabled: true +sidecarContainers: + - name: reflex + image: acim/go-reflex:1.17.3 + command: ["reflex", "-d", "fancy"] + workingDir: /usr/local/etc/haproxy + args: + - -svr + - "..data" + - -- + - bash + - -c + - 'pkill -SIGUSR2 "haproxy|hapee-lb"' + volumeMounts: + - name: haproxy-config + mountPath: /usr/local/etc/haproxy + resources: + limits: + memory: 128Mi + requests: + cpu: 50m + memory: 64Mi +``` + +## Installing as non-root with binding to privileged ports + +To be able to bind to privileged ports such as tcp/80 and tcp/443 without root privileges (UID and GID are set to 1000 in the example, as HAProxy Docker image has UID/GID of 1000 reserved for HAProxy), there is a special workaround required as `NET_BIND_SERVICE` capability is [not propagated](https://github.com/kubernetes/kubernetes/issues/56374), so we need to use `initContainers` feature as well: + +```yaml +kind: DaemonSet +containerPorts: + http: 80 + https: 443 + stat: 1024 +daemonset: + useHostNetwork: true + useHostPort: true + hostPorts: + http: 80 + https: 443 + stat: 1024 +config: | + global + log stdout format raw local0 + maxconn 1024 + defaults + log global + timeout client 60s + timeout connect 60s + timeout server 60s + frontend fe_main + bind :80 + default_backend be_main + backend be_main + server web1 127.0.0.1:8080 check +securityContext: + enabled: true + runAsUser: 1000 + runAsGroup: 1000 +initContainers: + - name: sysctl + image: "busybox:musl" + command: + - /bin/sh + - -c + - sysctl -w net.ipv4.ip_unprivileged_port_start=0 + securityContext: + privileged: true +``` + +## Upgrading the chart + +To upgrade the _my-release_ deployment: + +```console +helm upgrade my-release haproxytech/haproxy +``` + +## Uninstalling the chart + +To uninstall/delete the _my-release_ deployment: + +```console +helm delete my-release +``` + +## Debugging + +It is possible to generate a set of YAML files for testing/debugging: + +```console +helm install my-release haproxytech/haproxy \ + --debug \ + --dry-run +``` + +## Contributing + +We welcome all contributions. Please refer to [guidelines](../CONTRIBUTING.md) on how to make a contribution. diff --git a/haproxy/chart-icon.png b/haproxy/chart-icon.png new file mode 100644 index 0000000..1c7acc4 Binary files /dev/null and b/haproxy/chart-icon.png differ diff --git a/haproxy/ci/daemonset-basic-values.yaml b/haproxy/ci/daemonset-basic-values.yaml new file mode 100644 index 0000000..ae9d34c --- /dev/null +++ b/haproxy/ci/daemonset-basic-values.yaml @@ -0,0 +1,2 @@ +kind: DaemonSet +replicaCount: 2 diff --git a/haproxy/ci/daemonset-hostnet-values.yaml b/haproxy/ci/daemonset-hostnet-values.yaml new file mode 100644 index 0000000..01e09a0 --- /dev/null +++ b/haproxy/ci/daemonset-hostnet-values.yaml @@ -0,0 +1,13 @@ +kind: DaemonSet +containerPorts: + http: 8080 + https: 8443 + stat: 8024 +daemonset: + useHostNetwork: true + useHostPort: true + hostPorts: + http: 8080 + https: 8443 + stat: 8024 +dnsPolicy: ClusterFirstWithHostNet diff --git a/haproxy/ci/daemonset-ingress-values.yaml b/haproxy/ci/daemonset-ingress-values.yaml new file mode 100644 index 0000000..a65660c --- /dev/null +++ b/haproxy/ci/daemonset-ingress-values.yaml @@ -0,0 +1,3 @@ +kind: DaemonSet +ingress: + enabled: true diff --git a/haproxy/ci/daemonset-ipfamily-values.yaml b/haproxy/ci/daemonset-ipfamily-values.yaml new file mode 100644 index 0000000..7623a91 --- /dev/null +++ b/haproxy/ci/daemonset-ipfamily-values.yaml @@ -0,0 +1,4 @@ +kind: DaemonSet +service: + ipFamilies: [IPv4] + ipFamilyPolicy: SingleStack diff --git a/haproxy/ci/daemonset-probes-values.yaml b/haproxy/ci/daemonset-probes-values.yaml new file mode 100644 index 0000000..cd8a333 --- /dev/null +++ b/haproxy/ci/daemonset-probes-values.yaml @@ -0,0 +1,26 @@ +kind: DaemonSet +replicaCount: 2 +livenessProbe: + failureThreshold: 3 + successThreshold: 1 + initialDelaySeconds: 0 + timeoutSeconds: 1 + tcpSocket: + port: 80 + periodSeconds: 10 +readinessProbe: + failureThreshold: 3 + successThreshold: 1 + initialDelaySeconds: 0 + timeoutSeconds: 1 + tcpSocket: + port: 80 + periodSeconds: 10 +startupProbe: + failureThreshold: 20 + successThreshold: 1 + initialDelaySeconds: 0 + timeoutSeconds: 1 + tcpSocket: + port: 80 + periodSeconds: 1 diff --git a/haproxy/ci/deployment-basic-values.yaml b/haproxy/ci/deployment-basic-values.yaml new file mode 100644 index 0000000..6c24b02 --- /dev/null +++ b/haproxy/ci/deployment-basic-values.yaml @@ -0,0 +1 @@ +kind: Deployment diff --git a/haproxy/ci/deployment-config-values.yaml b/haproxy/ci/deployment-config-values.yaml new file mode 100644 index 0000000..965b270 --- /dev/null +++ b/haproxy/ci/deployment-config-values.yaml @@ -0,0 +1,18 @@ +config: | + global + log stdout format raw local0 + daemon + maxconn 1024 + + defaults + log global + timeout client 60s + timeout connect 60s + timeout server 60s + + frontend fe_main + bind :80 + default_backend be_main + + backend be_main + server web1 10.0.0.1:8080 check diff --git a/haproxy/ci/deployment-hpa-values.yaml b/haproxy/ci/deployment-hpa-values.yaml new file mode 100644 index 0000000..0b03ff7 --- /dev/null +++ b/haproxy/ci/deployment-hpa-values.yaml @@ -0,0 +1,19 @@ +kind: Deployment +replicaCount: null +autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 80 + behavior: + scaleUp: + policies: + - type: Percent + value: 900 + periodSeconds: 60 + scaleDown: + stabilizationWindowSeconds: 600 + policies: + - type: Pods + value: 1 + periodSeconds: 600 diff --git a/haproxy/ci/deployment-ingress-values.yaml b/haproxy/ci/deployment-ingress-values.yaml new file mode 100644 index 0000000..4476e34 --- /dev/null +++ b/haproxy/ci/deployment-ingress-values.yaml @@ -0,0 +1,3 @@ +kind: Deployment +ingress: + enabled: true diff --git a/haproxy/ci/deployment-ipfamily-values.yaml b/haproxy/ci/deployment-ipfamily-values.yaml new file mode 100644 index 0000000..86bb2d1 --- /dev/null +++ b/haproxy/ci/deployment-ipfamily-values.yaml @@ -0,0 +1,4 @@ +kind: Deployment +service: + ipFamilies: [IPv4] + ipFamilyPolicy: SingleStack diff --git a/haproxy/ci/deployment-probes-values.yaml b/haproxy/ci/deployment-probes-values.yaml new file mode 100644 index 0000000..c47559f --- /dev/null +++ b/haproxy/ci/deployment-probes-values.yaml @@ -0,0 +1,25 @@ +kind: Deployment +livenessProbe: + failureThreshold: 3 + successThreshold: 1 + initialDelaySeconds: 0 + timeoutSeconds: 1 + tcpSocket: + port: 80 + periodSeconds: 10 +readinessProbe: + failureThreshold: 3 + successThreshold: 1 + initialDelaySeconds: 0 + timeoutSeconds: 1 + tcpSocket: + port: 80 + periodSeconds: 10 +startupProbe: + failureThreshold: 20 + successThreshold: 1 + initialDelaySeconds: 0 + timeoutSeconds: 1 + tcpSocket: + port: 80 + periodSeconds: 1 diff --git a/haproxy/templates/NOTES.txt b/haproxy/templates/NOTES.txt new file mode 100644 index 0000000..63ba087 --- /dev/null +++ b/haproxy/templates/NOTES.txt @@ -0,0 +1,58 @@ +HAProxy has been has been successfully installed. This Chart is used to run HAProxy as a regular application, +as opposed to HAProxy Ingress Controller Chart. + +Controller image deployed is: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}". +Your HAProxy app is of a "{{ .Values.kind }}" kind. + +Service ports mapped are: +{{- $nodePorts := .Values.service.nodePorts }} +{{- $servicePortType := .Values.service.type }} +{{- if eq .Values.kind "Deployment" }} +{{- range $key, $value := .Values.containerPorts }} + - name: {{ $key }} + containerPort: {{ $value }} + protocol: TCP + {{- if and (hasKey $nodePorts $key) (eq $servicePortType "NodePort") }} + nodePort: {{ get $nodePorts $key }} + {{- end }} +{{- end }} +{{- end }} +{{- if eq .Values.kind "DaemonSet" }} +{{- $hostPorts := .Values.daemonset.hostPorts -}} +{{- $useHostPort := .Values.daemonset.useHostPort -}} +{{- range $key, $value := .Values.containerPorts }} + - name: {{ $key }} + containerPort: {{ $value }} + protocol: TCP + {{- if $useHostPort }} + hostPort: {{ index $hostPorts $key | default $value }} + {{- end }} + {{- if and (hasKey $nodePorts $key) (eq $servicePortType "NodePort") }} + nodePort: {{ get $nodePorts $key }} + {{- end }} +{{- end }} +{{- end }} + +To be able to bind to privileged ports as non-root, the following is required: + +securityContext: + enabled: true + runAsUser: 1000 + runAsGroup: 1000 +initContainers: + - name: sysctl + image: "busybox:musl" + command: + - /bin/sh + - -c + - sysctl -w net.ipv4.ip_unprivileged_port_start=0 + securityContext: + privileged: true + +Node IP can be found with: + $ kubectl --namespace {{ template "haproxy.namespace" . }} get nodes -o jsonpath="{.items[0].status.addresses[1].address}" + +For more examples and up to date documentation, please visit: + * Helm chart documentation: https://github.com/haproxytech/helm-charts/tree/main/haproxy + * HAProxy Alpine Docker container documentation: https://github.com/haproxytech/haproxy-docker-alpine + * HAProxy documentation: https://www.haproxy.org/download/2.7/doc/configuration.txt diff --git a/haproxy/templates/_helpers.tpl b/haproxy/templates/_helpers.tpl new file mode 100644 index 0000000..8c23e8f --- /dev/null +++ b/haproxy/templates/_helpers.tpl @@ -0,0 +1,105 @@ +{{/* +Copyright 2020 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{/* +Expand the name of the chart. +*/}} +{{- define "haproxy.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Allow the release namespace to be overridden for multi-namespace deployments in combined charts +*/}} +{{- define "haproxy.namespace" -}} +{{- if .Values.namespaceOverride -}} +{{- .Values.namespaceOverride -}} +{{- else -}} +{{- .Release.Namespace -}} +{{- end -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "haproxy.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "haproxy.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "haproxy.labels" -}} +helm.sh/chart: {{ include "haproxy.chart" . }} +{{ include "haproxy.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "haproxy.selectorLabels" -}} +app.kubernetes.io/name: {{ include "haproxy.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "haproxy.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "haproxy.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Create includes name +*/}} +{{- define "haproxy.includes" -}} +{{- printf "%s-%s" (include "haproxy.fullname" .) "includes" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Encode an imagePullSecret string. +*/}} +{{- define "haproxy.imagePullSecret" }} +{{- printf "{\"auths\": {\"%s\": {\"auth\": \"%s\"}}}" .Values.imageCredentials.registry (printf "%s:%s" .Values.imageCredentials.username .Values.imageCredentials.password | b64enc) | b64enc }} +{{- end }} + +{{/* vim: set filetype=mustache: */}} diff --git a/haproxy/templates/configmap.yaml b/haproxy/templates/configmap.yaml new file mode 100644 index 0000000..35c8b83 --- /dev/null +++ b/haproxy/templates/configmap.yaml @@ -0,0 +1,41 @@ +{{/* +Copyright 2020 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.config }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "haproxy.fullname" . }} + namespace: {{ include "haproxy.namespace" . }} + labels: + {{- include "haproxy.labels" . | nindent 4 }} +data: + {{ .Values.configMount.subPath | default "haproxy.cfg" }}: |+ + {{ tpl .Values.config . | nindent 4 }} +{{- end }} + +{{- if .Values.includes }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "haproxy.includes" . }} + namespace: {{ include "haproxy.namespace" . }} +data: +{{- range $key, $val := .Values.includes }} + {{ $key }}: | {{ $val | nindent 4 }} +{{- end }} +{{- end }} diff --git a/haproxy/templates/daemonset.yaml b/haproxy/templates/daemonset.yaml new file mode 100644 index 0000000..dcdd0de --- /dev/null +++ b/haproxy/templates/daemonset.yaml @@ -0,0 +1,193 @@ +{{/* +Copyright 2020 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if eq .Values.kind "DaemonSet" }} +{{- $useHostNetwork := .Values.daemonset.useHostNetwork -}} +{{- $useHostPort := .Values.daemonset.useHostPort -}} +{{- $hostPorts := .Values.daemonset.hostPorts -}} +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: {{ include "haproxy.fullname" . }} + namespace: {{ include "haproxy.namespace" . }} + labels: + {{- include "haproxy.labels" . | nindent 4 }} +spec: + minReadySeconds: {{ .Values.minReadySeconds }} + updateStrategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 1 + selector: + matchLabels: + {{- include "haproxy.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "haproxy.selectorLabels" . | nindent 8 }} + {{- if .Values.podLabels }} +{{ toYaml .Values.podLabels | indent 8 }} + {{- end }} + annotations: + {{- if .Values.checksumConfigMap.enabled }} + checksum/environment: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- end }} + {{- if .Values.podAnnotations }} +{{ tpl (toYaml .Values.podAnnotations) . | indent 8 }} + {{- end }} + spec: + {{- if .Values.shareProcessNamespace.enabled }} + shareProcessNamespace: true + {{- end }} + serviceAccountName: {{ include "haproxy.serviceAccountName" . }} +{{- if hasKey .Values.serviceAccount "automountServiceAccountToken" }} + automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} +{{- end }} + terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} + {{- if $useHostNetwork }} + hostNetwork: true + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} +{{- if .Values.dnsConfig }} + dnsConfig: +{{ toYaml .Values.dnsConfig | indent 8 }} +{{- end }} + dnsPolicy: {{ .Values.dnsPolicy }} +{{- if .Values.imageCredentials.registry }} + imagePullSecrets: + - name: {{ include "haproxy.fullname" . }} +{{- else if .Values.existingImagePullSecret }} + imagePullSecrets: + - name: {{ .Values.existingImagePullSecret }} +{{- end }} +{{- if .Values.priorityClassName }} + priorityClassName: {{ .Values.priorityClassName }} +{{- end }} + volumes: + - name: haproxy-config + configMap: + name: {{ include "haproxy.fullname" . }} + {{- if .Values.includes }} + - name: includes + projected: + sources: + - configMap: + name: {{ include "haproxy.includes" . }} + {{- end }} + {{- range $mountedSecret := .Values.mountedSecrets }} + - name: {{ $mountedSecret.volumeName }} + secret: + secretName: {{ $mountedSecret.secretName }} + {{- end }} + {{- with.Values.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + {{- with.Values.sidecarContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} + - name: {{ .Chart.Name }} + {{- if .Values.securityContext.enabled }} + securityContext: {{- omit .Values.securityContext "enabled" | toYaml | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- if .Values.args.enabled }} + args: + {{- range .Values.args.defaults }} + - {{ . }} + {{- end }} + {{- range .Values.args.extraArgs }} + - {{ . }} + {{- end }} + {{- end }} + ports: + {{- range $key, $value := .Values.containerPorts }} + - name: {{ $key }} + containerPort: {{ $value }} + protocol: TCP + {{- if and $useHostPort (index $hostPorts $key) }} + hostPort: {{ index $hostPorts $key }} + {{- end }} + {{- end }} + {{- with .Values.rawContainerPorts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + {{- with .Values.startupProbe }} + startupProbe: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + {{- with .Values.extraEnvs }} + env: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + {{- with .Values.extraEnvFrom }} + envFrom: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- if .Values.lifecycle }} + lifecycle: + {{- if eq "string" (printf "%T" .Values.lifecycle) }} +{{ tpl .Values.lifecycle . | indent 12 }} + {{- else }} +{{ toYaml .Values.lifecycle | indent 12 }} + {{- end }} + {{- end }} + volumeMounts: + - name: haproxy-config + mountPath: {{ .Values.configMount.mountPath }} + {{- if .Values.configMount.subPath }} + subPath: {{ .Values.configMount.subPath }} + {{- end }} + {{- if .Values.includes }} + - name: includes + mountPath: {{ .Values.includesMountPath }} + {{- end }} + {{- with.Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- range $mountedSecret := .Values.mountedSecrets }} + - name: {{ $mountedSecret.volumeName }} + mountPath: {{ $mountedSecret.mountPath }} + {{- end }} + {{- with.Values.initContainers }} + initContainers: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/haproxy/templates/deployment.yaml b/haproxy/templates/deployment.yaml new file mode 100644 index 0000000..7334e2d --- /dev/null +++ b/haproxy/templates/deployment.yaml @@ -0,0 +1,198 @@ +{{/* +Copyright 2020 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if eq .Values.kind "Deployment" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "haproxy.fullname" . }} + namespace: {{ include "haproxy.namespace" . }} + labels: + {{- include "haproxy.labels" . | nindent 4 }} + {{- if .Values.deploymentLabels }} +{{ tpl (toYaml .Values.deploymentLabels) . | indent 4 }} + {{- end }} + annotations: + {{- if .Values.deploymentAnnotations }} +{{ tpl (toYaml .Values.deploymentAnnotations) . | indent 4 }} + {{- end }} +spec: + minReadySeconds: {{ .Values.minReadySeconds }} + {{- if and (not .Values.autoscaling.enabled) (not .Values.keda.enabled) }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "haproxy.selectorLabels" . | nindent 6 }} + {{- with .Values.strategy }} + strategy: + {{- toYaml . | nindent 4 }} + {{- end }} + template: + metadata: + labels: + {{- include "haproxy.selectorLabels" . | nindent 8 }} + {{- if .Values.podLabels }} +{{ toYaml .Values.podLabels | indent 8 }} + {{- end }} + annotations: + {{- if .Values.checksumConfigMap.enabled }} + checksum/environment: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- end }} + {{- if .Values.podAnnotations }} +{{ tpl (toYaml .Values.podAnnotations) . | indent 8 }} + {{- end }} + spec: + {{- if .Values.shareProcessNamespace.enabled }} + shareProcessNamespace: true + {{- end }} + serviceAccountName: {{ include "haproxy.serviceAccountName" . }} +{{- if hasKey .Values.serviceAccount "automountServiceAccountToken" }} + automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} +{{- end }} + terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} +{{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} +{{- end }} +{{- if .Values.dnsConfig }} + dnsConfig: +{{ toYaml .Values.dnsConfig | indent 8 }} +{{- end }} + dnsPolicy: {{ .Values.dnsPolicy }} +{{- if .Values.imageCredentials.registry }} + imagePullSecrets: + - name: {{ include "haproxy.fullname" . }} +{{- else if .Values.existingImagePullSecret }} + imagePullSecrets: + - name: {{ .Values.existingImagePullSecret }} +{{- end }} +{{- if .Values.priorityClassName }} + priorityClassName: {{ .Values.priorityClassName }} +{{- end }} + volumes: + - name: haproxy-config + configMap: + name: {{ include "haproxy.fullname" . }} + {{- if .Values.includes }} + - name: includes + projected: + sources: + - configMap: + name: {{ include "haproxy.includes" . }} + {{- end }} + {{- range $mountedSecret := .Values.mountedSecrets }} + - name: {{ $mountedSecret.volumeName }} + secret: + secretName: {{ $mountedSecret.secretName }} + {{- end }} + {{- with.Values.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + {{- with.Values.sidecarContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} + - name: {{ .Chart.Name }} + {{- if .Values.securityContext.enabled }} + securityContext: {{- omit .Values.securityContext "enabled" | toYaml | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- if .Values.args.enabled }} + args: + {{- range .Values.args.defaults }} + - {{ . }} + {{- end }} + {{- range .Values.args.extraArgs }} + - {{ . }} + {{- end }} + {{- end }} + ports: + {{- range $key, $value := .Values.containerPorts }} + - name: {{ $key }} + containerPort: {{ $value }} + protocol: TCP + {{- end }} + {{- with .Values.rawContainerPorts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + {{- with .Values.startupProbe }} + startupProbe: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + {{- with .Values.extraEnvs }} + env: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + {{- with .Values.extraEnvFrom }} + envFrom: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- if .Values.lifecycle }} + lifecycle: + {{- if eq "string" (printf "%T" .Values.lifecycle) }} +{{ tpl .Values.lifecycle . | indent 12 }} + {{- else }} +{{ toYaml .Values.lifecycle | indent 12 }} + {{- end }} + {{- end }} + volumeMounts: + - name: haproxy-config + mountPath: {{ .Values.configMount.mountPath }} + {{- if .Values.configMount.subPath }} + subPath: {{ .Values.configMount.subPath }} + {{- end }} + {{- if .Values.includes }} + - name: includes + mountPath: {{ .Values.includesMountPath }} + {{- end }} + {{- with.Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- range $mountedSecret := .Values.mountedSecrets }} + - name: {{ $mountedSecret.volumeName }} + mountPath: {{ $mountedSecret.mountPath }} + {{- end }} + {{- with.Values.initContainers }} + initContainers: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/haproxy/templates/hpa.yaml b/haproxy/templates/hpa.yaml new file mode 100644 index 0000000..74afeb8 --- /dev/null +++ b/haproxy/templates/hpa.yaml @@ -0,0 +1,63 @@ +{{/* +Copyright 2020 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and (eq .Values.kind "Deployment") .Values.autoscaling.enabled }} +{{- if not .Values.keda.enabled }} +{{- if .Capabilities.APIVersions.Has "autoscaling/v2" }} +apiVersion: autoscaling/v2 +{{- else if .Capabilities.APIVersions.Has "autoscaling/v2beta2" }} +apiVersion: autoscaling/v2beta2 +{{- else }} + {{- fail "ERROR: You must have autoscaling/v2 or autoscaling/v2beta2 to use HorizontalPodAutoscaler" }} +{{- end }} +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "haproxy.fullname" . }} + namespace: {{ include "haproxy.namespace" . }} + labels: + {{- include "haproxy.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "haproxy.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + {{- if .Values.autoscaling.behavior }} + behavior: {{- toYaml .Values.autoscaling.behavior | nindent 4 }} + {{- end }} + metrics: + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- with .Values.autoscaling.additionalMetrics }} + {{- toYaml . | trim | nindent 4 }} + {{- end }} +{{- end }} +{{- end }} diff --git a/haproxy/templates/httproute.yaml b/haproxy/templates/httproute.yaml new file mode 100644 index 0000000..58a521a --- /dev/null +++ b/haproxy/templates/httproute.yaml @@ -0,0 +1,40 @@ +{{- if .Values.httpRoute.enabled -}} +{{- $fullName := include "haproxy.fullname" . -}} +{{- $svcPort := .Values.httpRoute.servicePort -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ $fullName }} + labels: + {{- include "haproxy.labels" . | nindent 4 }} + {{- with .Values.httpRoute.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.httpRoute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + {{- with .Values.httpRoute.parentRefs }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.httpRoute.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.httpRoute.rules }} + {{- with .matches }} + - matches: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .filters }} + filters: + {{- toYaml . | nindent 8 }} + {{- end }} + backendRefs: + - name: {{ $fullName }} + port: {{ $svcPort }} + {{- end }} +{{- end }} diff --git a/haproxy/templates/ingress.yaml b/haproxy/templates/ingress.yaml new file mode 100644 index 0000000..9240a5d --- /dev/null +++ b/haproxy/templates/ingress.yaml @@ -0,0 +1,59 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "haproxy.fullname" . -}} +{{- $svcPort := .Values.ingress.servicePort -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + namespace: {{ include "haproxy.namespace" . }} + labels: + {{- include "haproxy.labels" . | nindent 4 }} + {{- with .Values.ingress.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- toYaml .Values.ingress.tls | nindent 4 }} + {{- end -}} +{{- end }} diff --git a/haproxy/templates/keda.yaml b/haproxy/templates/keda.yaml new file mode 100644 index 0000000..f4affa4 --- /dev/null +++ b/haproxy/templates/keda.yaml @@ -0,0 +1,54 @@ +{{/* +Copyright 2021 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and (eq .Values.kind "Deployment") .Values.keda.enabled }} +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{ include "haproxy.fullname" . }} + namespace: {{ include "haproxy.namespace" . }} + labels: + {{- include "haproxy.labels" . | nindent 4 }} + {{- if .Values.keda.scaledObject.annotations }} + annotations: {{ toYaml .Values.keda.scaledObject.annotations | nindent 4 }} + {{- end }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "haproxy.fullname" . }} + pollingInterval: {{ .Values.keda.pollingInterval }} + cooldownPeriod: {{ .Values.keda.cooldownPeriod }} + minReplicaCount: {{ .Values.keda.minReplicas }} + maxReplicaCount: {{ .Values.keda.maxReplicas }} + triggers: +{{- with .Values.keda.triggers }} +{{ toYaml . | indent 2 }} +{{ end }} +{{- with .Values.keda.fallback }} + fallback: +{{ toYaml . | indent 4 }} +{{- end }} + advanced: + restoreToOriginalReplicaCount: {{ .Values.keda.restoreToOriginalReplicaCount }} +{{- if .Values.keda.behavior }} + horizontalPodAutoscalerConfig: + behavior: +{{ with .Values.keda.behavior -}} +{{ toYaml . | indent 8 }} +{{ end }} +{{- end }} +{{- end }} diff --git a/haproxy/templates/poddisruptionbudget.yaml b/haproxy/templates/poddisruptionbudget.yaml new file mode 100644 index 0000000..fe899a9 --- /dev/null +++ b/haproxy/templates/poddisruptionbudget.yaml @@ -0,0 +1,41 @@ +{{/* +Copyright 2019 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.PodDisruptionBudget.enable }} +{{- if .Capabilities.APIVersions.Has "policy/v1" }} +apiVersion: policy/v1 +{{- else if .Capabilities.APIVersions.Has "policy/v1beta1" }} +apiVersion: policy/v1beta1 +{{- else }} + {{- fail "ERROR: You must have policy/v1 or policy/v1 to use PodDisruptionBudget" }} +{{- end }} +kind: PodDisruptionBudget +metadata: + name: {{ include "haproxy.fullname" . }} + namespace: {{ include "haproxy.namespace" . }} + labels: + {{- include "haproxy.labels" . | nindent 4 }} +spec: + {{- if .Values.PodDisruptionBudget.maxUnavailable }} + maxUnavailable: {{ .Values.PodDisruptionBudget.maxUnavailable }} + {{- end }} + {{- if .Values.PodDisruptionBudget.minAvailable }} + minAvailable: {{ .Values.PodDisruptionBudget.minAvailable }} + {{- end }} + selector: + matchLabels: + {{- include "haproxy.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/haproxy/templates/podsecuritypolicy.yaml b/haproxy/templates/podsecuritypolicy.yaml new file mode 100644 index 0000000..db76d79 --- /dev/null +++ b/haproxy/templates/podsecuritypolicy.yaml @@ -0,0 +1,85 @@ +{{/* +Copyright 2020 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if (semverCompare "<1.25.0-0" .Capabilities.KubeVersion.Version) }} +{{- if and .Values.rbac.create .Values.podSecurityPolicy.enabled -}} +{{- $useHostNetwork := .Values.daemonset.useHostNetwork -}} +{{- $useHostPort := .Values.daemonset.useHostPort -}} +{{- $hostPorts := .Values.daemonset.hostPorts -}} +{{- if .Capabilities.APIVersions.Has "policy/v1/PodSecurityPolicy" }} +apiVersion: policy/v1 +{{- else }} +apiVersion: policy/v1beta1 +{{- end }} +kind: PodSecurityPolicy +metadata: + name: {{ include "haproxy.fullname" . }} + namespace: {{ include "haproxy.namespace" . }} + labels: + {{- include "haproxy.labels" . | nindent 4 }} +{{- if .Values.podSecurityPolicy.annotations }} + annotations: +{{ toYaml .Values.podSecurityPolicy.annotations | indent 4 }} +{{- else }} + annotations: + seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'docker/default,runtime/default' + apparmor.security.beta.kubernetes.io/allowedProfileNames: 'runtime/default' + seccomp.security.alpha.kubernetes.io/defaultProfileName: 'runtime/default' + apparmor.security.beta.kubernetes.io/defaultProfileName: 'runtime/default' +{{- end }} +spec: + allowPrivilegeEscalation: true + allowedCapabilities: + - NET_BIND_SERVICE + defaultAllowPrivilegeEscalation: false + fsGroup: + rule: MustRunAs + ranges: + - max: 65535 + min: 1 +{{- if $useHostNetwork }} + hostNetwork: true +{{- end }} +{{- if or $useHostPort $useHostNetwork }} + hostPorts: +{{- range $key, $value := .Values.containerPorts }} + - min: {{ $value }} + max: {{ $value }} +{{- end }} +{{- end }} + hostIPC: false + hostPID: false + privileged: false + runAsUser: + rule: RunAsAny + seLinux: + rule: RunAsAny + supplementalGroups: + rule: MustRunAs + ranges: + - max: 65535 + min: 1 + volumes: + - configMap + - emptyDir + - projected + - secret + {{- with .Values.podSecurityPolicy.allowedUnsafeSysctls }} + allowedUnsafeSysctls: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} +{{- end }} diff --git a/haproxy/templates/pullsecret.yaml b/haproxy/templates/pullsecret.yaml new file mode 100644 index 0000000..1db5cde --- /dev/null +++ b/haproxy/templates/pullsecret.yaml @@ -0,0 +1,28 @@ +{{/* +Copyright 2020 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.imageCredentials.registry }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "haproxy.fullname" . }} + namespace: {{ include "haproxy.namespace" . }} + labels: + {{- include "haproxy.labels" . | nindent 4 }} +type: kubernetes.io/dockerconfigjson +data: + .dockerconfigjson: {{ include "haproxy.imagePullSecret" . }} +{{- end }} diff --git a/haproxy/templates/role.yaml b/haproxy/templates/role.yaml new file mode 100644 index 0000000..f044785 --- /dev/null +++ b/haproxy/templates/role.yaml @@ -0,0 +1,34 @@ +{{/* +Copyright 2019 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and .Values.rbac.create .Values.podSecurityPolicy.enabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "haproxy.fullname" . }} + namespace: {{ include "haproxy.namespace" . }} + labels: + {{- include "haproxy.labels" . | nindent 4 }} +rules: +- apiGroups: + - "policy" + resources: + - podsecuritypolicies + verbs: + - use + resourceNames: + - {{ include "haproxy.fullname" . }} +{{- end -}} diff --git a/haproxy/templates/rolebinding.yaml b/haproxy/templates/rolebinding.yaml new file mode 100644 index 0000000..032cfb3 --- /dev/null +++ b/haproxy/templates/rolebinding.yaml @@ -0,0 +1,33 @@ +{{/* +Copyright 2019 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and .Values.rbac.create .Values.podSecurityPolicy.enabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "haproxy.fullname" . }} + namespace: {{ include "haproxy.namespace" . }} + labels: + {{- include "haproxy.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "haproxy.fullname" . }} +subjects: +- kind: ServiceAccount + name: {{ include "haproxy.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end -}} diff --git a/haproxy/templates/service.yaml b/haproxy/templates/service.yaml new file mode 100644 index 0000000..bc36890 --- /dev/null +++ b/haproxy/templates/service.yaml @@ -0,0 +1,91 @@ +{{/* +Copyright 2020 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +apiVersion: v1 +kind: Service +metadata: + name: {{ include "haproxy.fullname" . }} + namespace: {{ include "haproxy.namespace" . }} + labels: + {{- include "haproxy.labels" . | nindent 4 }} +{{- range $key, $value := .Values.service.labels }} + {{ $key }}: {{ $value | quote }} +{{- end }} + annotations: +{{- range $key, $value := .Values.service.annotations }} + {{ $key }}: {{ $value | quote }} +{{- end }} +spec: + type: {{ .Values.service.type }} + selector: + {{- include "haproxy.selectorLabels" . | nindent 4 }} + {{- if .Values.service.externalTrafficPolicy }} + externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }} + {{- end }} + {{- if .Values.service.internalTrafficPolicy }} + internalTrafficPolicy: {{ .Values.service.internalTrafficPolicy }} + {{- end }} + {{- with .Values.service.clusterIP }} + clusterIP: {{ . | quote}} + {{- end }} + {{- with .Values.service.loadBalancerIP }} + loadBalancerIP: {{ . | quote }} + {{- end }} + {{- with .Values.service.loadBalancerSourceRanges }} + loadBalancerSourceRanges: + {{- toYaml . | nindent 2 }} + {{- end }} + {{- if .Values.service.ipFamilies }} + ipFamilies: + {{- toYaml .Values.service.ipFamilies | nindent 2 }} + {{- end }} + {{- if .Values.service.ipFamilyPolicy }} + ipFamilyPolicy: {{ .Values.service.ipFamilyPolicy | quote }} + {{- end }} + {{- with .Values.service.externalIPs }} + externalIPs: + {{- toYaml . | nindent 2 }} + {{- end }} + {{- if or .Values.containerPorts .Values.service.additionalPorts .Values.service.rawAdditionalPorts }} + {{- $nodePorts := .Values.service.nodePorts }} + {{- $servicePortType := .Values.service.type }} + ports: + {{- with .Values.containerPorts }} + {{- range $key, $port := . }} + - name: {{ $key }} + protocol: TCP + port: {{ $port }} + targetPort: {{ $key }} + {{- if and (hasKey $nodePorts $key) (eq $servicePortType "NodePort") }} + nodePort: {{ get $nodePorts $key }} + {{- end }} + {{- end }} + {{- end }} + {{- with .Values.service.additionalPorts }} + {{- range $key, $port := . }} + - name: {{ $key }} + protocol: TCP + port: {{ $port }} + targetPort: {{ $key }} + {{- if and (hasKey $nodePorts $key) (eq $servicePortType "NodePort") }} + nodePort: {{ get $nodePorts $key }} + {{- end }} + {{- end }} + {{- end }} + {{- with .Values.service.rawAdditionalPorts }} + {{- toYaml . | nindent 2 }} + {{- end }} + {{- end }} diff --git a/haproxy/templates/serviceaccount.yaml b/haproxy/templates/serviceaccount.yaml new file mode 100644 index 0000000..7c09a94 --- /dev/null +++ b/haproxy/templates/serviceaccount.yaml @@ -0,0 +1,32 @@ +{{/* +Copyright 2020 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "haproxy.serviceAccountName" . }} + namespace: {{ include "haproxy.namespace" . }} + labels: + {{- include "haproxy.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- if hasKey .Values.serviceAccount "automountServiceAccountToken" }} +automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} +{{- end }} +{{- end }} diff --git a/haproxy/templates/servicemonitor.yaml b/haproxy/templates/servicemonitor.yaml new file mode 100644 index 0000000..002fe8d --- /dev/null +++ b/haproxy/templates/servicemonitor.yaml @@ -0,0 +1,37 @@ +{{/* +Copyright 2022 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and (.Capabilities.APIVersions.Has "monitoring.coreos.com/v1") .Values.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "haproxy.fullname" . }} + namespace: {{ include "haproxy.namespace" . }} + labels: + {{- include "haproxy.labels" . | nindent 4 }} + {{- if .Values.serviceMonitor.extraLabels }} + {{ toYaml .Values.serviceMonitor.extraLabels | nindent 4 }} + {{- end }} +spec: + endpoints: + {{ .Values.serviceMonitor.endpoints | toYaml | nindent 4 }} + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} + selector: + matchLabels: + {{- include "haproxy.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/haproxy/values.yaml b/haproxy/values.yaml new file mode 100644 index 0000000..7d274c5 --- /dev/null +++ b/haproxy/values.yaml @@ -0,0 +1,643 @@ +# Copyright 2020 HAProxy Technologies LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## Default values for HAProxy + +## Configure Service Account +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/ +serviceAccount: + annotations: {} + create: true + name: + automountServiceAccountToken: true + +## Override namespace for for the whole chart +## If namespaceOverride is set, helm will use it's value instead of .Release.Namespace for all chart components. +## It is useful in case Haproxy is used as a dependency for another helm chart. Value can be overridden in parent chart values.yaml +## Example values.yaml of parent chart: +# namespaceOverride: haproxytech + +## Default values for image +image: + repository: docker.io/haproxytech/haproxy-alpine # can be changed to use CE or EE images + tag: "" # overrides the image tag whose default is the chart appVersion + pullPolicy: IfNotPresent + +## Automatically Roll Deployments +# ref: https://helm.sh/docs/howto/charts_tips_and_tricks/#automatically-roll-deployments +checksumConfigMap: + enabled: true + +## Share Process Namespace between Containers in a Pod +# ref: https://kubernetes.io/docs/tasks/configure-pod-container/share-process-namespace/ +shareProcessNamespace: + enabled: false + +## Pods: How Pods manage multiple containers +# ref: https://kubernetes.io/docs/concepts/workloads/pods/#workload-resources-for-managing-pods +# ref: https://kubernetes.io/docs/concepts/workloads/pods/#how-pods-manage-multiple-containers +sidecarContainers: [] + +## Reflex +# ref: https://github.com/cespare/reflex +# ref: https://hub.docker.com/r/acim/go-reflex +# - name: reflex +# image: acim/go-reflex:1.17.3 +# command: ["reflex", "-d", "fancy"] +# workingDir: /usr/local/etc/haproxy +# args: +# - -svr +# - "..data" +# - -- +# - bash +# - -c +# - 'pkill -SIGUSR2 "haproxy|hapee-lb"' +# ports: +# - name: tcp +# containerPort: 3000 +# protocol: TCP +# imagePullPolicy: IfNotPresent +# volumeMounts: +# - name: haproxy-config +# mountPath: /usr/local/etc/haproxy +# resources: +# limits: +# cpu: 100m +# memory: 128Mi +# requests: +# cpu: 50m +# memory: 64Mi + +## Deployment or DaemonSet pod mode +## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/ +## ref: https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/ +kind: Deployment # can be 'Deployment' or 'DaemonSet' +replicaCount: 1 # used only for Deployment mode + +## minReadySeconds setting of Deployment or DaemonSet +## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#min-ready-seconds +minReadySeconds: 0 + +## Command line arguments to pass to HAProxy +args: + enabled: true # EE images require disabling this due to S6-overlay + # ref: http://cbonte.github.io/haproxy-dconv/2.6/management.html#3 + defaults: ["-f", "/usr/local/etc/haproxy/haproxy.cfg"] + extraArgs: [] # EE images require disabling this due to S6-overlay + +## Annotations to add to the deployment metadata +## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +deploymentAnnotations: {} +# key: value + +deploymentLabels: {} +# key: value + +## Controller Container liveness/readiness probe configuration +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + {} + # failureThreshold: 3 + # successThreshold: 1 + # initialDelaySeconds: 0 + # timeoutSeconds: 1 + # tcpSocket: + # port: 80 + # periodSeconds: 10 + +readinessProbe: + {} + # failureThreshold: 3 + # successThreshold: 1 + # initialDelaySeconds: 0 + # timeoutSeconds: 1 + # tcpSocket: + # port: 80 + # periodSeconds: 10 + +startupProbe: + {} + # failureThreshold: 20 + # successThreshold: 1 + # initialDelaySeconds: 0 + # timeoutSeconds: 1 + # tcpSocket: + # port: 80 + # periodSeconds: 1 + +## DaemonSet configuration +## ref: https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/ +daemonset: + useHostNetwork: false # also modify dnsPolicy accordingly + useHostPort: false + hostPorts: + http: 80 + https: 443 + stat: 1024 + +## Init Containers +## ref: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ +initContainers: [] +# - name: sysctl +# image: "busybox:musl" +# command: +# - /bin/sh +# - -c +# - sysctl -w net.core.somaxconn=65536 +# securityContext: +# privileged: true + +## Pod termination grace period +## ref: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/ +terminationGracePeriodSeconds: 60 + +## Private Registry configuration +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imageCredentials: + registry: null # EE images require setting this + username: null # EE images require setting this + password: null # EE images require setting this +existingImagePullSecret: null + +## Container listener port configuration +## ref: https://kubernetes.io/docs/concepts/services-networking/connect-applications-service/ +containerPorts: # has to match hostPorts when useHostNetwork is true + http: 80 + https: 443 + stat: 1024 + +## Raw container ports configuration (alternative to containerPorts for more control) +## Allows specifying container ports in full Kubernetes format with custom protocols, names, etc. +## ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.33/#containerport-v1-core +rawContainerPorts: [] +# Example: +# - containerPort: 9090 +# name: metrics-port +# protocol: TCP +# - containerPort: 8080 +# name: custom-http +# protocol: TCP + +## Deployment strategy definition +## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy +strategy: {} +# rollingUpdate: +# maxSurge: 25% +# maxUnavailable: 25% +# type: RollingUpdate + +## Pod PriorityClass +## ref: https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass +priorityClassName: "" + +## Container lifecycle handlers +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/attach-handler-lifecycle-event/ +lifecycle: {} + ## Example preStop for graceful shutdown + # preStop: + # exec: + # command: ["/bin/sh", "-c", "kill -USR1 $(pidof haproxy); while killall -0 haproxy; do sleep 1; done"] + +## Additional envs to the main container +extraEnvs: [] +## Example passing the pod IP into a container +# - name: POD_IP +# valueFrom: +# fieldRef: +# fieldPath: status.podIP + +## Use envFrom to add env vars from a secret or ConfigMap to the HAProxy container +## ref: https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/ +extraEnvFrom: [] +## Example passing the pod IP into a container +# - configMapRef: +# name: ha-env-config + +## Additional volumeMounts to the controller main container +extraVolumeMounts: [] +## Example empty volume mounts when using securityContext->readOnlyRootFilesystem +# - name: etc-haproxy +# mountPath: /etc/haproxy +# - name: tmp +# mountPath: /tmp +# - name: var-state-haproxy +# mountPath: /var/state/haproxy + +## Additional volumes to the controller pod +extraVolumes: [] +## Example empty volumes when using securityContext->readOnlyRootFilesystem +# - name: etc-haproxy +# emptyDir: {} +# - name: tmp +# emptyDir: {} +# - name: var-state-haproxy +# emptyDir: {} + +## HAProxy daemon configuration +# ref: https://www.haproxy.org/download/2.6/doc/configuration.txt +config: | + global + log stdout format raw local0 + maxconn 1024 + + defaults + log global + timeout client 60s + timeout connect 60s + timeout server 60s + + frontend fe_main + bind :80 + default_backend be_main + + backend be_main + server web1 10.0.0.1:8080 check + +# Mount path and sub path for config file +configMount: + mountPath: /usr/local/etc/haproxy # EE images use /etc/hapee-VERSION/hapee-lb.cfg + subPath: "" # EE images use hapee-lb.cfg + +## Basic features : Maps +# ref: http://cbonte.github.io/haproxy-dconv/2.6/configuration.html#7.3.1-map +# ref: http://cbonte.github.io/haproxy-dconv/2.6/intro.html#3.3.8 +includes: + # routes.map: | + # www.example.com/v1 www.example2.com/v2 + # api.example.com/v1 api.example2.com/v2 + # static.example.com/v1 static.example2.com/v2 + # 200.http: | + # HTTP/1.1 200 OK + # Cache-Control: no-cache + # Connection: close + # Content-Type: text/html + #

200 OK

+ # Check passed. + # + +## Mount path for includes and maps +includesMountPath: /usr/local/etc/haproxy/includes # EE images use /etc/hapee-VERSION + +## Additional secrets to mount as volumes +## This is expected to be an array of dictionaries specifying the volume name, secret name and mount path +mountedSecrets: [] +# - volumeName: ssl-certificate +# secretName: star-example-com +# mountPath: /usr/local/etc/ssl + +## Pod Node assignment +## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ +nodeSelector: {} + +## Node Taints and Tolerations for pod-node cheduling through attraction/repelling +## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ +tolerations: [] +# - key: "key" +# operator: "Equal|Exists" +# value: "value" +# effect: "NoSchedule|PreferNoSchedule|NoExecute(1.6 only)" + +## Node Affinity for pod-node scheduling constraints +## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity +affinity: {} + +## Topology spread constraints (only used in kind: Deployment) +## ref: https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/ +topologySpreadConstraints: [] +# - maxSkew: 1 +# topologyKey: kubernetes.io/zone +# whenUnsatisfiable: DoNotSchedule +# labelSelector: +# matchLabels: +# app.kubernetes.io/name: kubernetes-ingress +# app.kubernetes.io/instance: kubernetes-ingress + +## Pod DNS Config +## ref: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/ +dnsConfig: {} + +## Pod DNS Policy +## Change this to ClusterFirstWithHostNet in case you have useHostNetwork set to true +## ref: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy +dnsPolicy: ClusterFirst + +## Additional labels to add to the pod container metadata +## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} +# key: value + +## Additional annotations to add to the pod container metadata +## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: {} +# key: value + +## Enable RBAC Authorization +## ref: https://kubernetes.io/docs/reference/access-authn-authz/rbac/ +rbac: + create: true + +## Disableable use of Pod Security Policy +## ref: https://kubernetes.io/docs/concepts/policy/pod-security-policy/ +podSecurityPolicy: + annotations: {} + ## Specify pod annotations + ## Ref: https://kubernetes.io/docs/concepts/policy/pod-security-policy/#apparmor + ## Ref: https://kubernetes.io/docs/concepts/policy/pod-security-policy/#seccomp + ## Ref: https://kubernetes.io/docs/concepts/policy/pod-security-policy/#sysctl + + ### WARNING!!! "Apparmor is only available Ubuntu/Debian distributions of Linux." + + # apparmor.security.beta.kubernetes.io/allowedProfileNames: runtime/default + # apparmor.security.beta.kubernetes.io/defaultProfileName: runtime/default + # seccomp.security.alpha.kubernetes.io/allowedProfileNames: runtime/default + # seccomp.security.alpha.kubernetes.io/defaultProfileName: runtime/default + enabled: false + # ref: https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/ + # Enable only when added kublet arg: --allowed-unsafe-sysctls strings + allowedUnsafeSysctls: + # - net.* + +## Pod Security Context +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ +podSecurityContext: {} + ### ref: https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster/ + ### Sysctls enable only when added kublet arg: --allowed-unsafe-sysctls strings + # sysctls: + # - name: net.ipv4.tcp_rmem + # value: 4096 16060 262144 + # - name: net.ipv4.tcp_wmem + # value: 4096 16384 262144 + # - name: net.ipv4.tcp_tw_reuse + # value: "1" + # - name: net.ipv4.ip_local_port_range + # value: 1024 65023 + # - name: net.ipv4.tcp_max_syn_backlog + # value: "60000" + # - name: net.ipv4.tcp_fin_timeout + # value: "30" + # - name: net.ipv4.tcp_synack_retries + # value: "3" + # - name: net.ipv4.ip_nonlocal_bind + # value: "1" + # - name: net.core.somaxconn + # value: "60000" + +## Container Security Context +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ +securityContext: {} +# enabled: true +# runAsNonRoot: true +# runAsUser: 1000 +# runAsGroup: 1000 +# allowPrivilegeEscalation: true +# capabilities: +# drop: +# - ALL +# add: +# - NET_BIND_SERVICE +# seccompProfile: +# type: RuntimeDefault + +## Compute Resources +## ref: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/ +resources: +# limits: +# cpu: 250m +# memory: 128Mi + requests: + cpu: 250m + memory: 128Mi + +## Horizontal Pod Scaler +## Only to be used with Deployment kind +## ref: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 7 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + # additionalMetrics: + # - type: Object + # object: + # metric: + # name: requests-per-second + # describedObject: + # apiVersion: networking.k8s.io/v1 + # kind: Ingress + # name: main-route + # target: + # type: Value + # value: 10k + ## Behavior + ## ref: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#configurable-scaling-behavior + # behavior: + # scaleDown: + # stabilizationWindowSeconds: 3600 + +keda: + enabled: false + minReplicas: 2 + maxReplicas: 20 + pollingInterval: 30 + cooldownPeriod: 300 + restoreToOriginalReplicaCount: false + # fallback: + # failureThreshold: 3 + # replicas: 6 + # behavior: static + scaledObject: + annotations: {} + behavior: {} + # scaleDown: + # stabilizationWindowSeconds: 300 + # policies: + # - type: Percent + # value: 100 + # periodSeconds: 15 + triggers: [] + # - type: prometheus + # metadata: + # serverAddress: http://:9090 + # metricName: haproxy_process_idle_time_percent + # threshold: '50' + # query: avg(100-avg_over_time(haproxy_process_idle_time_percent{container="kubernetes-ingress-controller",service="mytest-kubernetes-ingress"}[2m])) + +## Pod Disruption Budget +## Only to be used with Deployment kind +## ref: https://kubernetes.io/docs/tasks/run-application/configure-pdb/ +PodDisruptionBudget: + enable: false + # maxUnavailable: 1 + # minAvailable: 1 + +## Service configuration +## ref: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + type: ClusterIP # can be 'ClusterIP', 'NodePort', 'LoadBalancer' + + ## Service ClusterIP + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/ + clusterIP: "" + + ## LoadBalancer IP + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer + loadBalancerIP: "" + + ## Source IP ranges permitted to access Network Load Balancer + # ref: https://kubernetes.io/docs/tasks/access-application-cluster/configure-cloud-provider-firewall/ + loadBalancerSourceRanges: [] + + ## Service ExternalIPs + # ref: https://kubernetes.io/docs/concepts/services-networking/service/#external-ips + externalIPs: [] + + ## Service annotations + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + annotations: {} + + ## Service labels + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + labels: {} + + ## IPv4/IPv6 dual-stack + ## ref: https://kubernetes.io/docs/concepts/services-networking/dual-stack/ + ## + # ipFamilies: [IPv4, IPv6] + # ipFamilyPolicy: PreferDualStack + + ## Service externalTrafficPolicy + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#external-traffic-policy + # externalTrafficPolicy: Cluster + + ## Service internalTrafficPolicy + ## ref: https://kubernetes.io/docs/concepts/services-networking/service-traffic-policy/ + # internalTrafficPolicy: Cluster + + ## Additional Service ports to use(e.g. port of side container haproxy exporter) + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/ + additionalPorts: {} + # prometheus: 9101 + + ## Raw additional service ports configuration (alternative to additionalPorts for more control) + ## Allows specifying service ports in full Kubernetes format with custom protocols, names, etc. + ## ref: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.33/#serviceport-v1-core + rawAdditionalPorts: [] + # Example: + # - name: metrics-port + # port: 9090 + # targetPort: 9090 + # protocol: TCP + # - name: custom-service + # port: 8080 + # targetPort: custom-http + # protocol: TCP + + ## NodePort custom port + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#nodeport-custom-port + nodePorts: {} + # http: 32080 + # https: 32443 + +serviceMonitor: + ## Toggle the ServiceMonitor true if you have Prometheus Operator installed and configured + enabled: false + + ## Specify the labels to add to the ServiceMonitors to be selected for target discovery + extraLabels: {} + + ## Specify the endpoints + ## ref: https://github.com/prometheus-operator/prometheus-operator/blob/master/Documentation/design.md#servicemonitor + endpoints: + - port: prometheus + path: /metrics + scheme: http + interval: 30s + +## Configure Ingress +## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + servicePort: 80 + + ## Ingress class + ## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-class + className: "" + + ## Ingress labels + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + labels: {} + + ## Ingress annotations + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + + ## Ingress hosts + ## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-rules + hosts: + - host: haproxy.domain.com + paths: + - path: / + pathType: ImplementationSpecific + + ## Ingress TLS + ## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls + tls: [] + # - secretName: chart-example-tls + # hosts: + # - haproxy.domain.com + +# -- Expose the service via gateway-api HTTPRoute +# Requires Gateway API resources and suitable controller installed within the cluster +# (see: https://gateway-api.sigs.k8s.io/guides/) +httpRoute: + # HTTPRoute enabled. + enabled: false + # HTTPRoute servicePort to route to + servicePort: 80 + # HTTPRoute labels + labels: {} + # HTTPRoute annotations. + annotations: {} + # Which Gateways this Route is attached to. + parentRefs: + - name: gateway + sectionName: http + # namespace: default + # Hostnames matching HTTP header. + hostnames: + - chart-example.local + # List of rules and filters applied. + rules: + - matches: + - path: + type: PathPrefix + value: / + # filters: + # - type: RequestHeaderModifier + # requestHeaderModifier: + # set: + # - name: My-Overwrite-Header + # value: this-is-the-only-value + # remove: + # - User-Agent + # - matches: + # - path: + # type: PathPrefix + # value: /echo + # headers: + # - name: version + # value: v2 diff --git a/kubernetes-ingress/.helmignore b/kubernetes-ingress/.helmignore new file mode 100644 index 0000000..f0c1319 --- /dev/null +++ b/kubernetes-ingress/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/kubernetes-ingress/Chart.yaml b/kubernetes-ingress/Chart.yaml new file mode 100644 index 0000000..aa87b20 --- /dev/null +++ b/kubernetes-ingress/Chart.yaml @@ -0,0 +1,36 @@ +# Copyright 2019 HAProxy Technologies LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v2 +name: kubernetes-ingress +description: A Helm chart for HAProxy Kubernetes Ingress Controller +type: application +version: 1.49.0 +appVersion: 3.2.6 +kubeVersion: ">=1.23.0-0" +keywords: + - ingress + - haproxy +home: https://github.com/haproxytech/helm-charts/tree/main/kubernetes-ingress +sources: + - https://github.com/haproxytech/kubernetes-ingress +icon: https://raw.githubusercontent.com/haproxytech/helm-charts/main/kubernetes-ingress/chart-icon.png +maintainers: + - name: Dinko Korunic + email: dkorunic@haproxy.com +engine: gotpl +annotations: + artifacthub.io/changes: |- + - Use Ingress Controller 3.2.6 version for base image + - Add hostNetwork and hostPort support for Deployment (#341) diff --git a/kubernetes-ingress/README.md b/kubernetes-ingress/README.md new file mode 100644 index 0000000..4a9ad54 --- /dev/null +++ b/kubernetes-ingress/README.md @@ -0,0 +1,336 @@ +# ![HAProxy](https://github.com/haproxytech/kubernetes-ingress/raw/master/assets/images/haproxy-weblogo-210x49.png "HAProxy") + +## HAProxy Kubernetes Ingress Controller + +An ingress controller is a Kubernetes resource that routes traffic from outside your cluster to services within the cluster. HAProxy Kubernetes Ingress Controller uses ConfigMap to store the haproxy configuration. + +Detailed documentation can be found within the [Official Documentation](https://www.haproxy.com/documentation/kubernetes/latest/). + +Additional configuration details can be found in [annotation reference](https://github.com/haproxytech/kubernetes-ingress/tree/master/documentation) and in image [arguments reference](https://github.com/haproxytech/kubernetes-ingress/blob/master/documentation/controller.md). + +## Introduction + +This chart bootstraps an HAProxy kubernetes-ingress deployment/daemonset on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager. + +### Prerequisites + +- Kubernetes 1.22+ (recommended 1.24+) +- Helm 3.6+ (recommended 3.7+) + +## Before you begin + +### Setting up a Kubernetes Cluster + +The quickest way to setup a Kubernetes cluster is with [Azure Kubernetes Service](https://azure.microsoft.com/en-us/services/kubernetes-service/), [AWS Elastic Kubernetes Service](https://aws.amazon.com/eks/) or [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/) using their respective quick-start guides. + +For setting up Kubernetes on other cloud platforms or bare-metal servers refer to the Kubernetes [getting started guide](http://kubernetes.io/docs/getting-started-guides/). + +### Install Helm + +Get the latest [Helm release](https://github.com/helm/helm#install). + +### Adding Helm chart repo + +Once you have Helm installed, add the haproxytech Chart Repository as follows: + +```console +helm repo add haproxytech https://haproxytech.github.io/helm-charts + +helm repo update +``` + +Alternatively if you want to proceed with just OCI-based repository, skip this step and follow the installation with OCI. + +## Installing the chart + +To install the chart with Helm v3 as _my-release_ deployment from Chat Repository: + +```console +helm install my-release haproxytech/kubernetes-ingress +``` + +**_NOTE_**: To install the chart with Helm v2 (legacy Helm) the syntax requires adding deployment name to `--name` parameter: + +```console +helm install haproxytech/kubernetes-ingress \ + --name my-release +``` + +Alternatively also have OCI-based repository available for simplified access: + +```console +helm install oci://ghcr.io/haproxytech/helm-charts/kubernetes-ingress --version 1.44.1 +``` + +### Installing with unique name + +To auto-generate controller and its resources names when installing, use the following: + +```console +helm install haproxytech/kubernetes-ingress \ + --generate-name +``` + +### Installing from a private registry + +To install the chart using a private registry for controller into a separate namespace _prod_. + +**_NOTE_**: Helm v3 requires namespace to be precreated (eg. with `kubectl create namespace prod`) + +```console +helm install my-ingress haproxytech/kubernetes-ingress \ + --namespace prod \ + --set controller.image.tag=SOMETAG \ + --set controller.imageCredentials.registry=myregistry.domain.com \ + --set controller.imageCredentials.username=MYUSERNAME \ + --set controller.imageCredentials.password=MYPASSWORD +``` + +Alternatively, use a pre-configured (existing) imagePullSecret in the same namespace: + +```console +helm install my-ingress haproxytech/kubernetes-ingress \ + --namespace prod \ + --set controller.image.tag=SOMETAG \ + --set controller.existingImagePullSecret name-of-existing-image-pull-secret +``` + +### Using values from YAML file + +As opposed to using many `--set` invocations, much simpler approach is to define value overrides in a separate YAML file and specify them when invoking Helm: + +_mylb.yaml_: + +```yaml +controller: + kind: DaemonSet + ingressClass: haproxy + service: + type: LoadBalancer + annotations: + service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true" + service.beta.kubernetes.io/aws-load-balancer-internal: 0.0.0.0/0 +``` + +And invoking Helm becomes (compare to the previous example): + +```console +helm install my-ingress -f mylb.yml haproxytech/kubernetes-ingress +``` + +A typical YAML file for TCP services looks like (provided that configmap "[default/tcp](https://github.com/haproxytech/kubernetes-ingress/blob/master/documentation/controller.md)" was created) : + +```yaml +controller: + service: + tcpPorts: + - name: mysql + port: 3306 + targetPort: 3306 + extraArgs: + - --configmap-tcp-services=default/tcp +``` + +### Installing as DaemonSet + +Default controller mode is [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/), but it is possible to use [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) as well: + +```console +helm install my-ingress haproxytech/kubernetes-ingress \ + --set controller.kind=DaemonSet +``` + +### Installing in multi-ingress environment + +It is also possible to set controller ingress class to be used in [multi-ingress environments](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/#using-multiple-ingress-controllers): + +```console +helm install my-ingress haproxytech/kubernetes-ingress \ + --set controller.kind=DaemonSet \ + --set controller.ingressClass=haproxy +``` + +**_NOTE_**: make sure your Ingress routes have corresponding `ingress.class: haproxy` annotation. + +### Installing Gateway API support + +[Gateway API support](https://gateway-api.sigs.k8s.io/) can be installed and used wth controller. Supported features can seen in [Ingress Controller documentation](https://github.com/haproxytech/kubernetes-ingress/blob/master/documentation/gateway-api.md) + +```console +helm install my-ingress haproxytech/kubernetes-ingress \ + --set controller.gatewayControllerName=haproxy.org/gateway-controller +``` + +**_NOTE_**: Gateway API is not part of the default k8s API so it needs to be installed. + +### Installing with service annotations + +On some environments like EKS and GKE there might be a need to pass service annotations. Syntax can become a little tedious however: + +```console +helm install my-ingress haproxytech/kubernetes-ingress \ + --set controller.kind=DaemonSet \ + --set controller.ingressClass=haproxy \ + --set controller.service.type=LoadBalancer \ + --set controller.service.annotations."service\.beta\.kubernetes\.io/aws-load-balancer-internal"="0.0.0.0/0" \ + --set controller.service.annotations."service\.beta\.kubernetes\.io/aws-load-balancer-cross-zone-load-balancing-enabled"="true" +``` + +**_NOTE_**: With helm `--set` it is needed to put quotes and escape dots in the annotation key and commas in the value string. + +### Installing with Horizontal Pod Autoscaler (HPA) + +[HPA](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/) automatically scales number of replicas in Deployment or Replication Controller and adjusts replica count for the controller: + +```console +helm install my-ingress haproxytech/kubernetes-ingress \ + --set controller.autoscaling.enabled=true +``` + +### Installing the ServiceMonitor + +If you're using the [Prometheus Operator](https://github.com/prometheus-operator/prometheus-operator), you can automatically install the `ServiceMonitor` definition in order to automate the scraping options according to your needs. + +```console +helm repo add prometheus-community https://prometheus-community.github.io/helm-charts + +helm install prometheus prometheus-community/kube-prometheus-stack \ + --set prometheus.prometheusSpec.podMonitorSelectorNilUsesHelmValues=false \ + --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false + +helm install my-ingress haproxytech/kubernetes-ingress \ + --set controller.serviceMonitor.enabled=true +``` + +### Installing the PodMonitor + +As an alternative to a `ServiceMonitor` you can use a `PodMonitor`, which targets the pods directly instead of using a service. +If you're using the [Prometheus Operator](https://github.com/prometheus-operator/prometheus-operator), you can automatically install the `PodMonitor` definition in order to automate the scraping options according to your needs. + +```console +helm repo add prometheus-community https://prometheus-community.github.io/helm-charts + +helm install prometheus prometheus-community/kube-prometheus-stack \ + --set prometheus.prometheusSpec.podMonitorSelectorNilUsesHelmValues=false \ + --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false + +helm install my-ingress haproxytech/kubernetes-ingress \ + --set controller.podMonitor.enabled=true +``` + +### Installing with Kubernetes Event-driven Autoscaling (KEDA) + +[KEDA](https://keda.sh/docs/2.3/concepts/scaling-deployments/) is an improved scaling solution built on top of HPA which allows autoscaling criteria based on information from any event source including Prometheus metrics collected from HAProxy native Prometheus Exporter. + +To enable KEDA, you will also need to install Prometheus Operator and ServiceMonitor enabled (serverAddress has to match `prometheus-kube-prometheus-prometheus` service IP): + +_mykeda.yaml_: + +```yaml +controller: + kind: Deployment + serviceMonitor: + enabled: true + keda: + enabled: true + minReplicas: 1 + maxReplicas: 5 + triggers: + - type: prometheus + metadata: + serverAddress: http://10.96.206.247:9090 + metricName: haproxy_frontend_current_sessions + threshold: "100" + query: sum(rate(haproxy_frontend_current_sessions{proxy="http"}[2m])) +``` + +Note: Other options to trigger scaling can be found in Prometheus [native exporter documentation](https://github.com/haproxy/haproxy/blob/master/addons/promex/README), but some ideas are: + +- `haproxy_process_idle_time_percent` +- `haproxy_frontend_current_sessions` +- `haproxy_backend_current_queue` + +And to install: + +```console +helm repo add prometheus-community https://prometheus-community.github.io/helm-charts +helm repo add kedacore https://kedacore.github.io/charts + +helm repo update + +helm install prometheus prometheus-community/kube-prometheus-stack \ + --set prometheus.prometheusSpec.podMonitorSelectorNilUsesHelmValues=false \ + --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false + +kubectl create namespace keda +helm install keda kedacore/keda --namespace keda + +helm install mytest haproxytech/kubernetes-ingress -f mykeda.yaml +``` + +### Installing on Amazon Elastic Kubernetes Service (EKS) + +By default AWS LB does not support mixed protocols (TCP and UDP) on the same port yet, resulting in the following error on deploy: + +``` +Error syncing load balancer: failed to ensure load balancer: mixed protocol is not supported for LoadBalancer +``` + +This issue can be easily fixed by disabling QUIC support (requires `udp/443` listener) with the following: + +```console +helm install my-ingress haproxytech/kubernetes-ingress \ + --set controller.service.type=LoadBalancer \ + --set controller.service.enablePorts.quic=false +``` + +### Installing on Azure Managed Kubernetes Service (AKS) + +By default Azure LB sends probe to `/` and expects HTTP status codes of 200-399 to consider Pod healthy, which means probes end up on default HTTP backend returning HTTP 404 status code. Since v1.20 AKS service annotation `service.beta.kubernetes.io/azure-load-balancer-health-probe-request-path` can be used to override health probe behaviour and we recommend using the following annotation on AKS to target `/healthz` endpoint for health probes: + +```console +helm install ... + --set controller.service.type=LoadBalancer \ + --set controller.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path"=/healthz +``` + +## Upgrading the chart + +To upgrade the _my-release_ deployment: + +```console +helm upgrade my-release haproxytech/kubernetes-ingress +``` + +By default Helm [does not upgrade](https://helm.sh/docs/chart_best_practices/custom_resource_definitions/) CRDs during an upgrade, so before doing an upgrade it is mandatory to upgrade CRDs to the latest version by hand **before** doing a Helm chart upgrade. + +```console +kubectl apply -f https://raw.githubusercontent.com/haproxytech/helm-charts/main/kubernetes-ingress/crds/core.haproxy.org_defaults.yaml +kubectl apply -f https://raw.githubusercontent.com/haproxytech/helm-charts/main/kubernetes-ingress/crds/core.haproxy.org_globals.yaml +kubectl apply -f https://raw.githubusercontent.com/haproxytech/helm-charts/main/kubernetes-ingress/crds/core.haproxy.org_backends.yaml +``` + +Note: from Helm Chart 1.35.0, Helm Chart contains CRD install/upgrade job that will take care of both installing and +upgrading CRDs accordingly. + +## Uninstalling the chart + +To uninstall/delete the _my-release_ deployment: + +```console +helm delete my-release +``` + +## Debugging + +It is possible to generate a set of YAML files for testing/debugging: + +```console +helm install my-release haproxytech/kubernetes-ingress \ + --debug \ + --dry-run +``` + +## Contributing + +We welcome all contributions. Please refer to [guidelines](../CONTRIBUTING.md) on how to make a contribution. diff --git a/kubernetes-ingress/chart-icon.png b/kubernetes-ingress/chart-icon.png new file mode 100644 index 0000000..1c7acc4 Binary files /dev/null and b/kubernetes-ingress/chart-icon.png differ diff --git a/kubernetes-ingress/ci/daemonset-customconfig-templated-values.yaml b/kubernetes-ingress/ci/daemonset-customconfig-templated-values.yaml new file mode 100644 index 0000000..621842a --- /dev/null +++ b/kubernetes-ingress/ci/daemonset-customconfig-templated-values.yaml @@ -0,0 +1,6 @@ +controller: + kind: DaemonSet + config: | + rate-limit: "{{ .Values.controller.configVars.rateLimit | required "controller.configVars.rateLimit is required" }}" + configVars: + rateLimit: "ON" diff --git a/kubernetes-ingress/ci/daemonset-customconfig-values.yaml b/kubernetes-ingress/ci/daemonset-customconfig-values.yaml new file mode 100644 index 0000000..116158a --- /dev/null +++ b/kubernetes-ingress/ci/daemonset-customconfig-values.yaml @@ -0,0 +1,4 @@ +controller: + kind: DaemonSet + config: + rate-limit: "ON" diff --git a/kubernetes-ingress/ci/daemonset-customnodeport-values.yaml b/kubernetes-ingress/ci/daemonset-customnodeport-values.yaml new file mode 100644 index 0000000..c9de04c --- /dev/null +++ b/kubernetes-ingress/ci/daemonset-customnodeport-values.yaml @@ -0,0 +1,7 @@ +controller: + kind: DaemonSet + service: + type: NodePort + ports: + 8000: 10000 + 8001: 10001 diff --git a/kubernetes-ingress/ci/daemonset-default-values.yaml b/kubernetes-ingress/ci/daemonset-default-values.yaml new file mode 100644 index 0000000..ddb2562 --- /dev/null +++ b/kubernetes-ingress/ci/daemonset-default-values.yaml @@ -0,0 +1,2 @@ +controller: + kind: DaemonSet diff --git a/kubernetes-ingress/ci/daemonset-disableddefaultbackend-values.yaml b/kubernetes-ingress/ci/daemonset-disableddefaultbackend-values.yaml new file mode 100644 index 0000000..3a1687a --- /dev/null +++ b/kubernetes-ingress/ci/daemonset-disableddefaultbackend-values.yaml @@ -0,0 +1,4 @@ +controller: + kind: DaemonSet +defaultBackend: + enabled: false diff --git a/kubernetes-ingress/ci/daemonset-disabledsecretconfig-values.yaml b/kubernetes-ingress/ci/daemonset-disabledsecretconfig-values.yaml new file mode 100644 index 0000000..362fbb9 --- /dev/null +++ b/kubernetes-ingress/ci/daemonset-disabledsecretconfig-values.yaml @@ -0,0 +1,4 @@ +controller: + kind: DaemonSet + defaultTLSSecret: + enabled: false diff --git a/kubernetes-ingress/ci/daemonset-enableports-values.yaml b/kubernetes-ingress/ci/daemonset-enableports-values.yaml new file mode 100644 index 0000000..9a41dac --- /dev/null +++ b/kubernetes-ingress/ci/daemonset-enableports-values.yaml @@ -0,0 +1,7 @@ +controller: + kind: DaemonSet + service: + enablePorts: + http: false + https: true + stat: false diff --git a/kubernetes-ingress/ci/daemonset-extraargs-values.yaml b/kubernetes-ingress/ci/daemonset-extraargs-values.yaml new file mode 100644 index 0000000..691acbc --- /dev/null +++ b/kubernetes-ingress/ci/daemonset-extraargs-values.yaml @@ -0,0 +1,4 @@ +controller: + kind: DaemonSet + extraArgs: + - --namespace-whitelist=default diff --git a/kubernetes-ingress/ci/daemonset-extraenvs-values.yaml b/kubernetes-ingress/ci/daemonset-extraenvs-values.yaml new file mode 100644 index 0000000..35294fa --- /dev/null +++ b/kubernetes-ingress/ci/daemonset-extraenvs-values.yaml @@ -0,0 +1,7 @@ +controller: + kind: DaemonSet + extraEnvs: + - name: TEST_STR1 + value: foo + - name: TEST_STR2 + value: baz diff --git a/kubernetes-ingress/ci/daemonset-hostport-values.yaml b/kubernetes-ingress/ci/daemonset-hostport-values.yaml new file mode 100644 index 0000000..45042ea --- /dev/null +++ b/kubernetes-ingress/ci/daemonset-hostport-values.yaml @@ -0,0 +1,8 @@ +controller: + kind: DaemonSet + daemonset: + useHostPort: true + hostPorts: + http: 80 + https: 443 + stat: 1024 diff --git a/kubernetes-ingress/ci/daemonset-ingressclass-values.yaml b/kubernetes-ingress/ci/daemonset-ingressclass-values.yaml new file mode 100644 index 0000000..15b3dae --- /dev/null +++ b/kubernetes-ingress/ci/daemonset-ingressclass-values.yaml @@ -0,0 +1,6 @@ +controller: + kind: DaemonSet + ingressClass: haproxy + ingressClassResource: + enabled: true + default: true diff --git a/kubernetes-ingress/ci/daemonset-ipfamily-values.yaml b/kubernetes-ingress/ci/daemonset-ipfamily-values.yaml new file mode 100644 index 0000000..cc8a976 --- /dev/null +++ b/kubernetes-ingress/ci/daemonset-ipfamily-values.yaml @@ -0,0 +1,5 @@ +controller: + kind: DaemonSet + service: + ipFamilies: [IPv4] + ipFamilyPolicy: SingleStack diff --git a/kubernetes-ingress/ci/daemonset-kubernetesgateway-values.yaml b/kubernetes-ingress/ci/daemonset-kubernetesgateway-values.yaml new file mode 100644 index 0000000..026f75b --- /dev/null +++ b/kubernetes-ingress/ci/daemonset-kubernetesgateway-values.yaml @@ -0,0 +1,5 @@ +controller: + kind: DaemonSet + kubernetesGateway: + enabled: true + gatewayControllerName: haproxy.org/gateway-controller diff --git a/kubernetes-ingress/ci/daemonset-nodeport-values.yaml b/kubernetes-ingress/ci/daemonset-nodeport-values.yaml new file mode 100644 index 0000000..ebc8f10 --- /dev/null +++ b/kubernetes-ingress/ci/daemonset-nodeport-values.yaml @@ -0,0 +1,4 @@ +controller: + kind: DaemonSet + service: + type: NodePort diff --git a/kubernetes-ingress/ci/daemonset-privileged-ports.values.yaml b/kubernetes-ingress/ci/daemonset-privileged-ports.values.yaml new file mode 100644 index 0000000..1efe9a7 --- /dev/null +++ b/kubernetes-ingress/ci/daemonset-privileged-ports.values.yaml @@ -0,0 +1,6 @@ +controller: + kind: DaemonSet + containerPort: + http: 80 + https: 443 + stat: 1024 diff --git a/kubernetes-ingress/ci/daemonset-publishservice-values.yaml b/kubernetes-ingress/ci/daemonset-publishservice-values.yaml new file mode 100644 index 0000000..b538cb5 --- /dev/null +++ b/kubernetes-ingress/ci/daemonset-publishservice-values.yaml @@ -0,0 +1,5 @@ +controller: + kind: DaemonSet + service: + annotations: + service.beta.kubernetes.io/aws-load-balancer-internal: 0.0.0.0/0 diff --git a/kubernetes-ingress/ci/daemonset-serviceannotation-values.yaml b/kubernetes-ingress/ci/daemonset-serviceannotation-values.yaml new file mode 100644 index 0000000..b538cb5 --- /dev/null +++ b/kubernetes-ingress/ci/daemonset-serviceannotation-values.yaml @@ -0,0 +1,5 @@ +controller: + kind: DaemonSet + service: + annotations: + service.beta.kubernetes.io/aws-load-balancer-internal: 0.0.0.0/0 diff --git a/kubernetes-ingress/ci/daemonset-strategy-values.yaml b/kubernetes-ingress/ci/daemonset-strategy-values.yaml new file mode 100644 index 0000000..ed45d7a --- /dev/null +++ b/kubernetes-ingress/ci/daemonset-strategy-values.yaml @@ -0,0 +1,7 @@ +controller: + kind: DaemonSet + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 diff --git a/kubernetes-ingress/ci/daemonset-unprivileged-values.yaml b/kubernetes-ingress/ci/daemonset-unprivileged-values.yaml new file mode 100644 index 0000000..0311d62 --- /dev/null +++ b/kubernetes-ingress/ci/daemonset-unprivileged-values.yaml @@ -0,0 +1,4 @@ +controller: + kind: DaemonSet + unprivileged: true + allowPrivilegedPorts: true diff --git a/kubernetes-ingress/ci/deployment-customconfig-templated-values.yaml b/kubernetes-ingress/ci/deployment-customconfig-templated-values.yaml new file mode 100644 index 0000000..e06384a --- /dev/null +++ b/kubernetes-ingress/ci/deployment-customconfig-templated-values.yaml @@ -0,0 +1,5 @@ +controller: + config: | + rate-limit: "{{ .Values.controller.configVars.rateLimit | required "controller.configVars.rateLimit is required" }}" + configVars: + rateLimit: "ON" diff --git a/kubernetes-ingress/ci/deployment-customconfig-values.yaml b/kubernetes-ingress/ci/deployment-customconfig-values.yaml new file mode 100644 index 0000000..12c48d2 --- /dev/null +++ b/kubernetes-ingress/ci/deployment-customconfig-values.yaml @@ -0,0 +1,3 @@ +controller: + config: + rate-limit: "ON" diff --git a/kubernetes-ingress/ci/deployment-customnodeport-values.yaml b/kubernetes-ingress/ci/deployment-customnodeport-values.yaml new file mode 100644 index 0000000..f044362 --- /dev/null +++ b/kubernetes-ingress/ci/deployment-customnodeport-values.yaml @@ -0,0 +1,6 @@ +controller: + service: + type: NodePort + ports: + 8000: 10000 + 8001: 10001 diff --git a/kubernetes-ingress/ci/deployment-default-values.yaml b/kubernetes-ingress/ci/deployment-default-values.yaml new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/kubernetes-ingress/ci/deployment-default-values.yaml @@ -0,0 +1 @@ +# diff --git a/kubernetes-ingress/ci/deployment-disableddefaultbackend-values.yaml b/kubernetes-ingress/ci/deployment-disableddefaultbackend-values.yaml new file mode 100644 index 0000000..ba2a61e --- /dev/null +++ b/kubernetes-ingress/ci/deployment-disableddefaultbackend-values.yaml @@ -0,0 +1,2 @@ +defaultBackend: + enabled: false diff --git a/kubernetes-ingress/ci/deployment-disabledsecretconfig-values.yaml b/kubernetes-ingress/ci/deployment-disabledsecretconfig-values.yaml new file mode 100644 index 0000000..7676459 --- /dev/null +++ b/kubernetes-ingress/ci/deployment-disabledsecretconfig-values.yaml @@ -0,0 +1,3 @@ +controller: + defaultTLSSecret: + enabled: false diff --git a/kubernetes-ingress/ci/deployment-enableports-values.yaml b/kubernetes-ingress/ci/deployment-enableports-values.yaml new file mode 100644 index 0000000..03ff297 --- /dev/null +++ b/kubernetes-ingress/ci/deployment-enableports-values.yaml @@ -0,0 +1,6 @@ +controller: + service: + enablePorts: + http: false + https: true + stat: false diff --git a/kubernetes-ingress/ci/deployment-extraargs-values.yaml b/kubernetes-ingress/ci/deployment-extraargs-values.yaml new file mode 100644 index 0000000..d0e1dbe --- /dev/null +++ b/kubernetes-ingress/ci/deployment-extraargs-values.yaml @@ -0,0 +1,3 @@ +controller: + extraArgs: + - --namespace-whitelist=default diff --git a/kubernetes-ingress/ci/deployment-extraenvs-values.yaml b/kubernetes-ingress/ci/deployment-extraenvs-values.yaml new file mode 100644 index 0000000..1f9e30c --- /dev/null +++ b/kubernetes-ingress/ci/deployment-extraenvs-values.yaml @@ -0,0 +1,6 @@ +controller: + extraEnvs: + - name: TEST_STR1 + value: foo + - name: TEST_STR2 + value: baz diff --git a/kubernetes-ingress/ci/deployment-hpa-values.yaml b/kubernetes-ingress/ci/deployment-hpa-values.yaml new file mode 100644 index 0000000..f5da547 --- /dev/null +++ b/kubernetes-ingress/ci/deployment-hpa-values.yaml @@ -0,0 +1,25 @@ +controller: + autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 80 + behavior: + scaleUp: + policies: + - type: Percent + value: 900 + periodSeconds: 60 + scaleDown: + stabilizationWindowSeconds: 600 + policies: + - type: Pods + value: 1 + periodSeconds: 600 + +defaultBackend: + autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 2 + targetCPUUtilizationPercentage: 50 diff --git a/kubernetes-ingress/ci/deployment-ingressclass-values.yaml b/kubernetes-ingress/ci/deployment-ingressclass-values.yaml new file mode 100644 index 0000000..6f4f1fc --- /dev/null +++ b/kubernetes-ingress/ci/deployment-ingressclass-values.yaml @@ -0,0 +1,5 @@ +controller: + ingressClass: haproxy + ingressClassResource: + enabled: true + default: true diff --git a/kubernetes-ingress/ci/deployment-ipfamily-values.yaml b/kubernetes-ingress/ci/deployment-ipfamily-values.yaml new file mode 100644 index 0000000..6776d90 --- /dev/null +++ b/kubernetes-ingress/ci/deployment-ipfamily-values.yaml @@ -0,0 +1,4 @@ +controller: + service: + ipFamilies: [IPv4] + ipFamilyPolicy: SingleStack diff --git a/kubernetes-ingress/ci/deployment-kubernetesgateway-values.yaml b/kubernetes-ingress/ci/deployment-kubernetesgateway-values.yaml new file mode 100644 index 0000000..c78bea2 --- /dev/null +++ b/kubernetes-ingress/ci/deployment-kubernetesgateway-values.yaml @@ -0,0 +1,4 @@ +controller: + kubernetesGateway: + enabled: true + gatewayControllerName: haproxy.org/gateway-controller diff --git a/kubernetes-ingress/ci/deployment-nodeport-values.yaml b/kubernetes-ingress/ci/deployment-nodeport-values.yaml new file mode 100644 index 0000000..ffdc47b --- /dev/null +++ b/kubernetes-ingress/ci/deployment-nodeport-values.yaml @@ -0,0 +1,3 @@ +controller: + service: + type: NodePort diff --git a/kubernetes-ingress/ci/deployment-podannotations-templated-values.yaml b/kubernetes-ingress/ci/deployment-podannotations-templated-values.yaml new file mode 100644 index 0000000..9f76a57 --- /dev/null +++ b/kubernetes-ingress/ci/deployment-podannotations-templated-values.yaml @@ -0,0 +1,5 @@ +controller: + podAnnotations: | + my-checksum: {{ $.Values.myCustomVar | toYaml | sha256sum }} +myCustomVar: + FOO: BAR diff --git a/kubernetes-ingress/ci/deployment-podmonitor-values.yaml b/kubernetes-ingress/ci/deployment-podmonitor-values.yaml new file mode 100644 index 0000000..a895bcc --- /dev/null +++ b/kubernetes-ingress/ci/deployment-podmonitor-values.yaml @@ -0,0 +1,3 @@ +controller: + podMonitor: + enabled: true diff --git a/kubernetes-ingress/ci/deployment-privileged-ports.values.yaml b/kubernetes-ingress/ci/deployment-privileged-ports.values.yaml new file mode 100644 index 0000000..b1dc2bb --- /dev/null +++ b/kubernetes-ingress/ci/deployment-privileged-ports.values.yaml @@ -0,0 +1,5 @@ +controller: + containerPort: + http: 80 + https: 443 + stat: 1024 diff --git a/kubernetes-ingress/ci/deployment-publishservice-values.yaml b/kubernetes-ingress/ci/deployment-publishservice-values.yaml new file mode 100644 index 0000000..6d8bf9b --- /dev/null +++ b/kubernetes-ingress/ci/deployment-publishservice-values.yaml @@ -0,0 +1,4 @@ +controller: + kind: DaemonSet + publishService: + enabled: true diff --git a/kubernetes-ingress/ci/deployment-replicacount-unset.yaml b/kubernetes-ingress/ci/deployment-replicacount-unset.yaml new file mode 100644 index 0000000..78ee300 --- /dev/null +++ b/kubernetes-ingress/ci/deployment-replicacount-unset.yaml @@ -0,0 +1,5 @@ +controller: + replicaCount: null + +defaultBackend: + replicaCount: null diff --git a/kubernetes-ingress/ci/deployment-strategy-values.yaml b/kubernetes-ingress/ci/deployment-strategy-values.yaml new file mode 100644 index 0000000..939312a --- /dev/null +++ b/kubernetes-ingress/ci/deployment-strategy-values.yaml @@ -0,0 +1,6 @@ +controller: + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 diff --git a/kubernetes-ingress/ci/deployment-unprivileged-values.yaml b/kubernetes-ingress/ci/deployment-unprivileged-values.yaml new file mode 100644 index 0000000..475f8b3 --- /dev/null +++ b/kubernetes-ingress/ci/deployment-unprivileged-values.yaml @@ -0,0 +1,3 @@ +controller: + unprivileged: true + allowPrivilegedPorts: true diff --git a/kubernetes-ingress/templates/NOTES.txt b/kubernetes-ingress/templates/NOTES.txt new file mode 100644 index 0000000..ba3d01d --- /dev/null +++ b/kubernetes-ingress/templates/NOTES.txt @@ -0,0 +1,109 @@ +HAProxy Kubernetes Ingress Controller has been successfully installed. + +Controller image deployed is: "{{ .Values.controller.image.repository }}:{{ .Values.controller.image.tag | default .Chart.AppVersion }}". +Your controller is of a "{{ .Values.controller.kind }}" kind. Your controller service is running as a "{{ .Values.controller.service.type }}" type. +{{- if .Values.rbac.create}} +RBAC authorization is enabled. +{{- else}} +RBAC authorization is disabled. +{{- end}} +{{- if .Values.controller.ingressClass}} +Controller ingress.class is set to "{{ .Values.controller.ingressClass }}" so make sure to use same annotation for +Ingress resource. +{{- end}} +{{- if .Values.controller.gatewayControllerName}} +Controller Gateway Controller Name is set to "{{ .Values.controller.gatewayControllerName }}" so make sure +that Gateway API CRDs are installed in Kubernetes. +{{- end}} + +Service ports mapped are: +{{- if eq .Values.controller.kind "Deployment" }} +{{- range $key, $value := .Values.controller.containerPort }} + - name: {{ $key }} + containerPort: {{ $value }} + protocol: TCP +{{- end }} +{{- if and (semverCompare ">=1.24.0-0" .Capabilities.KubeVersion.Version) .Values.controller.service.enablePorts.quic }} + - name: quic + containerPort: {{ .Values.controller.containerPort.https }} + protocol: UDP +{{- end }} +{{- range .Values.controller.service.tcpPorts }} + - name: {{ .name | trunc 15 | trimSuffix "-" }} + containerPort: {{ .targetPort }} + protocol: TCP +{{- end }} +{{- end }} +{{- if eq .Values.controller.kind "DaemonSet" }} +{{- $useHostPort := .Values.controller.daemonset.useHostPort -}} +{{- $hostPorts := .Values.controller.daemonset.hostPorts -}} +{{- $hostIP := .Values.controller.daemonset.hostIP -}} +{{- range $key, $value := .Values.controller.containerPort }} + - name: {{ $key }} + containerPort: {{ $value }} + protocol: TCP +{{- if $useHostPort }} + hostPort: {{ index $hostPorts $key | default $value }} +{{- end }} +{{- if $hostIP }} + hostIP: {{ $hostIP }} +{{- end }} +{{- end }} +{{- if and (semverCompare ">=1.24.0-0" .Capabilities.KubeVersion.Version) .Values.controller.service.enablePorts.quic }} + - name: quic + containerPort: {{ .Values.controller.containerPort.https }} + protocol: UDP +{{- if $useHostPort }} + hostPort: {{ .Values.controller.daemonset.hostPorts.https }} +{{- end }} +{{- if $hostIP }} + hostIP: {{ $hostIP }} +{{- end }} +{{- end }} +{{- range .Values.controller.service.tcpPorts }} + - name: {{ .name | trunc 15 | trimSuffix "-" }} + containerPort: {{ .port }} + protocol: TCP +{{- if $useHostPort }} + hostPort: {{ .port }} +{{- end }} +{{- if $hostIP }} + hostIP: {{ $hostIP }} +{{- end }} +{{- end }} +{{- end }} + +Node IP can be found with: + $ kubectl --namespace {{ include "kubernetes-ingress.namespace" . }} get nodes -o jsonpath="{.items[0].status.addresses[1].address}" + +The following ingress resource routes traffic to pods that match the following: + * service name: web + * client's Host header: webdemo.com + * path begins with / + + --- + apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: web-ingress + namespace: default + annotations: + ingress.class: "haproxy" + spec: + rules: + - host: webdemo.com + http: + paths: + - path: / + backend: + serviceName: web + servicePort: 80 + +In case that you are using multi-ingress controller environment, make sure to use ingress.class annotation and match it +with helm chart option controller.ingressClass. + +For more examples and up to date documentation, please visit: + * Helm chart documentation: https://github.com/haproxytech/helm-charts/tree/main/kubernetes-ingress + * Controller documentation: https://www.haproxy.com/documentation/kubernetes/latest/ + * Annotation reference: https://github.com/haproxytech/kubernetes-ingress/tree/master/documentation + * Image parameters reference: https://github.com/haproxytech/kubernetes-ingress/blob/master/documentation/controller.md diff --git a/kubernetes-ingress/templates/_helpers.tpl b/kubernetes-ingress/templates/_helpers.tpl new file mode 100644 index 0000000..d6e0d92 --- /dev/null +++ b/kubernetes-ingress/templates/_helpers.tpl @@ -0,0 +1,259 @@ +{{/* +Copyright 2019 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{/* +Expand the name of the chart. +*/}} +{{- define "kubernetes-ingress.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Allow the release namespace to be overridden for multi-namespace deployments in combined charts +*/}} +{{- define "kubernetes-ingress.namespace" -}} +{{- if .Values.namespaceOverride -}} +{{- .Values.namespaceOverride -}} +{{- else -}} +{{- .Release.Namespace -}} +{{- end -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "kubernetes-ingress.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "kubernetes-ingress.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create HAProxy Ingress Chart labels +*/}} +{{- define "kubernetes-ingress.helmChartLabels" -}} +helm.sh/chart: {{ include "kubernetes-ingress.chart" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Create HAProxy Ingress Selector labels +*/}} +{{- define "kubernetes-ingress.selectorLabels" -}} +app.kubernetes.io/name: {{ include "kubernetes-ingress.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create HAProxy Ingress labels +*/}} +{{- define "kubernetes-ingress.labels" -}} +{{ include "kubernetes-ingress.selectorLabels" . }} +{{ include "kubernetes-ingress.helmChartLabels" . }} +{{- end }} + +{{/* +Create CRD Job selector labels +*/}} +{{- define "kubernetes-ingress.crdJobSelectorLabels" -}} +app.kubernetes.io/name: {{ include "kubernetes-ingress.serviceProxyName" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create CRD Job labels +*/}} +{{- define "kubernetes-ingress.crdJobLabels" -}} +{{ include "kubernetes-ingress.crdJobSelectorLabels" . }} +{{ include "kubernetes-ingress.helmChartLabels" . }} +{{- end }} + +{{/* +Create Service Proxy selector labels +*/}} +{{- define "kubernetes-ingress.serviceProxySelectorLabels" -}} +app.kubernetes.io/name: {{ include "kubernetes-ingress.serviceProxyName" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create Service Proxy labels +*/}} +{{- define "kubernetes-ingress.serviceProxyLabels" -}} +{{ include "kubernetes-ingress.serviceProxySelectorLabels" . }} +{{ include "kubernetes-ingress.helmChartLabels" . }} +{{- end }} + +{{/* +Encode an imagePullSecret string. +*/}} +{{- define "kubernetes-ingress.imagePullSecret" }} +{{- printf "{\"auths\": {\"%s\": {\"auth\": \"%s\"}}}" .Values.controller.imageCredentials.registry (printf "%s:%s" .Values.controller.imageCredentials.username .Values.controller.imageCredentials.password | b64enc) | b64enc }} +{{- end }} + +{{/* +Encode an imagePullSecret string for the default backend. +*/}} +{{- define "kubernetes-ingress.defaultBackend.imagePullSecret" }} +{{- printf "{\"auths\": {\"%s\": {\"auth\": \"%s\"}}}" .Values.defaultBackend.imageCredentials.registry (printf "%s:%s" .Values.defaultBackend.imageCredentials.username .Values.defaultBackend.imageCredentials.password | b64enc) | b64enc }} +{{- end }} + +{{/* +Generate default certificate for HAProxy. +*/}} +{{- define "kubernetes-ingress.gen-certs" -}} +{{- $ca := genCA "kubernetes-ingress-ca" 365 -}} +{{- $cn := printf "%s.%s" .Release.Name (include "kubernetes-ingress.namespace" .) -}} +{{- $cert := genSignedCert $cn nil nil 365 $ca -}} +tls.crt: {{ $cert.Cert | b64enc }} +tls.key: {{ $cert.Key | b64enc }} +{{- end -}} + +{{/* +Create the name of the controller service account to use. +*/}} +{{- define "kubernetes-ingress.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} + {{ default (include "kubernetes-ingress.fullname" .) .Values.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.serviceAccount.name }} +{{- end -}} +{{- end -}} + +{{/* +Create the name of the backend service account to use - only used when podsecuritypolicy is also enabled +*/}} +{{- define "kubernetes-ingress.defaultBackend.serviceAccountName" -}} +{{- if or .Values.serviceAccount.create .Values.defaultBackend.serviceAccount.create -}} + {{ default (printf "%s-%s" (include "kubernetes-ingress.fullname" .) .Values.defaultBackend.name) .Values.defaultBackend.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.defaultBackend.serviceAccount.name }} +{{- end -}} +{{- end -}} + +{{/* +Create a default fully qualified default backend name. +*/}} +{{- define "kubernetes-ingress.defaultBackend.fullname" -}} +{{- printf "%s-%s" (include "kubernetes-ingress.fullname" .) .Values.defaultBackend.name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified default cert secret name. +*/}} +{{- define "kubernetes-ingress.defaultTLSSecret.fullname" -}} +{{- printf "%s-%s" (include "kubernetes-ingress.fullname" .) "default-cert" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Construct the path for the publish-service. +By default this will use the / matching the controller's service name. +Users can provide an override for an explicit service they want to use via `.Values.controller.publishService.pathOverride` +*/}} +{{- define "kubernetes-ingress.publishServicePath" -}} +{{- $defServicePath := printf "%s/%s" (include "kubernetes-ingress.namespace" .) (include "kubernetes-ingress.fullname" .) -}} +{{- $servicePath := default $defServicePath .Values.controller.publishService.pathOverride }} +{{- print $servicePath | trimSuffix "-" -}} +{{- end -}} + +{{/* +Construct the syslog-server annotation +*/}} +{{- define "kubernetes-ingress.syslogServer" -}} +{{- range $key, $val := .Values.controller.logging.traffic -}} +{{- printf "%s:%s, " $key $val }} +{{- end -}} +{{- end -}} + +{{/* +Render controller pod sysctls. + +Input: .Values.controller.sysctls (map[string]string) +Also keeps the existing allowPrivilegedPorts behaviour by adding +net.ipv4.ip_unprivileged_port_start=0 unless explicitly overridden via controller.sysctls. +*/}} +{{- define "kubernetes-ingress.controller.sysctls" -}} +{{- $sysctls := .Values.controller.sysctls | default dict -}} +{{- $keys := keys $sysctls | sortAlpha -}} +{{- $needPrivPorts := and .Values.controller.unprivileged .Values.controller.allowPrivilegedPorts (not (hasKey $sysctls "net.ipv4.ip_unprivileged_port_start")) -}} +{{- if or (gt (len $keys) 0) $needPrivPorts -}} +sysctls: +{{- range $name := $keys }} + - name: {{ $name }} + value: {{ index $sysctls $name | quote }} +{{- end }} +{{- if $needPrivPorts }} + - name: net.ipv4.ip_unprivileged_port_start + value: "0" +{{- end }} +{{- end -}} +{{- end -}} + +{{/* +Create a default fully qualified ServiceMonitor name. +*/}} +{{- define "kubernetes-ingress.serviceMonitorName" -}} +{{- default (include "kubernetes-ingress.fullname" .) .Values.controller.serviceMonitor.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified PodMonitor name. +*/}} +{{- define "kubernetes-ingress.podMonitorName" -}} +{{- default (include "kubernetes-ingress.fullname" .) .Values.controller.podMonitor.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a FQDN for the Service metrics. +*/}} +{{- define "kubernetes-ingress.serviceMetricsName" -}} +{{- printf "%s-%s" (include "kubernetes-ingress.fullname" . | trunc 56 | trimSuffix "-") "metrics" }} +{{- end -}} + +{{/* +Create a default fully qualified unique CRD job name. +*/}} +{{- define "kubernetes-ingress.crdjob.fullname" -}} +{{- printf "%s-%s-%d" (include "kubernetes-ingress.fullname" .) "crdjob" .Release.Revision | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a FQDN for the proxy pods. +*/}} +{{- define "kubernetes-ingress.serviceProxyName" -}} +{{- printf "%s-%s" (include "kubernetes-ingress.fullname" . | trunc 58 | trimSuffix "-") "proxy" }} +{{- end -}} + +{{/* vim: set filetype=mustache: */}} diff --git a/kubernetes-ingress/templates/clusterrole.yaml b/kubernetes-ingress/templates/clusterrole.yaml new file mode 100644 index 0000000..88d79f1 --- /dev/null +++ b/kubernetes-ingress/templates/clusterrole.yaml @@ -0,0 +1,158 @@ +{{/* +Copyright 2019 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.rbac.create -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "kubernetes-ingress.fullname" . }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} +rules: +- apiGroups: + - "" + resources: + - configmaps + - endpoints + - nodes + - pods + - services + - namespaces + - events + - serviceaccounts + verbs: + - get + - list + - watch +{{- if and (eq .Values.controller.sync.mode "fetch") (eq .Values.controller.sync.fetchParams.source "proxy") }} +- apiGroups: + - "" + resources: + - services + - pods + verbs: + - update +{{- end }} +- apiGroups: + - "extensions" + - "networking.k8s.io" + resources: + - ingresses + - ingresses/status + - ingressclasses + verbs: + - get + - list + - watch +- apiGroups: + - "extensions" + - "networking.k8s.io" + resources: + - ingresses/status + verbs: + - update +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch + - create + - patch + - update +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - get + - list + - watch +- apiGroups: + - core.haproxy.org + resources: + - '*' + verbs: + - get + - list + - watch + - update +- apiGroups: + - ingress.v1.haproxy.org + - ingress.v1.haproxy.com + - ingress.v3.haproxy.org + - ingress.v3.haproxy.com + resources: + - "*" + verbs: + - get + - list + - watch + - update +- apiGroups: + - "apiextensions.k8s.io" + resources: + - customresourcedefinitions + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +{{- if .Values.controller.kubernetesGateway.enabled }} +- apiGroups: + - "gateway.networking.k8s.io" + resources: + - referencegrants + - gateways + - gatewayclasses + - tcproutes + verbs: + - get + - list + - watch +- apiGroups: + - "gateway.networking.k8s.io" + resources: + - gatewayclasses/status + - gateways/status + - tcproutes/status + verbs: + - update +{{- end }} +- apiGroups: + - "apps" + resources: + - replicasets + - deployments + - daemonsets + verbs: + - get + - list + - watch +{{- if and (eq .Values.controller.sync.mode "fetch") (eq .Values.controller.sync.fetchParams.source "proxy") }} +- apiGroups: + - "coordination.k8s.io" + resources: + - leases + verbs: + - "*" +{{- end }} +{{- end -}} diff --git a/kubernetes-ingress/templates/clusterrolebinding.yaml b/kubernetes-ingress/templates/clusterrolebinding.yaml new file mode 100644 index 0000000..bf1a23b --- /dev/null +++ b/kubernetes-ingress/templates/clusterrolebinding.yaml @@ -0,0 +1,33 @@ +{{/* +Copyright 2019 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.rbac.create -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "kubernetes-ingress.fullname" . }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "kubernetes-ingress.fullname" . }} +subjects: +- kind: ServiceAccount + name: {{ include "kubernetes-ingress.serviceAccountName" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} +{{- end -}} + diff --git a/kubernetes-ingress/templates/controller-configmap.yaml b/kubernetes-ingress/templates/controller-configmap.yaml new file mode 100644 index 0000000..e09a786 --- /dev/null +++ b/kubernetes-ingress/templates/controller-configmap.yaml @@ -0,0 +1,38 @@ +{{/* +Copyright 2019 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "kubernetes-ingress.fullname" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} +{{- if .Values.controller.configAnnotations }} + annotations: +{{ toYaml .Values.controller.configAnnotations | indent 4 }} +{{- end }} +data: +{{- if .Values.controller.logging.traffic }} + syslog-server: {{ include "kubernetes-ingress.syslogServer" . }} +{{- end }} +{{- if .Values.controller.config }} +{{- if eq "string" (printf "%T" .Values.controller.config) }} +{{ tpl .Values.controller.config . | indent 2 }} +{{- else }} +{{ toYaml .Values.controller.config | indent 2 }} +{{- end }} +{{- end }} diff --git a/kubernetes-ingress/templates/controller-crdjob.yaml b/kubernetes-ingress/templates/controller-crdjob.yaml new file mode 100644 index 0000000..0f39429 --- /dev/null +++ b/kubernetes-ingress/templates/controller-crdjob.yaml @@ -0,0 +1,112 @@ +{{/* +Copyright 2023 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "kubernetes-ingress.crdjob.fullname" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} + labels: + {{- include "kubernetes-ingress.crdJobLabels" . | nindent 4 }} + annotations: + argocd.argoproj.io/hook: Sync + argocd.argoproj.io/hook-delete-policy: HookSucceeded + helm.sh/hook: post-install,pre-upgrade + helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded + {{- with .Values.controller.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: +{{- if or (.Capabilities.APIVersions.Has "batch/v1alpha1") (semverCompare ">=1.23.0-0" .Capabilities.KubeVersion.Version) }} +{{- if .Values.crdjob.ttlSecondsAfterFinished }} + ttlSecondsAfterFinished: {{ .Values.crdjob.ttlSecondsAfterFinished }} +{{- end }} +{{- end }} + backoffLimit: 0 + template: + metadata: + labels: + {{- include "kubernetes-ingress.crdJobSelectorLabels" . | nindent 8 }} + {{- if .Values.controller.podLabels }} +{{ toYaml .Values.controller.podLabels | indent 8 }} + {{- end }} + {{- if .Values.crdjob.podAnnotations }} + annotations: +{{- if eq "string" (printf "%T" .Values.crdjob.podAnnotations) }} +{{ tpl .Values.crdjob.podAnnotations . | indent 8 }} +{{- else }} +{{ toYaml .Values.crdjob.podAnnotations | indent 8 }} +{{- end }} + {{- end }} + spec: + restartPolicy: Never + serviceAccountName: {{ include "kubernetes-ingress.serviceAccountName" . }} +{{- if .Values.controller.imageCredentials.registry }} + imagePullSecrets: + - name: {{ include "kubernetes-ingress.fullname" . }} +{{- else if .Values.controller.existingImagePullSecret }} + imagePullSecrets: + - name: {{ .Values.controller.existingImagePullSecret }} +{{- end }} +{{- if .Values.controller.priorityClassName }} + priorityClassName: {{ .Values.controller.priorityClassName }} +{{- end }} +{{- if .Values.controller.runtimeClassName }} + runtimeClassName: {{ .Values.controller.runtimeClassName }} +{{- end }} +{{- if .Values.controller.unprivileged }} + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 +{{- end }} + containers: + - name: crd + image: "{{ .Values.controller.image.repository }}:{{ .Values.controller.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.controller.image.pullPolicy }} + command: + - /haproxy-ingress-controller + - --job-check-crd + {{- if .Values.controller.unprivileged }} + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: {{ .Values.controller.allowPrivilegeEscalation }} + capabilities: + drop: + - ALL + add: + - NET_BIND_SERVICE + {{- if .Values.controller.enableRuntimeDefaultSeccompProfile }} + seccompProfile: + type: RuntimeDefault + {{- end }} + resources: + {{- toYaml .Values.crdjob.resources | nindent 12 }} + {{- end }} + {{- with .Values.crdjob.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.crdjob.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.crdjob.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/kubernetes-ingress/templates/controller-daemonset.yaml b/kubernetes-ingress/templates/controller-daemonset.yaml new file mode 100644 index 0000000..5cbdfef --- /dev/null +++ b/kubernetes-ingress/templates/controller-daemonset.yaml @@ -0,0 +1,300 @@ +{{/* +Copyright 2019 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if eq .Values.controller.kind "DaemonSet" }} +{{- $useHostNetwork := .Values.controller.daemonset.useHostNetwork -}} +{{- $useHostPort := .Values.controller.daemonset.useHostPort -}} +{{- $hostPorts := .Values.controller.daemonset.hostPorts -}} +{{- $hostIP := .Values.controller.daemonset.hostIP -}} +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: {{ include "kubernetes-ingress.fullname" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} + {{- with .Values.controller.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} + {{- if .Values.controller.extraLabels }} +{{ toYaml .Values.controller.extraLabels | indent 4 }} + {{- end }} +spec: + minReadySeconds: {{ .Values.controller.minReadySeconds }} + {{- with .Values.controller.strategy }} + updateStrategy: + {{- toYaml . | nindent 4 }} + {{- end }} + selector: + matchLabels: + {{- include "kubernetes-ingress.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "kubernetes-ingress.selectorLabels" . | nindent 8 }} + {{- if .Values.controller.podLabels }} +{{ toYaml .Values.controller.podLabels | indent 8 }} + {{- end }} + {{- if .Values.controller.podAnnotations }} + annotations: +{{- if eq "string" (printf "%T" .Values.controller.podAnnotations) }} +{{ tpl .Values.controller.podAnnotations . | indent 8 }} +{{- else }} +{{ toYaml .Values.controller.podAnnotations | indent 8 }} +{{- end }} + {{- end }} + spec: + enableServiceLinks: {{ .Values.controller.enableServiceLinks }} + serviceAccountName: {{ include "kubernetes-ingress.serviceAccountName" . }} +{{- if hasKey .Values.serviceAccount "automountServiceAccountToken" }} + automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} +{{- end }} + terminationGracePeriodSeconds: {{ .Values.controller.terminationGracePeriodSeconds }} + {{- if $useHostNetwork }} + hostNetwork: true + {{- end }} +{{- if .Values.controller.dnsConfig }} + dnsConfig: +{{ toYaml .Values.controller.dnsConfig | indent 8 }} +{{- end }} + dnsPolicy: {{ .Values.controller.dnsPolicy }} +{{- if .Values.controller.imageCredentials.registry }} + imagePullSecrets: + - name: {{ include "kubernetes-ingress.fullname" . }} +{{- else if .Values.controller.existingImagePullSecret }} + imagePullSecrets: + - name: {{ .Values.controller.existingImagePullSecret }} +{{- end }} +{{- if .Values.controller.priorityClassName }} + priorityClassName: {{ .Values.controller.priorityClassName }} +{{- end }} +{{- if .Values.controller.runtimeClassName }} + runtimeClassName: {{ .Values.controller.runtimeClassName }} +{{- end }} +{{- if or .Values.controller.unprivileged (gt (len (.Values.controller.sysctls | default dict)) 0) }} + securityContext: +{{- if .Values.controller.unprivileged }} + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 +{{- end }} +{{ include "kubernetes-ingress.controller.sysctls" . | nindent 8 }} +{{- end }} + containers: + - name: {{ include "kubernetes-ingress.name" . }}-{{ .Values.controller.name }} + image: "{{ .Values.controller.image.repository }}:{{ .Values.controller.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.controller.image.pullPolicy }} + args: +{{- if .Values.controller.defaultTLSSecret.enabled -}} +{{- if and .Values.controller.defaultTLSSecret.secret .Values.controller.defaultTLSSecret.secretNamespace }} + - --default-ssl-certificate={{ tpl .Values.controller.defaultTLSSecret.secretNamespace . }}/{{ .Values.controller.defaultTLSSecret.secret }} +{{- else }} + - --default-ssl-certificate={{ include "kubernetes-ingress.namespace" . }}/{{ include "kubernetes-ingress.defaultTLSSecret.fullname" . }} +{{- end }} +{{- end }} + - --configmap={{ include "kubernetes-ingress.namespace" . }}/{{ include "kubernetes-ingress.fullname" . }} + - --http-bind-port={{ .Values.controller.containerPort.http }} + - --https-bind-port={{ .Values.controller.containerPort.https }} +{{- if and (semverCompare ">=1.24.0-0" .Capabilities.KubeVersion.Version) .Values.controller.service.enablePorts.quic }} + - --quic-bind-port={{ .Values.controller.containerPort.https }} + - --quic-announce-port={{ .Values.controller.service.ports.https }} +{{- end }} +{{- if .Values.controller.ingressClass }} + - --ingress.class={{ .Values.controller.ingressClass }} +{{- end }} +{{- if and .Values.controller.kubernetesGateway.enabled .Values.controller.kubernetesGateway.gatewayControllerName }} + - --gateway-controller-name={{ .Values.controller.kubernetesGateway.gatewayControllerName }} +{{- end }} +{{- if .Values.controller.publishService.enabled }} + - --publish-service={{ include "kubernetes-ingress.publishServicePath" . }} +{{- end }} +{{- if .Values.controller.logging.level }} + - --log={{ .Values.controller.logging.level }} +{{- end }} +{{- if .Values.controller.service.enablePorts.admin }} + - --prometheus + - --pprof +{{- end }} +{{- range .Values.controller.extraArgs }} + - {{ . }} +{{- end }} + {{- if .Values.controller.unprivileged }} + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: {{ .Values.controller.allowPrivilegeEscalation }} + capabilities: + drop: + - ALL + add: + - NET_BIND_SERVICE + {{- if .Values.controller.enableRuntimeDefaultSeccompProfile }} + seccompProfile: + type: RuntimeDefault + {{- end }} + {{- end }} + ports: + {{- range $key, $value := .Values.controller.containerPort }} + - name: {{ $key }} + containerPort: {{ $value }} + protocol: TCP + {{- if and $useHostPort (index $hostPorts $key) }} + hostPort: {{ index $hostPorts $key }} + {{- end }} + {{- if $hostIP }} + hostIP: {{ $hostIP }} + {{- end }} + {{- end }} + {{- if and (semverCompare ">=1.24.0-0" .Capabilities.KubeVersion.Version) .Values.controller.service.enablePorts.quic }} + - name: quic + containerPort: {{ .Values.controller.containerPort.https }} + protocol: UDP + {{- if $useHostPort }} + hostPort: {{ .Values.controller.daemonset.hostPorts.https }} + {{- end }} + {{- if $hostIP }} + hostIP: {{ $hostIP }} + {{- end }} + {{- end }} + {{- range .Values.controller.service.tcpPorts }} + - name: {{ .name | trunc 15 | trimSuffix "-" }} + containerPort: {{ .port }} + protocol: TCP + {{- if $useHostPort }} + hostPort: {{ .port }} + {{- end }} + {{- if $hostIP }} + hostIP: {{ $hostIP }} + {{- end }} + {{- end }} + {{- with .Values.controller.livenessProbe }} + livenessProbe: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + {{- with .Values.controller.readinessProbe }} + readinessProbe: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + {{- with .Values.controller.startupProbe }} + startupProbe: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + env: + {{- if .Values.aws.licenseConfigSecretName }} + - name: AWS_WEB_IDENTITY_REFRESH_TOKEN_FILE + value: "/var/run/secrets/product-license/license_token" + - name: AWS_ROLE_ARN + valueFrom: + secretKeyRef: + name: {{ .Values.aws.licenseConfigSecretName }} + key: iam_role + {{- end }} + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + {{- if .Values.controller.extraEnvs -}} + {{- toYaml .Values.controller.extraEnvs | nindent 10 }} + {{- end }} + {{- with .Values.controller.extraEnvFrom }} + envFrom: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.controller.resources | nindent 12 }} + {{- if .Values.controller.lifecycle }} + lifecycle: + {{- if eq "string" (printf "%T" .Values.controller.lifecycle) }} +{{ tpl .Values.controller.lifecycle . | indent 12 }} + {{- else }} +{{ toYaml .Values.controller.lifecycle | indent 12 }} + {{- end }} + {{- end }} + volumeMounts: + - name: tmp + mountPath: /tmp + subPath: tmp + - name: tmp + mountPath: /run + subPath: run + {{- if .Values.aws.licenseConfigSecretName }} + - name: aws-product-license + readOnly: true + mountPath: /var/run/secrets/product-license + {{- end }} + {{- if eq "string" (printf "%T" .Values.controller.extraVolumeMounts) }} +{{ tpl .Values.controller.extraVolumeMounts . | indent 12 }} + {{- else if gt (len .Values.controller.extraVolumeMounts) 0 }} +{{ toYaml .Values.controller.extraVolumeMounts | indent 12 }} + {{- end }} + {{- if .Values.controller.extraContainers }} + {{- if eq "string" (printf "%T" .Values.controller.extraContainers) }} +{{ tpl .Values.controller.extraContainers . | indent 8 }} + {{- else }} +{{ toYaml .Values.controller.extraContainers | indent 8 }} + {{- end }} + {{- end }} + volumes: + - name: tmp + {{- if semverCompare ">=1.21.0-0" .Capabilities.KubeVersion.Version }} + emptyDir: + medium: Memory + sizeLimit: 64Mi + {{- else }} + emptyDir: {} + {{- end }} + {{- if .Values.aws.licenseConfigSecretName }} + - name: aws-product-license + secret: + secretName: {{ .Values.aws.licenseConfigSecretName }} + optional: true + {{- end }} + {{- if eq "string" (printf "%T" .Values.controller.extraVolumes) }} +{{ tpl .Values.controller.extraVolumes . | indent 8 }} + {{- else if gt (len .Values.controller.extraVolumes) 0 }} +{{ toYaml .Values.controller.extraVolumes | indent 8 }} + {{- end }} + {{- if .Values.controller.initContainers }} + initContainers: + {{- if eq "string" (printf "%T" .Values.controller.initContainers) }} +{{ tpl .Values.controller.initContainers . | indent 8 }} + {{- else }} +{{ toYaml .Values.controller.initContainers | indent 8 }} + {{- end }} + {{- end }} + {{- with .Values.controller.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/kubernetes-ingress/templates/controller-defaultcertsecret.yaml b/kubernetes-ingress/templates/controller-defaultcertsecret.yaml new file mode 100644 index 0000000..587ac30 --- /dev/null +++ b/kubernetes-ingress/templates/controller-defaultcertsecret.yaml @@ -0,0 +1,33 @@ +{{/* +Copyright 2019 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.controller.defaultTLSSecret.enabled }} +{{- if and (not .Values.controller.defaultTLSSecret.secret) .Values.controller.defaultTLSSecret.secretNamespace }} +apiVersion: v1 +kind: Secret +type: kubernetes.io/tls +metadata: + name: {{ include "kubernetes-ingress.defaultTLSSecret.fullname" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": "pre-install" + "helm.sh/hook-delete-policy": "before-hook-creation" +data: +{{ ( include "kubernetes-ingress.gen-certs" . ) | indent 2 }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/kubernetes-ingress/templates/controller-deployment.yaml b/kubernetes-ingress/templates/controller-deployment.yaml new file mode 100644 index 0000000..b43df85 --- /dev/null +++ b/kubernetes-ingress/templates/controller-deployment.yaml @@ -0,0 +1,318 @@ +{{/* +Copyright 2019 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if eq .Values.controller.kind "Deployment" }} +{{- $useHostNetwork := .Values.controller.deployment.useHostNetwork -}} +{{- $useHostPort := .Values.controller.deployment.useHostPort -}} +{{- $hostPorts := .Values.controller.deployment.hostPorts -}} +{{- $hostIP := .Values.controller.deployment.hostIP -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "kubernetes-ingress.fullname" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} + {{- with .Values.controller.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} + {{- if .Values.controller.extraLabels }} +{{ toYaml .Values.controller.extraLabels | indent 4 }} + {{- end }} +spec: + {{- if and (not .Values.controller.autoscaling.enabled) (not .Values.controller.keda.enabled) }} + replicas: {{ .Values.controller.replicaCount }} + {{- end }} + minReadySeconds: {{ .Values.controller.minReadySeconds }} + selector: + matchLabels: + {{- include "kubernetes-ingress.selectorLabels" . | nindent 6 }} + {{- with .Values.controller.strategy }} + strategy: + {{- toYaml . | nindent 4 }} + {{- end }} + template: + metadata: + labels: + {{- include "kubernetes-ingress.selectorLabels" . | nindent 8 }} + {{- if .Values.controller.podLabels }} +{{ toYaml .Values.controller.podLabels | indent 8 }} + {{- end }} + {{- if .Values.controller.podAnnotations }} + annotations: +{{- if eq "string" (printf "%T" .Values.controller.podAnnotations) }} +{{ tpl .Values.controller.podAnnotations . | indent 8 }} +{{- else }} +{{ toYaml .Values.controller.podAnnotations | indent 8 }} +{{- end }} + {{- end }} + spec: + enableServiceLinks: {{ .Values.controller.enableServiceLinks }} + serviceAccountName: {{ include "kubernetes-ingress.serviceAccountName" . }} +{{- if hasKey .Values.serviceAccount "automountServiceAccountToken" }} + automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} +{{- end }} + terminationGracePeriodSeconds: {{ .Values.controller.terminationGracePeriodSeconds }} + {{- if $useHostNetwork }} + hostNetwork: true + {{- end }} +{{- with .Values.controller.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} +{{- end }} +{{- if .Values.controller.dnsConfig }} + dnsConfig: +{{ toYaml .Values.controller.dnsConfig | indent 8 }} +{{- end }} + dnsPolicy: {{ .Values.controller.dnsPolicy }} +{{- if .Values.controller.imageCredentials.registry }} + imagePullSecrets: + - name: {{ include "kubernetes-ingress.fullname" . }} +{{- else if .Values.controller.existingImagePullSecret }} + imagePullSecrets: + - name: {{ .Values.controller.existingImagePullSecret }} +{{- end }} +{{- if .Values.controller.priorityClassName }} + priorityClassName: {{ .Values.controller.priorityClassName }} +{{- end }} +{{- if .Values.controller.runtimeClassName }} + runtimeClassName: {{ .Values.controller.runtimeClassName }} +{{- end }} +{{- if or .Values.controller.unprivileged (gt (len (.Values.controller.sysctls | default dict)) 0) }} + securityContext: +{{- if .Values.controller.unprivileged }} + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 +{{- end }} +{{ include "kubernetes-ingress.controller.sysctls" . | nindent 8 }} +{{- end }} + containers: + - name: {{ include "kubernetes-ingress.name" . }}-{{ .Values.controller.name }} + image: "{{ .Values.controller.image.repository }}:{{ .Values.controller.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.controller.image.pullPolicy }} + args: +{{- if .Values.controller.defaultTLSSecret.enabled -}} +{{- if and .Values.controller.defaultTLSSecret.secret .Values.controller.defaultTLSSecret.secretNamespace }} + - --default-ssl-certificate={{ tpl .Values.controller.defaultTLSSecret.secretNamespace . }}/{{ .Values.controller.defaultTLSSecret.secret }} +{{- else }} + - --default-ssl-certificate={{ include "kubernetes-ingress.namespace" . }}/{{ include "kubernetes-ingress.defaultTLSSecret.fullname" . }} +{{- end }} +{{- end }} + - --configmap={{ include "kubernetes-ingress.namespace" . }}/{{ include "kubernetes-ingress.fullname" . }} + - --http-bind-port={{ .Values.controller.containerPort.http }} + - --https-bind-port={{ .Values.controller.containerPort.https }} +{{- if and (semverCompare ">=1.24.0-0" .Capabilities.KubeVersion.Version) .Values.controller.service.enablePorts.quic }} + - --quic-bind-port={{ .Values.controller.containerPort.https }} + - --quic-announce-port={{ .Values.controller.service.ports.https }} +{{- end }} +{{- if .Values.controller.ingressClass }} + - --ingress.class={{ .Values.controller.ingressClass }} +{{- end }} +{{- if and .Values.controller.kubernetesGateway.enabled .Values.controller.kubernetesGateway.gatewayControllerName }} + - --gateway-controller-name={{ .Values.controller.kubernetesGateway.gatewayControllerName }} +{{- end }} +{{- if .Values.controller.publishService.enabled }} + - --publish-service={{ include "kubernetes-ingress.publishServicePath" . }} +{{- end }} +{{- if .Values.controller.logging.level }} + - --log={{ .Values.controller.logging.level }} +{{- end }} +{{- if .Values.controller.service.enablePorts.admin }} + - --prometheus + - --pprof +{{- end }} +{{- if eq .Values.controller.sync.mode "fetch" }} + {{- if .Values.controller.sync.fetchParams.period }} + - --proxy-k8s-fetch-period={{ .Values.controller.sync.fetchParams.period }} + {{- end }} + {{- if eq .Values.controller.sync.fetchParams.source "k8s" }} + - --k8s-api-sync-type=k8s + {{- else if eq .Values.controller.sync.fetchParams.source "proxy" }} + - --k8s-api-sync-type=proxy + - --proxy-svc-label-selector={{ .Values.controller.sync.proxyParams.proxySvcLabelSelector }} + {{- end }} +{{- end }} +{{- range .Values.controller.extraArgs }} + - {{ . }} +{{- end }} + {{- if .Values.controller.unprivileged }} + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: {{ .Values.controller.allowPrivilegeEscalation }} + capabilities: + drop: + - ALL + add: + - NET_BIND_SERVICE + {{- if .Values.controller.enableRuntimeDefaultSeccompProfile }} + seccompProfile: + type: RuntimeDefault + {{- end }} + {{- end }} + ports: + {{- range $key, $value := .Values.controller.containerPort }} + - name: {{ $key }} + containerPort: {{ $value }} + protocol: TCP + {{- if and $useHostPort (index $hostPorts $key) }} + hostPort: {{ index $hostPorts $key }} + {{- end }} + {{- if $hostIP }} + hostIP: {{ $hostIP }} + {{- end }} + {{- end }} + {{- if and (semverCompare ">=1.24.0-0" .Capabilities.KubeVersion.Version) .Values.controller.service.enablePorts.quic }} + - name: quic + containerPort: {{ .Values.controller.containerPort.https }} + protocol: UDP + {{- if $useHostPort }} + hostPort: {{ .Values.controller.deployment.hostPorts.https }} + {{- end }} + {{- if $hostIP }} + hostIP: {{ $hostIP }} + {{- end }} + {{- end }} + {{- range .Values.controller.service.tcpPorts }} + - name: {{ .name | trunc 15 | trimSuffix "-" }} + containerPort: {{ .targetPort }} + protocol: TCP + {{- if $useHostPort }} + hostPort: {{ .port }} + {{- end }} + {{- if $hostIP }} + hostIP: {{ $hostIP }} + {{- end }} + {{- end }} + {{- with .Values.controller.livenessProbe }} + livenessProbe: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + {{- with .Values.controller.readinessProbe }} + readinessProbe: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + {{- with .Values.controller.startupProbe }} + startupProbe: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + env: + {{- if .Values.aws.licenseConfigSecretName }} + - name: AWS_WEB_IDENTITY_REFRESH_TOKEN_FILE + value: "/var/run/secrets/product-license/license_token" + - name: AWS_ROLE_ARN + valueFrom: + secretKeyRef: + name: {{ .Values.aws.licenseConfigSecretName }} + key: iam_role + {{- end }} + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + {{- if .Values.controller.extraEnvs -}} + {{- toYaml .Values.controller.extraEnvs | nindent 10 }} + {{- end }} + {{- with .Values.controller.extraEnvFrom }} + envFrom: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.controller.resources | nindent 12 }} + {{- if .Values.controller.lifecycle }} + lifecycle: + {{- if eq "string" (printf "%T" .Values.controller.lifecycle) }} +{{ tpl .Values.controller.lifecycle . | indent 12 }} + {{- else }} +{{ toYaml .Values.controller.lifecycle | indent 12 }} + {{- end }} + {{- end }} + volumeMounts: + - name: tmp + mountPath: /tmp + subPath: tmp + - name: tmp + mountPath: /run + subPath: run + {{- if .Values.aws.licenseConfigSecretName }} + - name: aws-product-license + readOnly: true + mountPath: /var/run/secrets/product-license + {{- end }} + {{- if eq "string" (printf "%T" .Values.controller.extraVolumeMounts) }} +{{ tpl .Values.controller.extraVolumeMounts . | indent 12 }} + {{- else if gt (len .Values.controller.extraVolumeMounts) 0 }} +{{ toYaml .Values.controller.extraVolumeMounts | indent 12 }} + {{- end }} + {{- if .Values.controller.extraContainers }} + {{- if eq "string" (printf "%T" .Values.controller.extraContainers) }} +{{ tpl .Values.controller.extraContainers . | indent 8 }} + {{- else }} +{{ toYaml .Values.controller.extraContainers | indent 8 }} + {{- end }} + {{- end }} + volumes: + - name: tmp + {{- if semverCompare ">=1.21.0-0" .Capabilities.KubeVersion.Version }} + emptyDir: + medium: Memory + sizeLimit: 64Mi + {{- else }} + emptyDir: {} + {{- end }} + {{- if .Values.aws.licenseConfigSecretName }} + - name: aws-product-license + secret: + secretName: {{ .Values.aws.licenseConfigSecretName }} + optional: true + {{- end }} + {{- if eq "string" (printf "%T" .Values.controller.extraVolumes) }} +{{ tpl .Values.controller.extraVolumes . | indent 8 }} + {{- else if gt (len .Values.controller.extraVolumes) 0 }} +{{ toYaml .Values.controller.extraVolumes | indent 8 }} + {{- end }} + {{- if .Values.controller.initContainers }} + initContainers: + {{- if eq "string" (printf "%T" .Values.controller.initContainers) }} +{{ tpl .Values.controller.initContainers . | indent 8 }} + {{- else }} +{{ toYaml .Values.controller.initContainers | indent 8 }} + {{- end }} + {{- end }} + {{- with .Values.controller.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/kubernetes-ingress/templates/controller-hpa.yaml b/kubernetes-ingress/templates/controller-hpa.yaml new file mode 100644 index 0000000..8657646 --- /dev/null +++ b/kubernetes-ingress/templates/controller-hpa.yaml @@ -0,0 +1,67 @@ +{{/* +Copyright 2020 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and (eq .Values.controller.kind "Deployment") .Values.controller.autoscaling.enabled }} +{{- if not .Values.controller.keda.enabled }} +{{- if or (.Capabilities.APIVersions.Has "autoscaling/v2") (semverCompare ">=1.23.0-0" .Capabilities.KubeVersion.Version) }} +apiVersion: autoscaling/v2 +{{- else if .Capabilities.APIVersions.Has "autoscaling/v2beta2" }} +apiVersion: autoscaling/v2beta2 +{{- else }} + {{- fail "ERROR: You must have autoscaling/v2 or autoscaling/v2beta2 to use HorizontalPodAutoscaler" }} +{{- end }} +kind: HorizontalPodAutoscaler +metadata: +{{- if .Values.controller.autoscaling.annotations }} + annotations: +{{ toYaml .Values.controller.autoscaling.annotations | indent 4 }} +{{- end }} + name: {{ include "kubernetes-ingress.fullname" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "kubernetes-ingress.fullname" . }} + minReplicas: {{ .Values.controller.autoscaling.minReplicas }} + maxReplicas: {{ .Values.controller.autoscaling.maxReplicas }} + {{- if .Values.controller.autoscaling.behavior }} + behavior: {{- toYaml .Values.controller.autoscaling.behavior | nindent 4 }} + {{- end }} + metrics: + {{- if .Values.controller.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.controller.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.controller.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.controller.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} + {{- if .Values.controller.autoscaling.custom }} + {{- toYaml .Values.controller.autoscaling.custom | nindent 4 }} + {{- end }} +{{- end }} +{{- end }} diff --git a/kubernetes-ingress/templates/controller-ingressclass.yaml b/kubernetes-ingress/templates/controller-ingressclass.yaml new file mode 100644 index 0000000..23ac4c3 --- /dev/null +++ b/kubernetes-ingress/templates/controller-ingressclass.yaml @@ -0,0 +1,42 @@ +{{/* +Copyright 2021 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if semverCompare ">=1.16.0-0" .Capabilities.KubeVersion.Version }} +{{- if or (.Capabilities.APIVersions.Has "networking.k8s.io/v1/IngressClass") (semverCompare ">=1.19.0-0" .Capabilities.KubeVersion.Version) }} +apiVersion: networking.k8s.io/v1 +{{- else }} +apiVersion: networking.k8s.io/v1beta1 +{{- end }} +kind: IngressClass +metadata: + name: {{ .Values.controller.ingressClassResource.name }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} +{{- if .Values.controller.ingressClassResource.default }} + annotations: + ingressclass.kubernetes.io/is-default-class: "true" +{{- end }} +spec: +{{- if not .Values.controller.ingressClass }} + controller: haproxy.org/ingress-controller +{{- else }} + controller: haproxy.org/ingress-controller/{{ .Values.controller.ingressClass }} +{{- end }} + {{- if .Values.controller.ingressClassResource.parameters }} + parameters: +{{ toYaml .Values.controller.ingressClassResource.parameters | indent 4 }} +{{- end }} +{{- end }} diff --git a/kubernetes-ingress/templates/controller-keda.yaml b/kubernetes-ingress/templates/controller-keda.yaml new file mode 100644 index 0000000..18a60fc --- /dev/null +++ b/kubernetes-ingress/templates/controller-keda.yaml @@ -0,0 +1,57 @@ +{{/* +Copyright 2021 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and (eq .Values.controller.kind "Deployment") .Values.controller.keda.enabled }} +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{ include "kubernetes-ingress.fullname" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} + {{- if .Values.controller.keda.scaledObject.annotations }} + annotations: {{ toYaml .Values.controller.keda.scaledObject.annotations | nindent 4 }} + {{- end }} +spec: + scaleTargetRef: + name: {{ include "kubernetes-ingress.fullname" . }} + pollingInterval: {{ .Values.controller.keda.pollingInterval }} + cooldownPeriod: {{ .Values.controller.keda.cooldownPeriod }} + minReplicaCount: {{ .Values.controller.keda.minReplicas }} + maxReplicaCount: {{ .Values.controller.keda.maxReplicas }} + triggers: +{{- with .Values.controller.keda.triggers }} +{{ toYaml . | indent 2 }} +{{ end }} +{{- with .Values.controller.keda.fallback }} + fallback: +{{ toYaml . | indent 4 }} +{{- end }} + advanced: + restoreToOriginalReplicaCount: {{ .Values.controller.keda.restoreToOriginalReplicaCount }} +{{- if .Values.controller.keda.horizontalPodAutoscalerConfig }} + horizontalPodAutoscalerConfig: +{{- if .Values.controller.keda.horizontalPodAutoscalerConfig.name }} + name: {{ .Values.controller.keda.horizontalPodAutoscalerConfig.name }} +{{- end }} +{{- if .Values.controller.keda.horizontalPodAutoscalerConfig.behavior }} + behavior: +{{ with .Values.controller.keda.horizontalPodAutoscalerConfig.behavior -}} +{{ toYaml . | indent 8 }} +{{ end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/kubernetes-ingress/templates/controller-poddisruptionbudget.yaml b/kubernetes-ingress/templates/controller-poddisruptionbudget.yaml new file mode 100644 index 0000000..83bbb3f --- /dev/null +++ b/kubernetes-ingress/templates/controller-poddisruptionbudget.yaml @@ -0,0 +1,39 @@ +{{/* +Copyright 2019 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.controller.PodDisruptionBudget.enable }} +{{- if or (.Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget") (semverCompare ">=1.21.0-0" .Capabilities.KubeVersion.Version) }} +apiVersion: policy/v1 +{{- else }} +apiVersion: policy/v1beta1 +{{- end }} +kind: PodDisruptionBudget +metadata: + name: {{ include "kubernetes-ingress.fullname" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} +spec: + {{- if .Values.controller.PodDisruptionBudget.maxUnavailable }} + maxUnavailable: {{ .Values.controller.PodDisruptionBudget.maxUnavailable }} + {{- end }} + {{- if .Values.controller.PodDisruptionBudget.minAvailable }} + minAvailable: {{ .Values.controller.PodDisruptionBudget.minAvailable }} + {{- end }} + selector: + matchLabels: + {{- include "kubernetes-ingress.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/kubernetes-ingress/templates/controller-podmonitor.yaml b/kubernetes-ingress/templates/controller-podmonitor.yaml new file mode 100644 index 0000000..3016dfd --- /dev/null +++ b/kubernetes-ingress/templates/controller-podmonitor.yaml @@ -0,0 +1,37 @@ +{{/* +Copyright 2024 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and (.Capabilities.APIVersions.Has "monitoring.coreos.com/v1") .Values.controller.podMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: {{ include "kubernetes-ingress.podMonitorName" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} + {{- if .Values.controller.podMonitor.extraLabels }} + {{ toYaml .Values.controller.podMonitor.extraLabels | nindent 4 }} + {{- end }} +spec: + podMetricsEndpoints: + {{ .Values.controller.podMonitor.endpoints | toYaml | nindent 4 }} + namespaceSelector: + matchNames: + - {{ include "kubernetes-ingress.namespace" . }} + selector: + matchLabels: + {{- include "kubernetes-ingress.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/kubernetes-ingress/templates/controller-podsecuritypolicy.yaml b/kubernetes-ingress/templates/controller-podsecuritypolicy.yaml new file mode 100644 index 0000000..77696fe --- /dev/null +++ b/kubernetes-ingress/templates/controller-podsecuritypolicy.yaml @@ -0,0 +1,82 @@ +{{/* +Copyright 2019 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if (semverCompare "<1.25.0-0" .Capabilities.KubeVersion.Version) }} +{{- if and .Values.rbac.create .Values.podSecurityPolicy.enabled }} +{{- $useHostNetwork := .Values.controller.daemonset.useHostNetwork }} +{{- $useHostPort := .Values.controller.daemonset.useHostPort }} +{{- $hostPorts := .Values.controller.daemonset.hostPorts -}} +{{- if or (.Capabilities.APIVersions.Has "policy/v1/PodSecurityPolicy") (semverCompare ">=1.21.0-0" .Capabilities.KubeVersion.Version) }} +apiVersion: policy/v1 +{{- else }} +apiVersion: policy/v1beta1 +{{- end }} +kind: PodSecurityPolicy +metadata: +{{- if .Values.podSecurityPolicy.annotations }} + annotations: +{{ toYaml .Values.podSecurityPolicy.annotations | indent 4 }} +{{- end }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} + name: {{ include "kubernetes-ingress.fullname" . }} + annotations: + seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'docker/default,runtime/default' + apparmor.security.beta.kubernetes.io/allowedProfileNames: 'runtime/default' + seccomp.security.alpha.kubernetes.io/defaultProfileName: 'runtime/default' + apparmor.security.beta.kubernetes.io/defaultProfileName: 'runtime/default' +spec: + allowPrivilegeEscalation: {{ .Values.controller.allowPrivilegeEscalation }} + allowedCapabilities: + - NET_BIND_SERVICE + defaultAllowPrivilegeEscalation: false + fsGroup: + rule: MustRunAs + ranges: + - max: 65535 + min: 1 +{{- if $useHostNetwork }} + hostNetwork: true +{{- end }} +{{- if or $useHostPort $useHostNetwork }} + hostPorts: +{{- range $key, $value := .Values.controller.containerPort }} + - min: {{ $value }} + max: {{ $value }} +{{- end }} +{{- range .Values.controller.service.tcpPorts }} + - min: {{ .port }} + max: {{ .port }} +{{- end }} +{{- end }} + hostIPC: false + hostPID: false + privileged: false + runAsUser: + rule: RunAsAny + seLinux: + rule: RunAsAny + supplementalGroups: + rule: MustRunAs + ranges: + - max: 65535 + min: 1 + volumes: + - configMap + - downwardAPI + - secret +{{- end }} +{{- end }} diff --git a/kubernetes-ingress/templates/controller-proxy-deployment.yaml b/kubernetes-ingress/templates/controller-proxy-deployment.yaml new file mode 100644 index 0000000..37cd7df --- /dev/null +++ b/kubernetes-ingress/templates/controller-proxy-deployment.yaml @@ -0,0 +1,283 @@ +{{/* +Copyright 2024 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and (eq .Values.controller.sync.mode "fetch") (eq .Values.controller.sync.fetchParams.source "proxy") }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "kubernetes-ingress.serviceProxyName" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} + {{- with .Values.controller.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + labels: + {{- include "kubernetes-ingress.serviceProxyLabels" . | nindent 4 }} + {{- if .Values.controller.extraLabels }} +{{ toYaml .Values.controller.extraLabels | indent 4 }} + {{- end }} +spec: + {{- if and (not .Values.controller.autoscaling.enabled) (not .Values.controller.keda.enabled) }} + replicas: {{ .Values.controller.sync.proxyParams.replicaCount }} + {{- end }} + minReadySeconds: {{ .Values.controller.minReadySeconds }} + selector: + matchLabels: + {{- include "kubernetes-ingress.serviceProxySelectorLabels" . | nindent 6 }} + {{- with .Values.controller.strategy }} + strategy: + {{- toYaml . | nindent 4 }} + {{- end }} + template: + metadata: + labels: + {{- include "kubernetes-ingress.serviceProxySelectorLabels" . | nindent 8 }} + {{- if .Values.controller.podLabels }} +{{ toYaml .Values.controller.podLabels | indent 8 }} + {{- end }} + {{- if .Values.controller.podAnnotations }} + annotations: +{{- if eq "string" (printf "%T" .Values.controller.podAnnotations) }} +{{ tpl .Values.controller.podAnnotations . | indent 8 }} +{{- else }} +{{ toYaml .Values.controller.podAnnotations | indent 8 }} +{{- end }} + {{- end }} + spec: + enableServiceLinks: {{ .Values.controller.enableServiceLinks }} + serviceAccountName: {{ include "kubernetes-ingress.serviceAccountName" . }} + terminationGracePeriodSeconds: {{ .Values.controller.terminationGracePeriodSeconds }} +{{- with .Values.controller.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} +{{- end }} +{{- if .Values.controller.dnsConfig }} + dnsConfig: +{{ toYaml .Values.controller.dnsConfig | indent 8 }} +{{- end }} + dnsPolicy: {{ .Values.controller.dnsPolicy }} +{{- if .Values.controller.imageCredentials.registry }} + imagePullSecrets: + - name: {{ include "kubernetes-ingress.fullname" . }} +{{- else if .Values.controller.existingImagePullSecret }} + imagePullSecrets: + - name: {{ .Values.controller.existingImagePullSecret }} +{{- end }} +{{- if .Values.controller.priorityClassName }} + priorityClassName: {{ .Values.controller.priorityClassName }} +{{- end }} +{{- if .Values.controller.runtimeClassName }} + runtimeClassName: {{ .Values.controller.runtimeClassName }} +{{- end }} +{{- if .Values.controller.unprivileged }} + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 +{{- if .Values.controller.allowPrivilegedPorts }} + sysctls: + - name: net.ipv4.ip_unprivileged_port_start + value: "0" +{{- end }} +{{- end }} + containers: + - name: {{ include "kubernetes-ingress.name" . }}-{{ .Values.controller.name }} + image: "{{ .Values.controller.image.repository }}:{{ .Values.controller.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.controller.image.pullPolicy }} + args: +{{- if .Values.controller.defaultTLSSecret.enabled -}} +{{- if and .Values.controller.defaultTLSSecret.secret .Values.controller.defaultTLSSecret.secretNamespace }} + - --default-ssl-certificate={{ tpl .Values.controller.defaultTLSSecret.secretNamespace . }}/{{ .Values.controller.defaultTLSSecret.secret }} +{{- else }} + - --default-ssl-certificate={{ include "kubernetes-ingress.namespace" . }}/{{ include "kubernetes-ingress.defaultTLSSecret.fullname" . }} +{{- end }} +{{- end }} + - --configmap={{ include "kubernetes-ingress.namespace" . }}/{{ include "kubernetes-ingress.fullname" . }} + - --http-bind-port={{ .Values.controller.containerPort.http }} + - --https-bind-port={{ .Values.controller.containerPort.https }} +{{- if and (semverCompare ">=1.24.0-0" .Capabilities.KubeVersion.Version) .Values.controller.service.enablePorts.quic }} + - --quic-bind-port={{ .Values.controller.containerPort.https }} + - --quic-announce-port={{ .Values.controller.service.ports.https }} +{{- end }} +{{- if .Values.controller.ingressClass }} + - --ingress.class={{ .Values.controller.ingressClass }} +{{- end }} +{{- if and .Values.controller.kubernetesGateway.enabled .Values.controller.kubernetesGateway.gatewayControllerName }} + - --gateway-controller-name={{ .Values.controller.kubernetesGateway.gatewayControllerName }} +{{- end }} +{{- if .Values.controller.publishService.enabled }} + - --publish-service={{ include "kubernetes-ingress.publishServicePath" . }} +{{- end }} +{{- if .Values.controller.logging.level }} + - --log={{ .Values.controller.logging.level }} +{{- end }} +{{- if .Values.controller.service.enablePorts.admin }} + - --prometheus + - --pprof +{{- end }} + - --proxy-server-mode + - --k8s-api-sync-type=k8s + - --proxy-svc-label-selector={{ .Values.controller.sync.proxyParams.proxySvcLabelSelector }} +{{- if .Values.controller.sync.fetchParams.period }} + - --proxy-k8s-fetch-period={{ .Values.controller.sync.fetchParams.period }} +{{- end }} +{{- range .Values.controller.extraArgs }} + - {{ . }} +{{- end }} + {{- if .Values.controller.unprivileged }} + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: {{ .Values.controller.allowPrivilegeEscalation }} + capabilities: + drop: + - ALL + add: + - NET_BIND_SERVICE + {{- if .Values.controller.enableRuntimeDefaultSeccompProfile }} + seccompProfile: + type: RuntimeDefault + {{- end }} + {{- end }} + ports: + {{- range $key, $value := .Values.controller.containerPort }} + - name: {{ $key }} + containerPort: {{ $value }} + protocol: TCP + {{- end }} + {{- if and (semverCompare ">=1.24.0-0" .Capabilities.KubeVersion.Version) .Values.controller.service.enablePorts.quic }} + - name: quic + containerPort: {{ .Values.controller.containerPort.https }} + protocol: UDP + {{- end }} + {{- range .Values.controller.service.tcpPorts }} + - name: {{ .name | trunc 15 | trimSuffix "-" }} + containerPort: {{ .targetPort }} + protocol: TCP + {{- end }} + {{- with .Values.controller.livenessProbe }} + livenessProbe: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + {{- with .Values.controller.readinessProbe }} + readinessProbe: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + {{- with .Values.controller.startupProbe }} + startupProbe: + {{- toYaml . | trim | nindent 12 }} + {{- end }} + env: + {{- if .Values.aws.licenseConfigSecretName }} + - name: AWS_WEB_IDENTITY_REFRESH_TOKEN_FILE + value: "/var/run/secrets/product-license/license_token" + - name: AWS_ROLE_ARN + valueFrom: + secretKeyRef: + name: {{ .Values.aws.licenseConfigSecretName }} + key: iam_role + {{- end }} + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + {{- if .Values.controller.extraEnvs -}} + {{- toYaml .Values.controller.extraEnvs | nindent 10 }} + {{- end }} + resources: + {{- toYaml .Values.controller.resources | nindent 12 }} + {{- if .Values.controller.lifecycle }} + lifecycle: + {{- if eq "string" (printf "%T" .Values.controller.lifecycle) }} +{{ tpl .Values.controller.lifecycle . | indent 12 }} + {{- else }} +{{ toYaml .Values.controller.lifecycle | indent 12 }} + {{- end }} + {{- end }} + volumeMounts: + - name: tmp + mountPath: /tmp + subPath: tmp + - name: tmp + mountPath: /run + subPath: run + {{- if .Values.aws.licenseConfigSecretName }} + - name: aws-product-license + readOnly: true + mountPath: /var/run/secrets/product-license + {{- end }} + {{- if eq "string" (printf "%T" .Values.controller.extraVolumeMounts) }} +{{ tpl .Values.controller.extraVolumeMounts . | indent 12 }} + {{- else if gt (len .Values.controller.extraVolumeMounts) 0 }} +{{ toYaml .Values.controller.extraVolumeMounts | indent 12 }} + {{- end }} + {{- if .Values.controller.extraContainers }} + {{- if eq "string" (printf "%T" .Values.controller.extraContainers) }} +{{ tpl .Values.controller.extraContainers . | indent 8 }} + {{- else }} +{{ toYaml .Values.controller.extraContainers | indent 8 }} + {{- end }} + {{- end }} + volumes: + - name: tmp + {{- if semverCompare ">=1.21.0-0" .Capabilities.KubeVersion.Version }} + emptyDir: + medium: Memory + sizeLimit: 64Mi + {{- else }} + emptyDir: {} + {{- end }} + {{- if .Values.aws.licenseConfigSecretName }} + - name: aws-product-license + secret: + secretName: {{ .Values.aws.licenseConfigSecretName }} + optional: true + {{- end }} + {{- if eq "string" (printf "%T" .Values.controller.extraVolumes) }} +{{ tpl .Values.controller.extraVolumes . | indent 8 }} + {{- else if gt (len .Values.controller.extraVolumes) 0 }} +{{ toYaml .Values.controller.extraVolumes | indent 8 }} + {{- end }} + {{- if .Values.controller.initContainers }} + initContainers: + {{- if eq "string" (printf "%T" .Values.controller.initContainers) }} +{{ tpl .Values.controller.initContainers . | indent 8 }} + {{- else }} +{{ toYaml .Values.controller.initContainers | indent 8 }} + {{- end }} + {{- end }} + {{- with .Values.controller.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.controller.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/kubernetes-ingress/templates/controller-proxy-service.yaml b/kubernetes-ingress/templates/controller-proxy-service.yaml new file mode 100644 index 0000000..98c7791 --- /dev/null +++ b/kubernetes-ingress/templates/controller-proxy-service.yaml @@ -0,0 +1,68 @@ +{{/* +Copyright 2024 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and (eq .Values.controller.sync.mode "fetch") (eq .Values.controller.sync.fetchParams.source "proxy") }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "kubernetes-ingress.serviceProxyName" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} + labels: + {{- include "kubernetes-ingress.serviceProxyLabels" . | nindent 4 }} + {{ (split ":" .Values.controller.sync.proxyParams.proxySvcLabelSelector)._0 }}: {{ (split ":" .Values.controller.sync.proxyParams.proxySvcLabelSelector)._1 }} +{{- if .Values.controller.service.labels }} +{{ toYaml .Values.controller.service.labels | indent 4 }} +{{- end }} + annotations: +{{- range $key, $value := .Values.controller.service.annotations }} + {{ $key }}: {{ $value | quote }} +{{- end }} +spec: + type: ClusterIP + {{- if .Values.controller.service.healthCheckNodePort }} + healthCheckNodePort: {{ .Values.controller.service.healthCheckNodePort }} + {{- end }} + ports: + {{- if .Values.controller.service.enablePorts.http }} + - name: http + port: {{ .Values.controller.service.ports.http }} + protocol: TCP + {{- if semverCompare ">=1.20.0-0" .Capabilities.KubeVersion.Version }} + appProtocol: http + {{- end }} + targetPort: {{ .Values.controller.service.targetPorts.http }} + {{- if .Values.controller.service.nodePorts.http }} + nodePort: {{ .Values.controller.service.nodePorts.http }} + {{- end }} + {{- end }} + {{- if .Values.controller.service.enablePorts.https }} + - name: https + port: {{ .Values.controller.service.ports.https }} + protocol: TCP + {{- if semverCompare ">=1.20.0-0" .Capabilities.KubeVersion.Version }} + appProtocol: https + {{- end }} + targetPort: {{ .Values.controller.service.targetPorts.https }} + {{- if .Values.controller.service.nodePorts.https }} + nodePort: {{ .Values.controller.service.nodePorts.https }} + {{- end }} + {{- end }} + selector: + {{- include "kubernetes-ingress.serviceProxySelectorLabels" . | nindent 4 }} + {{- if .Values.controller.service.sessionAffinity }} + sessionAffinity: {{ .Values.controller.service.sessionAffinity }} + {{- end }} +{{- end }} diff --git a/kubernetes-ingress/templates/controller-pullsecret.yaml b/kubernetes-ingress/templates/controller-pullsecret.yaml new file mode 100644 index 0000000..bd53a5b --- /dev/null +++ b/kubernetes-ingress/templates/controller-pullsecret.yaml @@ -0,0 +1,28 @@ +{{/* +Copyright 2019 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.controller.imageCredentials.registry }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "kubernetes-ingress.fullname" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} +type: kubernetes.io/dockerconfigjson +data: + .dockerconfigjson: {{ include "kubernetes-ingress.imagePullSecret" . }} +{{- end }} diff --git a/kubernetes-ingress/templates/controller-role.yaml b/kubernetes-ingress/templates/controller-role.yaml new file mode 100644 index 0000000..1bde843 --- /dev/null +++ b/kubernetes-ingress/templates/controller-role.yaml @@ -0,0 +1,34 @@ +{{/* +Copyright 2019 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and .Values.rbac.create .Values.podSecurityPolicy.enabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "kubernetes-ingress.fullname" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} +rules: +- apiGroups: + - "policy" + resources: + - podsecuritypolicies + verbs: + - use + resourceNames: + - {{ include "kubernetes-ingress.fullname" . }} +{{- end -}} diff --git a/kubernetes-ingress/templates/controller-rolebinding.yaml b/kubernetes-ingress/templates/controller-rolebinding.yaml new file mode 100644 index 0000000..56f89e4 --- /dev/null +++ b/kubernetes-ingress/templates/controller-rolebinding.yaml @@ -0,0 +1,33 @@ +{{/* +Copyright 2019 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and .Values.rbac.create .Values.podSecurityPolicy.enabled -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "kubernetes-ingress.fullname" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "kubernetes-ingress.fullname" . }} +subjects: +- kind: ServiceAccount + name: {{ include "kubernetes-ingress.serviceAccountName" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} +{{- end -}} diff --git a/kubernetes-ingress/templates/controller-service-metrics.yaml b/kubernetes-ingress/templates/controller-service-metrics.yaml new file mode 100644 index 0000000..9536037 --- /dev/null +++ b/kubernetes-ingress/templates/controller-service-metrics.yaml @@ -0,0 +1,56 @@ +{{/* +Copyright 2022 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{/* +The following Service resource will be created upon certain conditions: +- The ServiceMonitor integration is enabled +- A Service resource must be created + +The reason for that is that the Ingress Controller would make it available to the outside +sensitive data such as its metrics, and the operator wants to keep these data private +(such as the value of "controller.service.enablePorts.stat=false"). + +To let the Prometheus Operator being able to scrape the metrics, an additional service +is going to be created, allowing it to expose of these in the internal Kubernetes networking. +*/}} +{{- if and (.Values.controller.serviceMonitor.enabled) (.Values.controller.service.enabled) }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "kubernetes-ingress.serviceMetricsName" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} +{{- if .Values.controller.service.metrics.labels }} +{{ toYaml .Values.controller.service.metrics.labels | indent 4 }} +{{- end }} + annotations: +{{- range $key, $value := .Values.controller.service.metrics.annotations }} + {{ $key }}: {{ $value | quote }} +{{- end }} +spec: + type: {{ .Values.controller.service.metrics.type }} + ports: + - name: stat + port: {{ .Values.controller.service.ports.stat }} + protocol: TCP + targetPort: {{ .Values.controller.service.targetPorts.stat }} + {{- if .Values.controller.service.nodePorts.stat }} + nodePort: {{ .Values.controller.service.nodePorts.stat }} + {{- end }} + selector: + {{- include "kubernetes-ingress.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/kubernetes-ingress/templates/controller-service.yaml b/kubernetes-ingress/templates/controller-service.yaml new file mode 100644 index 0000000..9b33188 --- /dev/null +++ b/kubernetes-ingress/templates/controller-service.yaml @@ -0,0 +1,133 @@ +{{/* +Copyright 2019 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.controller.service.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "kubernetes-ingress.fullname" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} +{{- if .Values.controller.service.labels }} +{{ toYaml .Values.controller.service.labels | indent 4 }} +{{- end }} + annotations: +{{- range $key, $value := .Values.controller.service.annotations }} + {{ $key }}: {{ $value | quote }} +{{- end }} +spec: + {{ with .Values.controller.service.clusterIP }}clusterIP: {{ . }}{{ end }} + type: {{ .Values.controller.service.type }} + {{- if .Values.controller.service.externalTrafficPolicy }} + externalTrafficPolicy: {{ .Values.controller.service.externalTrafficPolicy }} + {{- end }} + {{- if .Values.controller.service.healthCheckNodePort }} + healthCheckNodePort: {{ .Values.controller.service.healthCheckNodePort }} + {{- end }} + ports: + {{- if .Values.controller.service.enablePorts.http }} + - name: http + port: {{ .Values.controller.service.ports.http }} + protocol: TCP + {{- if semverCompare ">=1.20.0-0" .Capabilities.KubeVersion.Version }} + appProtocol: http + {{- end }} + targetPort: {{ .Values.controller.service.targetPorts.http }} + {{- if .Values.controller.service.nodePorts.http }} + nodePort: {{ .Values.controller.service.nodePorts.http }} + {{- end }} + {{- end }} + {{- if .Values.controller.service.enablePorts.https }} + - name: https + port: {{ .Values.controller.service.ports.https }} + protocol: TCP + {{- if semverCompare ">=1.20.0-0" .Capabilities.KubeVersion.Version }} + appProtocol: https + {{- end }} + targetPort: {{ .Values.controller.service.targetPorts.https }} + {{- if .Values.controller.service.nodePorts.https }} + nodePort: {{ .Values.controller.service.nodePorts.https }} + {{- end }} + {{- end }} + {{- if and (semverCompare ">=1.24.0-0" .Capabilities.KubeVersion.Version) .Values.controller.service.enablePorts.quic }} + - name: quic + port: {{ .Values.controller.service.ports.https }} + protocol: UDP + {{- if semverCompare ">=1.20.0-0" .Capabilities.KubeVersion.Version }} + appProtocol: https + {{- end }} + targetPort: {{ .Values.controller.service.targetPorts.quic }} + {{- if .Values.controller.service.nodePorts.https }} + nodePort: {{ .Values.controller.service.nodePorts.https }} + {{- end }} + {{- end }} + {{- if .Values.controller.service.enablePorts.stat }} + - name: stat + port: {{ .Values.controller.service.ports.stat }} + protocol: TCP + targetPort: {{ .Values.controller.service.targetPorts.stat }} + {{- if .Values.controller.service.nodePorts.stat }} + nodePort: {{ .Values.controller.service.nodePorts.stat }} + {{- end }} + {{- end }} + {{- if .Values.controller.service.enablePorts.admin }} + - name: admin + port: {{ .Values.controller.service.ports.admin }} + protocol: TCP + targetPort: {{ .Values.controller.service.targetPorts.admin }} + {{- if .Values.controller.service.nodePorts.admin }} + nodePort: {{ .Values.controller.service.nodePorts.admin }} + {{- end }} + {{- end }} + {{- range .Values.controller.service.tcpPorts }} + - name: {{ .name | trunc 15 | trimSuffix "-" }} + port: {{ .port }} + protocol: TCP + targetPort: {{ .targetPort }} + {{- if .nodePort }} + nodePort: {{ .nodePort }} + {{- end }} + {{- end }} + selector: + {{- include "kubernetes-ingress.selectorLabels" . | nindent 4 }} + {{- if .Values.controller.service.sessionAffinity }} + sessionAffinity: {{ .Values.controller.service.sessionAffinity }} + {{- end }} +{{- if .Values.controller.service.ipFamilies }} + ipFamilies: +{{- toYaml .Values.controller.service.ipFamilies | nindent 4 }} +{{- end }} +{{- if .Values.controller.service.ipFamilyPolicy }} + ipFamilyPolicy: {{ .Values.controller.service.ipFamilyPolicy | quote }} +{{- end }} + externalIPs: +{{- if .Values.controller.service.externalIPs }} +{{ toYaml .Values.controller.service.externalIPs | indent 4 }} +{{- end -}} +{{- if (eq .Values.controller.service.type "LoadBalancer") }} +{{- if .Values.controller.service.loadBalancerIP }} + loadBalancerIP: "{{ .Values.controller.service.loadBalancerIP }}" +{{- end }} +{{- if .Values.controller.service.loadBalancerSourceRanges }} + loadBalancerSourceRanges: +{{ toYaml .Values.controller.service.loadBalancerSourceRanges | indent 4 }} +{{- end }} +{{- if .Values.controller.service.loadBalancerClass}} + loadBalancerClass: "{{ .Values.controller.service.loadBalancerClass }}" +{{- end }} +{{- end }} +{{- end }} diff --git a/kubernetes-ingress/templates/controller-serviceaccount.yaml b/kubernetes-ingress/templates/controller-serviceaccount.yaml new file mode 100644 index 0000000..d630b54 --- /dev/null +++ b/kubernetes-ingress/templates/controller-serviceaccount.yaml @@ -0,0 +1,28 @@ +{{/* +Copyright 2019 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if or .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "kubernetes-ingress.serviceAccountName" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} +{{- if hasKey .Values.serviceAccount "automountServiceAccountToken" }} +automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} +{{- end }} +{{- end -}} diff --git a/kubernetes-ingress/templates/controller-servicemonitor.yaml b/kubernetes-ingress/templates/controller-servicemonitor.yaml new file mode 100644 index 0000000..dc8dc4c --- /dev/null +++ b/kubernetes-ingress/templates/controller-servicemonitor.yaml @@ -0,0 +1,37 @@ +{{/* +Copyright 2019 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if and (.Capabilities.APIVersions.Has "monitoring.coreos.com/v1") .Values.controller.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "kubernetes-ingress.serviceMonitorName" . }} + namespace: {{ include "kubernetes-ingress.namespace" . }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} + {{- if .Values.controller.serviceMonitor.extraLabels }} + {{ toYaml .Values.controller.serviceMonitor.extraLabels | nindent 4 }} + {{- end }} +spec: + endpoints: + {{ .Values.controller.serviceMonitor.endpoints | toYaml | nindent 4 }} + namespaceSelector: + matchNames: + - {{ include "kubernetes-ingress.namespace" . }} + selector: + matchLabels: + {{- include "kubernetes-ingress.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/kubernetes-ingress/templates/namespace.yaml b/kubernetes-ingress/templates/namespace.yaml new file mode 100644 index 0000000..f908a02 --- /dev/null +++ b/kubernetes-ingress/templates/namespace.yaml @@ -0,0 +1,27 @@ +{{/* +Copyright 2022 HAProxy Technologies LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/}} + +{{- if .Values.namespace.create -}} +apiVersion: v1 +kind: Namespace +metadata: + name: {{ include "kubernetes-ingress.namespace" . }} + labels: + {{- include "kubernetes-ingress.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": "pre-install" + "helm.sh/hook-weight": "-1" +{{- end -}} diff --git a/kubernetes-ingress/values.yaml b/kubernetes-ingress/values.yaml new file mode 100644 index 0000000..f21e2c7 --- /dev/null +++ b/kubernetes-ingress/values.yaml @@ -0,0 +1,690 @@ +# Copyright 2019 HAProxy Technologies LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +## Default values for kubernetes-ingress Chart for HAProxy Ingress Controller +## ref: https://github.com/haproxytech/kubernetes-ingress/tree/master/documentation + +podSecurityPolicy: + ## Specify pod annotations + ## ref: https://kubernetes.io/docs/concepts/policy/pod-security-policy/#apparmor + ## ref: https://kubernetes.io/docs/concepts/policy/pod-security-policy/#seccomp + ## ref: https://kubernetes.io/docs/concepts/policy/pod-security-policy/#sysctl + annotations: {} + # apparmor.security.beta.kubernetes.io/allowedProfileNames: runtime/default + # apparmor.security.beta.kubernetes.io/defaultProfileName: runtime/default + # seccomp.security.alpha.kubernetes.io/allowedProfileNames: runtime/default + # seccomp.security.alpha.kubernetes.io/defaultProfileName: runtime/default + enabled: false + +## Enable RBAC Authorization +## ref: https://kubernetes.io/docs/reference/access-authn-authz/rbac/ +rbac: + create: true + +## Create namespace +## https://kubernetes.io/docs/tasks/administer-cluster/namespaces-walkthrough/ +namespace: + create: false + +## Configure Service Account +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/ +serviceAccount: + create: true + name: + automountServiceAccountToken: true + +## Namespace override +## Allow the release namespace to be overridden for multi-namespace deployments in combined charts +# namespaceOverride: haproxytech + +## AWS Market Place integration +## Allows installation of the HAPEE Ingress Controller on AWS EKS and EKS-Anywhere. +## ref: https://docs.aws.amazon.com/marketplace/latest/userguide/container-anywhere-license-manager-integration.html +aws: + ## Name of the Secret deployed in the desired namespace containing the AWS license files + licenseConfigSecretName: "" + +## Controller default values +controller: + name: controller + image: + repository: docker.io/haproxytech/kubernetes-ingress # can be changed to use CE or EE Controller images + tag: "" # overrides the image tag whose default is the chart appVersion + pullPolicy: IfNotPresent + + ## Deployment or DaemonSet pod mode + ## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/ + ## ref: https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/ + kind: Deployment # can be 'Deployment' or 'DaemonSet' + replicaCount: 2 + + ## minReadySeconds setting of Deployment or DaemonSet + ## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#min-ready-seconds + minReadySeconds: 0 + + ## Running container without root privileges + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ + unprivileged: true + + ## Allow privileged port binding without root privileges + ## ref: https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster/ + ## Note: this feature enables net.ipv4.ip_unprivileged_port_start=0 sysctl when running in unprivileged mode + allowPrivilegedPorts: false + + ## Restricts container syscalls + ## ref: https://kubernetes.io/docs/tutorials/security/seccomp/ + enableRuntimeDefaultSeccompProfile: true + + ## Privilege escalation + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ + allowPrivilegeEscalation: false + + ## Init Containers + ## ref: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ + initContainers: [] + # - name: sysctl + # image: "busybox:musl" + # command: + # - /bin/sh + # - -c + # - sysctl -w net.core.somaxconn=65536 + # securityContext: + # privileged: true + + ## Pod sysctls (applies to Deployment/DaemonSet template) + ## ref: https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster/ + ## Note: when unprivileged=true and allowPrivilegedPorts=true, the chart will also + ## set net.ipv4.ip_unprivileged_port_start=0 unless you override it here. + sysctls: {} + # "net.core.somaxconn": "8192" + + ## Pod termination grace period + ## ref: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/ + terminationGracePeriodSeconds: 60 + + ## Private Registry configuration + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + imageCredentials: + registry: null + username: null + password: null + existingImagePullSecret: null + + ## Controller Container listener port configuration + ## ref: https://kubernetes.io/docs/concepts/services-networking/connect-applications-service/ + ## Note: If binding to privileged ports, allowPrivilegeEscalation will be required for NET_BIND_SERVICE to apply + containerPort: + http: 8080 + https: 8443 + stat: 1024 + admin: 6060 + + ## Controller Container liveness/readiness probe configuration + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ + livenessProbe: + httpGet: + path: /healthz + port: 1042 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + + readinessProbe: + httpGet: + path: /healthz + port: 1042 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + + startupProbe: + httpGet: + path: /healthz + port: 1042 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 30 + + ## IngressClass: + ## ref: https://github.com/haproxytech/kubernetes-ingress/blob/master/documentation/ingressclass.md + + # k8s >= 1.18: IngressClass resource used, in multi-ingress environments, to select ingress resources to implement. + # ref: https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-class + # ref: https://kubernetes.io/docs/concepts/services-networking/ingress/#default-ingress-class + # Note: Uses ingressClass as name for the Ingress Class object if enabled + ingressClassResource: + name: haproxy + default: false + parameters: {} + + # k8s < 1.18: Ingress Class used, in multi-ingress environments, for ingress.class annotation to select ingress resources to implement. + # k8s >= 1.18: Ingress Class used to target specific HAProxy Ingress Controller in multi-ingress envionments + # ref: https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/#using-multiple-ingress-controllers + ingressClass: haproxy # typically "haproxy" or null to receive all events + + # Gateway API controller, not available in K8s as default but can be installed + # ref: https://gateway-api.sigs.k8s.io/ + # ref: https://gateway-api.sigs.k8s.io/guides/#installing-a-gateway-controller + kubernetesGateway: + enabled: false + gatewayControllerName: haproxy.org/gateway-controller + + ## Additional labels to add to the deployment or daemonset metadata + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + extraLabels: {} + # key: value + + ## Additional annotations to add to the deployment or daemonset metadata + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + annotations: {} + # key: value + + ## Additional labels to add to the pod container metadata + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + podLabels: {} + # key: value + + ## Additional annotations to add to the pod container metadata + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + podAnnotations: {} + # key: value + + ## Allows to enable/disable environment variables for finding services + ## ref: https://kubernetes.io/docs/tutorials/services/connect-applications-service/#accessing-the-service + ## Note: Possible performance issues in large clusters: https://github.com/kubernetes/kubernetes/issues/92615 + enableServiceLinks: true + + ## Ingress TLS secret, if it is enabled and secret is null then controller will use auto-generated secret, otherwise + ## secret needs to contain name of the Secret object which has been created manually + ## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls + ## ref: https://kubernetes.io/docs/concepts/configuration/secret/ + defaultTLSSecret: + enabled: true + secretNamespace: '{{ include "kubernetes-ingress.namespace" . }}' + secret: null + + ## Compute Resources for controller container + ## ref: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/ + resources: + # limits: + # cpu: 250m + # memory: 400Mi + requests: + cpu: 250m + memory: 400Mi + + ## Horizontal Pod Scaler + ## Only to be used with Deployment kind + ## ref: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/ + autoscaling: + enabled: false + minReplicas: 2 + maxReplicas: 20 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + + ## HPA annotations + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + annotations: {} + # annotationKey: value + + ## Behavior + ## ref: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#configurable-scaling-behavior + # behavior: + # scaleDown: + # stabilizationWindowSeconds: 3600 + + ## Custom metrics (example) + ## ref: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/#autoscaling-on-multiple-metrics-and-custom-metrics + # custom: + # - type: Pods + # pods: + # metricName: haproxy_backend_current_sessions + # targetAverageValue: 2000 + + ## Kubernetes Event-driven Autoscaling: KEDA 2.x + ## ref: https://keda.sh/docs/2.3/concepts/scaling-deployments/ + ## Note: mutually exclusive with HPA, enabling KEDA disables HPA + ## Node: requires serviceMonitor enabled + keda: + enabled: false + minReplicas: 2 + maxReplicas: 20 + pollingInterval: 30 + cooldownPeriod: 300 + restoreToOriginalReplicaCount: false + # fallback: + # failureThreshold: 3 + # replicas: 6 + # behavior: static + scaledObject: + annotations: {} + horizontalPodAutoscalerConfig: {} + # name: "" + # behavior: + # scaleDown: + # stabilizationWindowSeconds: 300 + # policies: + # - type: Percent + # value: 100 + # periodSeconds: 15 + triggers: [] + # - type: prometheus + # metadata: + # serverAddress: http://:9090 + # metricName: haproxy_process_idle_time_percent + # threshold: '50' + # query: avg(100-avg_over_time(haproxy_process_idle_time_percent{container="kubernetes-ingress-controller",service="mytest-kubernetes-ingress"}[2m])) + + ## Pod Disruption Budget + ## Only to be used with Deployment kind + ## ref: https://kubernetes.io/docs/tasks/run-application/configure-pdb/ + PodDisruptionBudget: + enable: false + # maxUnavailable: 1 + # minAvailable: 1 + + ## Pod Node assignment + ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ + nodeSelector: {} + + ## Node Taints and Tolerations for pod-node cheduling through attraction/repelling + ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ + tolerations: [] + # - key: "key" + # operator: "Equal|Exists" + # value: "value" + # effect: "NoSchedule|PreferNoSchedule|NoExecute(1.6 only)" + + ## Node Affinity for pod-node scheduling constraints + ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity + affinity: {} + + ## Topology spread constraints (only used in kind: Deployment) + ## ref: https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/ + topologySpreadConstraints: [] + # - maxSkew: 1 + # topologyKey: kubernetes.io/zone + # whenUnsatisfiable: DoNotSchedule + # labelSelector: + # matchLabels: + # app.kubernetes.io/name: kubernetes-ingress + # app.kubernetes.io/instance: kubernetes-ingress + + ## Pod DNS Config + ## ref: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/ + dnsConfig: {} + + ## Pod DNS Policy + ## Change this to ClusterFirstWithHostNet in case you have useHostNetwork set to true + ## ref: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy + dnsPolicy: ClusterFirst + + ## Additional command line arguments to pass to Controller + ## ref: https://github.com/haproxytech/kubernetes-ingress/blob/master/documentation/controller.md + extraArgs: [] + # - --namespace-whitelist=default + # - --namespace-whitelist=namespace1 + # - --namespace-blacklist=namespace2 + # - --disable-ipv4 + # - --disable-ipv6 + # - --disable-http + # - --disable-https + # - --disable-quic + # - --sync-period=10s + + ## Custom configuration for Controller + ## ref: https://github.com/haproxytech/kubernetes-ingress/tree/master/documentation + config: {} + # timeout-connect: "250ms" + # servers-increment: "10" + # servers-increment-max-disabled: "10" + # rate-limit: "ON" + # rate-limit-expire: "1m" + # rate-limit-interval: "10s" + # rate-limit-size: "100k" + + ## Extra annotation for custom configmap for Controller + configAnnotations: {} + # annotationKey: value + + ## Controller Logging configuration + ## Careful: this block will be ignored if you use config.cr-global. + ## In this case, move your logging config in entry spec.log_targets in your CR. + logging: + ## Controller logging level + ## This only relevant to Controller logs + level: info + + ## HAProxy traffic logs + ## ref: https://github.com/haproxytech/kubernetes-ingress/tree/master/documentation#logging + traffic: {} + # address: "stdout" + # format: "raw" + # facility: "daemon" + + ## Mirrors the address of the service's endpoints to the + ## load-balancer status of all Ingress objects it satisfies. + publishService: + enabled: true + ## + ## Override of the publish service + ## Must be / + pathOverride: "" + + ## Controller Service configuration + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/ + service: + enabled: true # set to false when controller.kind is 'DaemonSet' and controller.daemonset.useHostPorts is true + + type: NodePort # can be 'ClusterIP', 'NodePort' or 'LoadBalancer' + + ## Service annotations + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + annotations: {} + + ## Service labels + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + labels: {} + + ## Health check node port + ## ref: https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip + healthCheckNodePort: 0 + + ## Service nodePorts to use for http, https and stat + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/ + ## If empty, random ports will be used + nodePorts: {} + # http: 31080 + # https: 31443 + # stat: 31024 + # admin: 31060 + + ## Service ports to use for http, https and stat + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/ + ports: + http: 80 + https: 443 + stat: 1024 + admin: 6060 + + ## The controller service ports for http, https and stat can be disabled by + ## setting below to false - this could be useful when only deploying haproxy + ## as a TCP loadbalancer + ## Note: At least one port (http, https, stat or from tcpPorts) has to be enabled + enablePorts: + http: true + https: true + quic: true + stat: true + admin: true + + ## Target port mappings for http, https and stat + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/ + targetPorts: + http: http + https: https + quic: quic + stat: stat + admin: admin + + ## Additional tcp ports to expose + ## This is especially useful for TCP services: + ## https://github.com/haproxytech/kubernetes-ingress/blob/master/documentation/controller.md + tcpPorts: [] + # - name: http-alt + # port: 8080 + # targetPort: http-alt + # nodePort: 32080 + + ## Set external traffic policy + ## Default is "Cluster", setting it to "Local" preserves source IP + ## ref: https://kubernetes.io/docs/tutorials/services/source-ip/#source-ip-for-services-with-typeloadbalancer + # externalTrafficPolicy: "Local" + + ## Expose service via external IPs that route to one or more cluster nodes + externalIPs: [] + + ## LoadBalancer IP + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer + loadBalancerIP: "" + + ## Source IP ranges permitted to access Network Load Balancer + ## ref: https://kubernetes.io/docs/tasks/access-application-cluster/configure-cloud-provider-firewall/ + loadBalancerSourceRanges: [] + + ## Class of load balancer implementation + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#load-balancer-class + loadBalancerClass: null + + ## Service ClusterIP + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/ + # clusterIP: "" + + ## IPv4/IPv6 dual-stack + ## ref: https://kubernetes.io/docs/concepts/services-networking/dual-stack/ + ## + # ipFamilies: [IPv4, IPv6] + # ipFamilyPolicy: PreferDualStack + + ## Service session affinity + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/ + # sessionAffinity: "" + + ## Controller Metrics Service configuration + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/ + metrics: + type: ClusterIP # can be 'ClusterIP', 'NodePort' or 'LoadBalancer' + + ## Service annotations + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + annotations: {} + + ## Service labels + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + labels: {} + + ## Controller Deployment configuration + ## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/ + deployment: + useHostNetwork: false # also modify dnsPolicy accordingly + useHostPort: false + hostIP: null + hostPorts: + http: 80 + https: 443 + stat: 1024 + + ## Controller DaemonSet configuration + ## ref: https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/ + daemonset: + useHostNetwork: false # also modify dnsPolicy accordingly + useHostPort: false + hostIP: null + hostPorts: + http: 80 + https: 443 + stat: 1024 + + ## Controller deployment strategy definition + ## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy + ## ref: https://kubernetes.io/docs/tasks/manage-daemon/update-daemon-set/ + strategy: + type: RollingUpdate + + ## Controller Pod PriorityClass + ## ref: https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass + priorityClassName: "" + + ## Pod runtime class name + ## ref: https://kubernetes.io/docs/concepts/containers/runtime-class/ + runtimeClassName: "" + + ## Controller container lifecycle handlers + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/attach-handler-lifecycle-event/ + lifecycle: {} + ## Example preStop for graceful shutdown + # preStop: + # exec: + # command: ["/bin/sh", "-c", "kill -USR1 $(pidof haproxy); while killall -0 haproxy; do sleep 1; done"] + + ## Set additional environment variables + extraEnvs: [] + ## Set TZ env to configure timezone on controller containers + # - name: TZ + # value: "Etc/UTC" + + ## Use envFrom to add env vars from a secret or ConfigMap to the HAProxy container + ## ref: https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/ + extraEnvFrom: [] + ## Example passing the pod IP into a container + # - configMapRef: + # name: ha-env-config + + ## Add additional containers + extraContainers: [] + ## Example sidecar + # - name: sidecar + # image: alpine # alpine is a simple Linux OS image + # command: ["/bin/sh"] + # args: ["-c", "while true; do date; sleep 5;done"] + + ## Additional volumeMounts to the controller main container + extraVolumeMounts: [] + ## Example empty volume mounts when using securityContext->readOnlyRootFilesystem + # - name: etc-haproxy + # mountPath: /etc/haproxy + # - name: tmp + # mountPath: /tmp + # - name: var-state-haproxy + # mountPath: /var/state/haproxy + + ## Additional volumes to the controller pod + extraVolumes: [] + ## Example empty volumes when using securityContext->readOnlyRootFilesystem + # - name: etc-haproxy + # emptyDir: {} + # - name: tmp + # emptyDir: {} + # - name: var-state-haproxy + # emptyDir: {} + + ## ServiceMonitor + ## ref: https://github.com/prometheus-operator/prometheus-operator/blob/master/Documentation/user-guides/getting-started.md + ## Note: requires Prometheus Operator to be able to work, for example: + ## helm install prometheus prometheus-community/kube-prometheus-stack \ + ## --set prometheus.prometheusSpec.podMonitorSelectorNilUsesHelmValues=false \ + ## --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false + serviceMonitor: + ## Toggle the ServiceMonitor true if you have Prometheus Operator installed and configured + ## Should not be enabled when controller.podMonitor.enabled is true + enabled: false + + ## Specify the labels to add to the ServiceMonitors to be selected for target discovery + extraLabels: {} + + ## Specify the endpoints + ## ref: https://github.com/prometheus-operator/prometheus-operator/blob/master/Documentation/design.md#servicemonitor + endpoints: + - port: stat + path: /metrics + scheme: http + interval: 30s + + ## PodMonitor + ## ref: https://github.com/prometheus-operator/prometheus-operator/blob/master/Documentation/user-guides/getting-started.md + ## Note: requires Prometheus Operator to be able to work, for example: + ## helm install prometheus prometheus-community/kube-prometheus-stack \ + ## --set prometheus.prometheusSpec.podMonitorSelectorNilUsesHelmValues=false \ + ## --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false + podMonitor: + ## Toggle the PodMonitor true if you have Prometheus Operator installed and configured + ## Should not be enabled when controller.serviceMonitor.enabled is true + enabled: false + + ## Specify the labels to add to the PodMonitors to be selected for target discovery + extraLabels: {} + + ## Specify the endpoints + ## ref: https://github.com/prometheus-operator/prometheus-operator/blob/master/Documentation/design.md#podMonitor + endpoints: + - port: stat + path: /metrics + scheme: http + interval: 30s + params: + scope: + - global + - frontend + - backend + + ## Controller sync mode with Kubernetes + ## Note: requires Enterprise Kubernetes Ingress Controller + ## Possible values: 'default' or 'fetch' + ## - 'default': the sync is done based on K8s informers (event based) + ## - 'fetch': the controller pulls data periodically (from K8s or from proxy) + sync: + mode: default # can be 'default' or 'fetch' + fetchParams: # Mandatory if mode is 'fetch' + # period: 3s # optional, default is 5s + source: k8s # possible values are: 'proxy', 'k8s' + proxyParams: # Mandatory if source is 'proxy' + replicaCount: 3 # number of replicas of the proxy, mandatory if source is 'proxy' + proxySvcLabelSelector: run:haproxy-ingress-proxy # label selector of the proxy service, mandatory if source is 'proxy' + +## CRD job default values +crdjob: + ## Additional annotations to add to the pod container metadata + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + podAnnotations: {} + # key: value + + ## Automatic job cleanup + ## ref: https://kubernetes.io/docs/concepts/workloads/controllers/ttlafterfinished/ + ttlSecondsAfterFinished: 600 + + ## Pod Node assignment + ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ + nodeSelector: {} + + ## Node Taints and Tolerations for pod-node cheduling through attraction/repelling + ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ + tolerations: [] + # - key: "key" + # operator: "Equal|Exists" + # value: "value" + # effect: "NoSchedule|PreferNoSchedule|NoExecute(1.6 only)" + + ## Node Affinity for pod-node scheduling constraints + ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity + affinity: {} + + ## Compute Resources for the CRD Job + ## ref: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/ + resources: + # limits: + # cpu: 250m + # memory: 400Mi + requests: + cpu: 250m + memory: 400Mi diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..8b1abae --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,18 @@ +site_name: haproxy-unified +site_description: custom deployed via ArgoCD +docs_dir: docs +exclude_docs: | + node_modules/ + vendor/ + .git/ + build/ + dist/ + site/ + __pycache__/ + *.tar.gz + *.jar + *.zip + +plugins: + - techdocs-core + - awesome-pages diff --git a/overlays/deploy/kustomization.yaml b/overlays/deploy/kustomization.yaml new file mode 100644 index 0000000..edb22f6 --- /dev/null +++ b/overlays/deploy/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# NOTE: Requires kustomize.buildOptions: --enable-helm in ArgoCD argocd-cm ConfigMap +helmCharts: + - name: haproxy-unified + repo: ../../kubernetes-manifests + releaseName: haproxy-unified + namespace: demo-apps + valuesFile: values.yaml +resources: + - k6-configmap.yaml + + diff --git a/overlays/ingress/ingress.yaml b/overlays/ingress/ingress.yaml new file mode 100644 index 0000000..253fcbd --- /dev/null +++ b/overlays/ingress/ingress.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: haproxy-unified + namespace: demo-apps + annotations: + kubernetes.io/ingress.class: nginx + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: nginx + tls: + - hosts: + - haproxy-unified.kyndemo.live + secretName: haproxy-unified-kyndemo-live-tls + rules: + - host: haproxy-unified.kyndemo.live + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: frontend + port: + number: 80 diff --git a/overlays/ingress/kustomization.yaml b/overlays/ingress/kustomization.yaml new file mode 100644 index 0000000..fb80823 --- /dev/null +++ b/overlays/ingress/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - ingress.yaml diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..676c61b --- /dev/null +++ b/test/README.md @@ -0,0 +1,257 @@ +# Helm Chart Testing + +Three test scripts are provided: + +1. **`local-test.sh`** — Offline lint + template validation (no cluster needed) +2. **`integration-test.sh`** — Deploy to a real Kind cluster and verify resources +3. **`ct-test.sh`** — Wrapper around [chart-testing](https://github.com/helm/chart-testing) (`ct`), matching what CircleCI runs + +## 1. Offline Tests (`local-test.sh`) + +Runs `helm lint` and `helm template` — no cluster required. + +### Prerequisites + +- [Helm](https://helm.sh/docs/intro/install/) v3.x + +### Usage + +```bash +# Test all charts +./test/local-test.sh + +# Test a specific chart +./test/local-test.sh haproxy-unified-gateway +./test/local-test.sh kubernetes-ingress +``` + +### What It Tests + +| Test | Description | +|------|-------------| +| **Chart.yaml metadata** | Verifies required fields (`name`, `version`, `appVersion`, `description`) | +| **helm lint (defaults)** | Runs `helm lint` with default values | +| **helm template (defaults)** | Renders templates and checks for errors | +| **Deployment vs DaemonSet** | Confirms charts that support `controller.kind` render the correct resource type | +| **HugConf cleanup hook** | Verifies cleanup hook renders/skips based on `hugconf.create` and targets correct name | +| **Metrics port** | Verifies metrics container port (31060) renders, `--metrics-auth` flag is correct for both `kube-rbac` (default) and `none`, metrics Service has both `stat` and `metrics` ports | +| **ServiceMonitor/PodMonitor** | Renders with `monitoring.coreos.com/v1` API, skipped without it, metrics Service only with ServiceMonitor | +| **CI values (lint + template)** | Lint and template for every `ci/*.yaml` file | + +--- + +## 2. Integration Tests (`integration-test.sh`) + +Installs charts into a real Kubernetes cluster, waits for pods to become Ready, +verifies key resources exist, then cleans up. + +By default the script creates a [Kind](https://kind.sigs.k8s.io/) cluster named +`dev-helm-charts`, runs all tests, and deletes the cluster when done. + +### Prerequisites + +- [Kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) +- [Helm](https://helm.sh/docs/intro/install/) v3.x +- `kubectl` + +### Usage + +```bash +# Test all charts (creates + destroys Kind cluster automatically) +./test/integration-test.sh + +# Test a specific chart +./test/integration-test.sh haproxy-unified-gateway +./test/integration-test.sh kubernetes-ingress +``` + +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `TIMEOUT` | `120` | Max seconds to wait for pods per install | +| `KEEP_NS` | `0` | Set to `1` to keep test namespaces after run (for debugging) | +| `CI_FILTER` | _(empty)_ | Glob pattern to filter `ci/` values files (e.g. `deployment-*`) | +| `TEST_FILTER` | _(empty)_ | Run only a specific test: `defaults`, `daemonset`, `hpa`, `pdb`, `metrics-port`, `monitoring`, `hugconf-cleanup`, `ci` | +| `KIND_KEEP_CLUSTER` | `0` | Set to `1` to keep the Kind cluster after tests finish | +| `KIND_CLUSTER_NAME` | `dev-helm-charts` | Custom Kind cluster name | +| `SKIP_KIND` | `0` | Set to `1` to skip Kind management and use your existing kubeconfig | + +### Examples + +```bash +# Run tests and keep the Kind cluster for manual inspection afterwards +KIND_KEEP_CLUSTER=1 ./test/integration-test.sh haproxy-unified-gateway + +# Re-run tests against the cluster you kept (reuses existing cluster) +./test/integration-test.sh haproxy-unified-gateway + +# Delete the cluster manually when done +kind delete cluster --name dev-helm-charts + +# Use your own cluster instead of Kind +SKIP_KIND=1 ./test/integration-test.sh haproxy-unified-gateway + +# Longer timeout for slow clusters +TIMEOUT=300 ./test/integration-test.sh haproxy-unified-gateway + +# Only run deployment-related ci/ test cases +CI_FILTER="deployment-*" ./test/integration-test.sh haproxy-unified-gateway + +# Run only the monitoring test (ServiceMonitor/PodMonitor) +TEST_FILTER=monitoring ./test/integration-test.sh haproxy-unified-gateway + +# Run only the metrics port test +TEST_FILTER=metrics-port ./test/integration-test.sh haproxy-unified-gateway + +# Run only the HugConf cleanup test +TEST_FILTER=hugconf-cleanup ./test/integration-test.sh haproxy-unified-gateway + +# Run only the defaults test +TEST_FILTER=defaults ./test/integration-test.sh haproxy-unified-gateway + +# Keep namespaces after the run for manual inspection +KEEP_NS=1 ./test/integration-test.sh haproxy-unified-gateway + +# Then clean up manually when done +kubectl get ns | grep '^it-' | awk '{print $1}' | xargs kubectl delete ns +``` + +### What It Tests + +For each chart, the script runs these phases: + +| Test | Description | +|------|-------------| +| **Install (defaults)** | `helm install` with default values, wait for pods, verify resources | +| **Install (DaemonSet)** | Same but with `controller.kind=DaemonSet` (if chart supports it) | +| **Install (HPA)** | Enables HPA, verifies HPA resource is created | +| **Install (PDB)** | Enables PDB, verifies PDB resource is created | +| **Metrics port** | Verifies controller pod has metrics container port 31060, `--metrics-auth=kube-rbac` arg, and main Service has stat port | +| **Install (monitoring)** | Installs Prometheus Operator CRDs, verifies ServiceMonitor/PodMonitor/metrics Service with both stat + metrics ports | +| **HugConf cleanup** | Verifies HugConf CR is deleted after `helm uninstall` | +| **Install ci/ values** | Installs with each `ci/*.yaml` file, waits for pods | + +Each test: +1. Creates an isolated namespace (`it---`) +2. Runs `helm install --wait` +3. Verifies pods reach Running/Completed state +4. On failure: dumps pod status, events, and container logs +5. Runs `helm uninstall` and deletes the namespace + +### On Failure + +When a test fails, the script automatically prints: +- Pod status (`kubectl get pods -o wide`) +- Recent events (`kubectl get events`) +- Container logs from unhealthy pods (last 20 lines) + +--- + +## 3. Chart Testing (`ct-test.sh`) + +Wrapper around [chart-testing](https://github.com/helm/chart-testing) (`ct`) — the same +tool CircleCI uses. Runs `ct lint` and `ct install` locally so you can validate before pushing. + +### Prerequisites + +- [ct](https://github.com/helm/chart-testing#installation) v3.x +- [yamale](https://github.com/23andMe/Yamale) (`pipx install yamale`) +- [yamllint](https://github.com/adrienverber/yamllint) (`pipx install yamllint`) +- [Helm](https://helm.sh/docs/intro/install/) v3.x +- [Kind](https://kind.sigs.k8s.io/) — only for `install` mode +- `kubectl` — only for `install` mode + +### ct Usage + +```bash +# Lint all charts +./test/ct-test.sh lint + +# Lint a specific chart +./test/ct-test.sh lint haproxy-unified-gateway + +# Install all charts on a Kind cluster (creates + destroys automatically) +./test/ct-test.sh install + +# Install a specific chart +./test/ct-test.sh install haproxy-unified-gateway + +# Lint + install in one go +./test/ct-test.sh all haproxy-unified-gateway +``` + +### ct Environment Variables + +| Variable | Default | Description | +| ------------------- | --------- | ---------------------------------------------------------------- | +| `KIND_CLUSTER_NAME` | `ct-dev` | Kind cluster name | +| `KIND_KEEP_CLUSTER` | `0` | Set to `1` to keep the Kind cluster after tests | +| `SKIP_KIND` | `0` | Set to `1` to skip Kind management and use existing kubeconfig | +| `CT_ARGS` | _(empty)_ | Extra arguments passed to `ct` (e.g. `--debug`) | + +### ct Examples + +```bash +# Lint with debug output +CT_ARGS="--debug" ./test/ct-test.sh lint haproxy-unified-gateway + +# Install and keep cluster for inspection +KIND_KEEP_CLUSTER=1 ./test/ct-test.sh install haproxy-unified-gateway + +# Use an existing cluster +SKIP_KIND=1 ./test/ct-test.sh install haproxy-unified-gateway + +# Delete the cluster manually when done +kind delete cluster --name ct-dev +``` + +### CircleCI Compatibility + +The CircleCI pipeline (`.circleci/config.yml`) runs: + +1. `ct lint --all` — validates Chart.yaml schema, runs `helm lint` with each `ci/*.yaml` file +2. `ct install --all` — installs each chart with each `ci/*.yaml` file on a Kind cluster + +`ct-test.sh` runs the exact same commands locally using the config in `test/ct.yaml`, +so if it passes locally it will pass in CI. + +--- + +## CI Values Files + +Both scripts use the `ci/` values files in each chart directory. These are the same +files used by [chart-testing](https://github.com/helm/chart-testing) (`ct`) in CircleCI. + +Naming convention: +``` +--values.yaml +``` + +Examples: +- `deployment-default-values.yaml` — Deployment with defaults +- `daemonset-hostport-values.yaml` — DaemonSet with host port mapping +- `deployment-hpa-values.yaml` — Deployment with HPA enabled + +## Adding New Test Cases + +1. Create a new values file in `/ci/` following the naming convention. +2. Validate offline: + + ```bash + ./test/local-test.sh + ``` + +3. Verify ct lint passes (matches CircleCI): + + ```bash + ./test/ct-test.sh lint + ``` + +4. Deploy to a cluster: + + ```bash + CI_FILTER="" ./test/integration-test.sh + ``` + +5. The file is automatically picked up by all three scripts and CircleCI's `ct lint`. diff --git a/test/ct-test.sh b/test/ct-test.sh new file mode 100755 index 0000000..64e341e --- /dev/null +++ b/test/ct-test.sh @@ -0,0 +1,205 @@ +#!/bin/bash +# +# Chart-testing (ct) wrapper for local development. +# +# Uses ct to lint and optionally install Helm charts, matching what +# CircleCI runs but locally against a Kind cluster. +# +# Prerequisites: +# - ct (https://github.com/helm/chart-testing) +# - kind (https://kind.sigs.k8s.io/) — only for install tests +# - kubectl, helm +# +# Usage: +# ./test/ct-test.sh # lint all charts +# ./test/ct-test.sh lint # lint all charts +# ./test/ct-test.sh lint haproxy-unified-gateway # lint one chart +# ./test/ct-test.sh install # install all charts (creates Kind cluster) +# ./test/ct-test.sh install haproxy-unified-gateway # install one chart +# +# Options (env vars): +# KIND_CLUSTER_NAME=name Custom cluster name (default: ct-dev) +# KIND_KEEP_CLUSTER=1 Don't delete the Kind cluster after tests +# SKIP_KIND=1 Skip Kind cluster management (use existing kubeconfig) +# CT_ARGS="..." Extra arguments passed to ct (e.g. "--debug") +# + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CT_CONFIG="${SCRIPT_DIR}/ct.yaml" +CT_VERSION="${CT_VERSION:-v3.10.0}" +KIND_CLUSTER_NAME="${KIND_CLUSTER_NAME:-ct-dev}" +KIND_KEEP_CLUSTER="${KIND_KEEP_CLUSTER:-0}" +SKIP_KIND="${SKIP_KIND:-0}" +CT_ARGS="${CT_ARGS:-}" +KIND_CREATED=false + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BOLD='\033[1m' +NC='\033[0m' + +# --- prerequisites --- + +check_tool() { + if ! command -v "$1" &>/dev/null; then + echo -e "${RED}ERROR:${NC} '$1' not found in PATH" >&2 + return 1 + fi +} + +# --- ct config files --- + +# Download chart_schema.yaml and lintconf.yaml from the ct release matching CT_VERSION. +ensure_ct_configs() { + local base_url="https://raw.githubusercontent.com/helm/chart-testing/${CT_VERSION}/etc" + local files=(chart_schema.yaml lintconf.yaml) + + for f in "${files[@]}"; do + if [ ! -f "${SCRIPT_DIR}/${f}" ]; then + echo -e "Downloading ${f} (ct ${CT_VERSION})..." + if ! curl -sSfL "${base_url}/${f}" -o "${SCRIPT_DIR}/${f}"; then + echo -e "${RED}ERROR:${NC} failed to download ${f}" >&2 + exit 1 + fi + fi + done +} + +# --- Kind cluster management --- + +create_kind_cluster() { + if [ "$SKIP_KIND" = "1" ]; then + echo -e "${YELLOW}SKIP_KIND=1, using existing kubeconfig${NC}" + return + fi + + check_tool kind + + if kind get clusters 2>/dev/null | grep -q "^${KIND_CLUSTER_NAME}$"; then + echo -e "Kind cluster ${BOLD}${KIND_CLUSTER_NAME}${NC} already exists, reusing" + kubectl cluster-info --context "kind-${KIND_CLUSTER_NAME}" &>/dev/null || { + echo -e "${RED}ERROR:${NC} cluster exists but is not reachable" >&2 + exit 1 + } + kubectl config use-context "kind-${KIND_CLUSTER_NAME}" &>/dev/null || true + return + fi + + echo -e "Creating Kind cluster ${BOLD}${KIND_CLUSTER_NAME}${NC}..." + kind create cluster --name "${KIND_CLUSTER_NAME}" --wait 120s + KIND_CREATED=true + echo -e "${GREEN}Kind cluster created${NC}" +} + +delete_kind_cluster() { + if [ "$SKIP_KIND" = "1" ]; then + return + fi + if [ "$KIND_KEEP_CLUSTER" = "1" ]; then + echo -e "\n${YELLOW}KIND_KEEP_CLUSTER=1, keeping cluster '${KIND_CLUSTER_NAME}'${NC}" + echo -e " Delete: kind delete cluster --name ${KIND_CLUSTER_NAME}" + return + fi + if $KIND_CREATED || kind get clusters 2>/dev/null | grep -q "^${KIND_CLUSTER_NAME}$"; then + echo -e "\nDeleting Kind cluster ${BOLD}${KIND_CLUSTER_NAME}${NC}..." + kind delete cluster --name "${KIND_CLUSTER_NAME}" + echo -e "${GREEN}Kind cluster deleted${NC}" + fi +} + +# --- ct commands --- + +do_lint() { + local chart_arg="$1" + + echo -e "\n${BOLD}=== ct lint ===${NC}" + + local ct_cmd=(ct lint --config "$CT_CONFIG") + + if [ -n "$chart_arg" ]; then + ct_cmd+=(--charts "$REPO_ROOT/$chart_arg") + else + ct_cmd+=(--all) + fi + + # shellcheck disable=SC2086 + "${ct_cmd[@]}" $CT_ARGS + echo -e "${GREEN}ct lint passed${NC}" +} + +do_install() { + local chart_arg="$1" + + echo -e "\n${BOLD}=== ct install ===${NC}" + + check_tool kubectl + check_tool helm + + create_kind_cluster + trap delete_kind_cluster EXIT + + # Verify cluster is reachable + if ! kubectl cluster-info &>/dev/null; then + echo -e "${RED}ERROR:${NC} cannot connect to Kubernetes cluster" >&2 + exit 1 + fi + + echo "Cluster: $(kubectl config current-context 2>/dev/null || echo 'unknown')" + + local ct_cmd=(ct install --config "$CT_CONFIG") + + if [ -n "$chart_arg" ]; then + ct_cmd+=(--charts "$REPO_ROOT/$chart_arg") + else + ct_cmd+=(--all) + fi + + # shellcheck disable=SC2086 + "${ct_cmd[@]}" $CT_ARGS + echo -e "${GREEN}ct install passed${NC}" +} + +# --- main --- + +main() { + local mode="${1:-lint}" + local chart="${2:-}" + + check_tool ct + check_tool helm + ensure_ct_configs + + echo -e "${BOLD}Chart Testing (ct) — local${NC}" + echo "Repo: $REPO_ROOT" + echo "Config: $CT_CONFIG" + echo "ct: $(ct version 2>&1 | head -1)" + echo "Helm: $(helm version --short 2>/dev/null)" + echo "Mode: $mode" + [ -n "$chart" ] && echo "Chart: $chart" + + cd "$REPO_ROOT" + + case "$mode" in + lint) + do_lint "$chart" + ;; + install) + do_install "$chart" + ;; + all) + do_lint "$chart" + do_install "$chart" + ;; + *) + echo -e "${RED}Unknown mode:${NC} $mode" + echo "Usage: $0 [lint|install|all] [chart-name]" + exit 1 + ;; + esac +} + +main "$@" diff --git a/test/ct.yaml b/test/ct.yaml new file mode 100644 index 0000000..2d6fd52 --- /dev/null +++ b/test/ct.yaml @@ -0,0 +1,5 @@ +chart-dirs: + - . +check-version-increment: false +chart-yaml-schema: test/chart_schema.yaml +lint-conf: test/lintconf.yaml diff --git a/test/integration-test.sh b/test/integration-test.sh new file mode 100755 index 0000000..dcdd341 --- /dev/null +++ b/test/integration-test.sh @@ -0,0 +1,784 @@ +#!/bin/bash +# +# Integration test: deploy Helm chart(s) to a real Kubernetes cluster. +# +# By default, creates a Kind cluster named "dev-helm-charts", runs the tests, +# and deletes the cluster when done. Set KIND_KEEP_CLUSTER=1 to keep it. +# +# Prerequisites: +# - kind (https://kind.sigs.k8s.io/) +# - kubectl +# - Helm v3.x +# +# Usage: +# ./test/integration-test.sh # test all charts +# ./test/integration-test.sh haproxy-unified-gateway # test only HUG +# ./test/integration-test.sh kubernetes-ingress # test only ingress +# +# Options (env vars): +# TIMEOUT=120 Max seconds to wait for pods (default: 120) +# KEEP_NS=1 Don't delete test namespaces on success (for debugging) +# CI_FILTER= Only test ci/ files matching pattern (e.g. "deployment-*") +# TEST_FILTER= Run only a specific test (defaults, daemonset, hpa, pdb, monitoring, ci) +# KIND_KEEP_CLUSTER=1 Don't delete the Kind cluster after tests +# KIND_CLUSTER_NAME=name Custom cluster name (default: dev-helm-charts) +# SKIP_KIND=1 Skip Kind cluster creation (use existing kubeconfig) +# + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +TIMEOUT="${TIMEOUT:-120}" +KEEP_NS="${KEEP_NS:-0}" +CI_FILTER="${CI_FILTER:-}" +KIND_KEEP_CLUSTER="${KIND_KEEP_CLUSTER:-0}" +KIND_CLUSTER_NAME="${KIND_CLUSTER_NAME:-dev-helm-charts}" +SKIP_KIND="${SKIP_KIND:-0}" +TEST_FILTER="${TEST_FILTER:-}" +PASSED=0 +FAILED=0 +SKIPPED=0 +FAILURES=() +NAMESPACES=() +KIND_CREATED=false +KEDA_CRDS_INSTALLED=false + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BOLD='\033[1m' +NC='\033[0m' + +log_pass() { echo -e " ${GREEN}PASS${NC} $1"; PASSED=$((PASSED + 1)); } +log_fail() { echo -e " ${RED}FAIL${NC} $1"; FAILED=$((FAILED + 1)); FAILURES+=("$1"); } +log_skip() { echo -e " ${YELLOW}SKIP${NC} $1"; SKIPPED=$((SKIPPED + 1)); } +log_section() { echo -e "\n${BOLD}=== $1 ===${NC}"; } +log_info() { echo -e " ${YELLOW}INFO${NC} $1"; } + +# --- Kind cluster management --- + +create_kind_cluster() { + if [ "$SKIP_KIND" = "1" ]; then + echo -e "${YELLOW}SKIP_KIND=1, using existing kubeconfig${NC}" + return + fi + + if ! command -v kind &>/dev/null; then + echo -e "${RED}ERROR:${NC} 'kind' not found in PATH" >&2 + echo "Install it: https://kind.sigs.k8s.io/docs/user/quick-start/#installation" >&2 + exit 1 + fi + + # Check if cluster already exists + if kind get clusters 2>/dev/null | grep -q "^${KIND_CLUSTER_NAME}$"; then + echo -e "Kind cluster ${BOLD}${KIND_CLUSTER_NAME}${NC} already exists, reusing it" + kubectl cluster-info --context "kind-${KIND_CLUSTER_NAME}" &>/dev/null || { + echo -e "${RED}ERROR:${NC} cluster exists but is not reachable" >&2 + exit 1 + } + export KUBECONFIG + KUBECONFIG=$(kind get kubeconfig-path --name="${KIND_CLUSTER_NAME}" 2>/dev/null || echo "") + kubectl config use-context "kind-${KIND_CLUSTER_NAME}" &>/dev/null || true + return + fi + + echo -e "Creating Kind cluster ${BOLD}${KIND_CLUSTER_NAME}${NC}..." + kind create cluster \ + --name "${KIND_CLUSTER_NAME}" \ + --wait 120s + KIND_CREATED=true + echo -e "${GREEN}Kind cluster created${NC}" +} + +delete_kind_cluster() { + if [ "$SKIP_KIND" = "1" ]; then + return + fi + + if [ "$KIND_KEEP_CLUSTER" = "1" ]; then + echo -e "\n${YELLOW}KIND_KEEP_CLUSTER=1, keeping cluster '${KIND_CLUSTER_NAME}'${NC}" + echo -e " To use: kubectl --context kind-${KIND_CLUSTER_NAME} ..." + echo -e " Delete: kind delete cluster --name ${KIND_CLUSTER_NAME}" + return + fi + + if $KIND_CREATED || kind get clusters 2>/dev/null | grep -q "^${KIND_CLUSTER_NAME}$"; then + echo -e "\nDeleting Kind cluster ${BOLD}${KIND_CLUSTER_NAME}${NC}..." + kind delete cluster --name "${KIND_CLUSTER_NAME}" + echo -e "${GREEN}Kind cluster deleted${NC}" + fi +} + +# --- helpers --- + +check_prerequisites() { + local ok=true + for cmd in kubectl helm; do + if ! command -v "$cmd" &>/dev/null; then + echo -e "${RED}ERROR:${NC} '$cmd' not found in PATH" >&2 + ok=false + fi + done + if ! kubectl cluster-info &>/dev/null; then + echo -e "${RED}ERROR:${NC} cannot connect to Kubernetes cluster" >&2 + ok=false + fi + if ! $ok; then + exit 1 + fi +} + +find_charts() { + local filter="${1:-}" + local charts=() + for dir in "$REPO_ROOT"/*/; do + [ -f "$dir/Chart.yaml" ] || continue + local name + name="$(basename "$dir")" + if [ -n "$filter" ] && [ "$name" != "$filter" ]; then + continue + fi + charts+=("$name") + done + if [ ${#charts[@]} -eq 0 ]; then + echo "No charts found${filter:+ matching '$filter'}." >&2 + exit 1 + fi + echo "${charts[@]}" +} + +# Generate a unique namespace for a test run +make_ns() { + local chart="$1" + local suffix="$2" + # namespace max 63 chars; keep it short + local ns="it-${chart}-${suffix}-$(date +%s)" + ns="${ns:0:63}" + echo "$ns" +} + +create_ns() { + local ns="$1" + kubectl create namespace "$ns" --dry-run=client -o yaml | kubectl apply -f - >/dev/null + NAMESPACES+=("$ns") +} + +delete_ns() { + local ns="$1" + if [ "$KEEP_NS" = "1" ]; then + log_info "KEEP_NS=1, not deleting namespace $ns" + return + fi + kubectl delete namespace "$ns" --wait=false --ignore-not-found >/dev/null 2>&1 || true +} + +# Wait for all pods in a namespace to be Ready (or Completed for Jobs) +wait_for_pods() { + local ns="$1" + local deadline=$((SECONDS + TIMEOUT)) + + while [ $SECONDS -lt $deadline ]; do + local not_ready=0 + local pod_lines + pod_lines=$(kubectl get pods -n "$ns" --no-headers 2>/dev/null || true) + + if [ -z "$pod_lines" ]; then + # No pods yet, keep waiting + sleep 2 + continue + fi + + while IFS= read -r line; do + local status + status=$(echo "$line" | awk '{print $3}') + case "$status" in + Running|Completed|Succeeded) + # Check if Running pods have all containers ready + if [ "$status" = "Running" ]; then + local ready_col + ready_col=$(echo "$line" | awk '{print $2}') + local ready_count="${ready_col%/*}" + local total_count="${ready_col#*/}" + if [ "$ready_count" != "$total_count" ]; then + not_ready=$((not_ready + 1)) + fi + fi + ;; + Error|CrashLoopBackOff|ImagePullBackOff|ErrImagePull|CreateContainerConfigError|InvalidImageName) + # Terminal failure + return 1 + ;; + *) + not_ready=$((not_ready + 1)) + ;; + esac + done <<< "$pod_lines" + + if [ "$not_ready" -eq 0 ]; then + return 0 + fi + sleep 3 + done + return 1 # timeout +} + +# Collect debug info on failure +dump_debug() { + local ns="$1" + echo -e " ${YELLOW}--- debug info (ns: $ns) ---${NC}" + echo " Pods:" + kubectl get pods -n "$ns" -o wide 2>/dev/null | sed 's/^/ /' + echo " Events (last 10):" + kubectl get events -n "$ns" --sort-by='.lastTimestamp' 2>/dev/null | tail -10 | sed 's/^/ /' + + # Show logs from non-ready pods + local pods + pods=$(kubectl get pods -n "$ns" --no-headers 2>/dev/null | awk '$3 !~ /Running|Completed|Succeeded/ {print $1}') + if [ -z "$pods" ]; then + # Also grab CrashLoopBackOff pods that show as "Running" + pods=$(kubectl get pods -n "$ns" --no-headers 2>/dev/null | awk '{split($2,a,"/"); if (a[1]!=a[2]) print $1}') + fi + for pod in $pods; do + echo " Logs ($pod):" + kubectl logs -n "$ns" "$pod" --all-containers --tail=20 2>/dev/null | sed 's/^/ /' || true + done + echo -e " ${YELLOW}--- end debug ---${NC}" +} + +# --- test functions --- + +# Return chart-specific helm args needed for test installs. +# Some charts need overrides to install cleanly on a fresh cluster. +chart_test_args() { + local chart="$1" + case "$chart" in + haproxy-unified-gateway) + # No special overrides needed: the HugConf CR is a post-install hook + # (weight 5) that runs after the CRD job (weight 0), avoiding the + # chicken-and-egg validation problem. + ;; + esac +} + +# Install chart with given values, wait for pods, verify resources, then clean up. +install_and_verify() { + local chart="$1" + local label="$2" + local ns="$3" + shift 3 + # Remaining args are passed to helm install + local helm_args=("$@") + # Add chart-specific test overrides + local extra_args + extra_args=$(chart_test_args "$chart") + if [ -n "$extra_args" ]; then + # shellcheck disable=SC2206 + helm_args+=($extra_args) + fi + + create_ns "$ns" + + # Install + if ! helm install "test-${chart}" "$REPO_ROOT/$chart" \ + --namespace "$ns" \ + --wait --timeout "${TIMEOUT}s" \ + "${helm_args[@]}" 2>&1 | sed 's/^/ /'; then + log_fail "$label (helm install failed)" + dump_debug "$ns" + delete_ns "$ns" + return + fi + + # Wait for pods to be ready (belt-and-suspenders on top of --wait) + if ! wait_for_pods "$ns"; then + log_fail "$label (pods not ready within ${TIMEOUT}s)" + dump_debug "$ns" + helm uninstall "test-${chart}" -n "$ns" --wait 2>/dev/null || true + delete_ns "$ns" + return + fi + + # Basic resource verification + local ok=true + + # Check that we have at least one Running or Completed pod + local running + running=$(kubectl get pods -n "$ns" --no-headers 2>/dev/null | grep -cE 'Running|Completed|Succeeded' || true) + if [ "$running" -eq 0 ]; then + echo -e " ${RED}No running pods found${NC}" + ok=false + fi + + # Verify ServiceAccount exists + if kubectl get serviceaccount -n "$ns" -l "app.kubernetes.io/instance=test-${chart}" --no-headers 2>/dev/null | grep -q .; then + : # ok + else + # Some charts may not label SA; check by name + if ! kubectl get serviceaccount -n "$ns" 2>/dev/null | grep -q "test-${chart}\|${chart}"; then + echo -e " ${YELLOW}ServiceAccount not found (may be expected)${NC}" + fi + fi + + # Verify Service exists (if applicable) + local svc_count + svc_count=$(kubectl get svc -n "$ns" --no-headers 2>/dev/null | wc -l) + if [ "$svc_count" -gt 0 ]; then + : # ok + fi + + if $ok; then + log_pass "$label" + else + log_fail "$label" + dump_debug "$ns" + fi + + # Cleanup + helm uninstall "test-${chart}" -n "$ns" --wait 2>/dev/null || true + delete_ns "$ns" +} + +# Test: default values install +test_install_defaults() { + local chart="$1" + local label="$chart: install (defaults)" + local ns + ns=$(make_ns "$chart" "defaults") + + install_and_verify "$chart" "$label" "$ns" +} + +# Install KEDA CRDs if not already installed. +# Required for ci/ values files that enable KEDA ScaledObject. +ensure_keda_crds() { + if $KEDA_CRDS_INSTALLED; then + return 0 + fi + + local keda_crd_url="https://github.com/kedacore/keda/releases/download/v2.16.1/keda-2.16.1-crds.yaml" + if kubectl apply --server-side -f "$keda_crd_url" >/dev/null 2>&1; then + KEDA_CRDS_INSTALLED=true + log_info "Installed KEDA CRDs (ScaledObject, ScaledJob, TriggerAuthentication)" + return 0 + else + return 1 + fi +} + +# Test: install with each ci/ values file +test_install_ci_values() { + local chart="$1" + local ci_dir="$REPO_ROOT/$chart/ci" + + if [ ! -d "$ci_dir" ]; then + log_skip "$chart: no ci/ directory" + return + fi + + local count=0 + for values_file in "$ci_dir"/*.yaml "$ci_dir"/*.yml; do + [ -f "$values_file" ] || continue + + local fname + fname="$(basename "$values_file")" + + # Apply CI_FILTER if set + if [ -n "$CI_FILTER" ]; then + # shellcheck disable=SC2254 + case "$fname" in + $CI_FILTER) ;; + *) continue ;; + esac + fi + + count=$((count + 1)) + + # Install KEDA CRDs if this values file enables KEDA + if [[ "$fname" == *keda* ]]; then + if ! ensure_keda_crds; then + log_skip "$chart: install ci/$fname - could not install KEDA CRDs" + continue + fi + fi + + local label="$chart: install ci/$fname" + local suffix="${fname%.yaml}" + suffix="${suffix%.yml}" + suffix="${suffix%-values}" + local ns + ns=$(make_ns "$chart" "$suffix") + + install_and_verify "$chart" "$label" "$ns" -f "$values_file" + done + + if [ "$count" -eq 0 ]; then + log_skip "$chart: ci/ directory is empty${CI_FILTER:+ (or no match for '$CI_FILTER')}" + fi +} + +# Test: Deployment vs DaemonSet mode (if chart supports it) +test_install_daemonset() { + local chart="$1" + local values_yaml="$REPO_ROOT/$chart/values.yaml" + + if ! grep -q 'kind: Deployment' "$values_yaml" 2>/dev/null; then + return + fi + if [ ! -f "$REPO_ROOT/$chart/templates/controller-daemonset.yaml" ]; then + return + fi + + local label="$chart: install (DaemonSet mode)" + local ns + ns=$(make_ns "$chart" "daemonset") + + install_and_verify "$chart" "$label" "$ns" --set controller.kind=DaemonSet +} + +# Test: verify HPA creates when enabled (if chart supports it) +test_install_hpa() { + local chart="$1" + local values_yaml="$REPO_ROOT/$chart/values.yaml" + + if ! grep -q 'autoscaling:' "$values_yaml" 2>/dev/null; then + return + fi + + local label="$chart: install (HPA enabled)" + local ns + ns=$(make_ns "$chart" "hpa") + + install_and_verify "$chart" "$label" "$ns" \ + --set controller.autoscaling.enabled=true \ + --set controller.autoscaling.minReplicas=1 \ + --set controller.autoscaling.maxReplicas=3 \ + --set controller.autoscaling.targetCPUUtilizationPercentage=80 +} + +# Test: verify PDB creates when enabled (if chart supports it) +test_install_pdb() { + local chart="$1" + local values_yaml="$REPO_ROOT/$chart/values.yaml" + + if ! grep -q 'podDisruptionBudget:' "$values_yaml" 2>/dev/null; then + return + fi + + local label="$chart: install (PDB enabled)" + local ns + ns=$(make_ns "$chart" "pdb") + + install_and_verify "$chart" "$label" "$ns" \ + --set controller.replicaCount=2 \ + --set controller.podDisruptionBudget.enabled=true \ + --set controller.podDisruptionBudget.minAvailable=1 +} + +# Test: verify metrics port is exposed and metricsAuth flag is passed +test_install_metrics_port() { + local chart="$1" + local values_yaml="$REPO_ROOT/$chart/values.yaml" + + if ! grep -q 'metricsAuth:' "$values_yaml" 2>/dev/null; then + return + fi + + local label="$chart: install (metrics port exposed)" + local ns + ns=$(make_ns "$chart" "metrics") + + create_ns "$ns" + + local extra_args + extra_args=$(chart_test_args "$chart") + # shellcheck disable=SC2086 + if ! helm install "test-${chart}" "$REPO_ROOT/$chart" \ + --namespace "$ns" \ + --wait --timeout "${TIMEOUT}s" \ + $extra_args 2>&1 | sed 's/^/ /'; then + log_fail "$label (helm install failed)" + dump_debug "$ns" + delete_ns "$ns" + return + fi + + if ! wait_for_pods "$ns"; then + log_fail "$label (pods not ready within ${TIMEOUT}s)" + dump_debug "$ns" + helm uninstall "test-${chart}" -n "$ns" --wait 2>/dev/null || true + delete_ns "$ns" + return + fi + + # Verify the controller pod has the metrics container port (31060) + local ports_label="$chart: controller pod has metrics port 31060" + if kubectl get pods -n "$ns" -o jsonpath='{.items[*].spec.containers[*].ports[*].containerPort}' 2>/dev/null | tr ' ' '\n' | grep -q '31060'; then + log_pass "$ports_label" + else + log_fail "$ports_label" + fi + + # Verify --metrics-auth=kube-rbac is in the container args + local args_label="$chart: controller pod has --metrics-auth=kube-rbac arg" + if kubectl get pods -n "$ns" -o jsonpath='{.items[*].spec.containers[*].args[*]}' 2>/dev/null | grep -q 'metrics-auth=kube-rbac'; then + log_pass "$args_label" + else + log_fail "$args_label" + fi + + # Verify the main Service has the stat port (it should NOT have metrics — that's only on the metrics Service) + local svc_label="$chart: main Service has stat port" + if kubectl get svc -n "$ns" "test-${chart}" -o jsonpath='{.spec.ports[*].name}' 2>/dev/null | grep -q 'stat'; then + log_pass "$svc_label" + else + log_fail "$svc_label" + fi + + helm uninstall "test-${chart}" -n "$ns" --wait 2>/dev/null || true + delete_ns "$ns" +} + +# Test: verify ServiceMonitor/PodMonitor install (if chart supports it) +# Installs Prometheus Operator CRDs so the monitoring.coreos.com/v1 API is available, +# then verifies the resources are created. +test_install_monitoring() { + local chart="$1" + local values_yaml="$REPO_ROOT/$chart/values.yaml" + + if ! grep -q 'serviceMonitor:' "$values_yaml" 2>/dev/null; then + return + fi + + # Install Prometheus Operator CRDs so the API is available + local prom_crds_installed=false + local prom_crd_url="https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/main/jsonnet/prometheus-operator/podmonitors-crd.json" + local sm_crd_url="https://raw.githubusercontent.com/prometheus-operator/prometheus-operator/main/jsonnet/prometheus-operator/servicemonitors-crd.json" + + # Try to apply the CRDs; if curl/URLs fail, skip + if kubectl apply -f "$sm_crd_url" >/dev/null 2>&1 && \ + kubectl apply -f "$prom_crd_url" >/dev/null 2>&1; then + prom_crds_installed=true + log_info "Installed Prometheus Operator CRDs (ServiceMonitor, PodMonitor)" + else + log_skip "$chart: install (ServiceMonitor) - could not install Prometheus Operator CRDs" + log_skip "$chart: install (PodMonitor) - could not install Prometheus Operator CRDs" + return + fi + + # ServiceMonitor test + local label="$chart: install (ServiceMonitor enabled)" + local ns + ns=$(make_ns "$chart" "svcmon") + create_ns "$ns" + + local extra_args + extra_args=$(chart_test_args "$chart") + # shellcheck disable=SC2086 + if helm install "test-${chart}" "$REPO_ROOT/$chart" \ + --namespace "$ns" \ + --wait --timeout "${TIMEOUT}s" \ + --set controller.serviceMonitor.enabled=true \ + $extra_args 2>&1 | sed 's/^/ /'; then + + # Verify ServiceMonitor exists + if kubectl get servicemonitor -n "$ns" --no-headers 2>/dev/null | grep -q .; then + log_pass "$label" + else + log_fail "$label (ServiceMonitor resource not found)" + fi + + # Verify metrics Service exists + local metrics_label="$chart: install (metrics Service created)" + if kubectl get svc -n "$ns" --no-headers 2>/dev/null | grep -q metrics; then + log_pass "$metrics_label" + else + log_fail "$metrics_label" + fi + + # Verify metrics Service exposes both stat and metrics ports + local ports_label="$chart: install (metrics Service has stat + metrics ports)" + local svc_ports + svc_ports=$(kubectl get svc -n "$ns" -l "app.kubernetes.io/instance=test-${chart}" -o jsonpath='{.items[*].spec.ports[*].name}' 2>/dev/null || true) + if echo "$svc_ports" | grep -q 'stat' && echo "$svc_ports" | grep -q 'metrics'; then + log_pass "$ports_label" + else + log_fail "$ports_label" + fi + else + log_fail "$label (helm install failed)" + dump_debug "$ns" + fi + + helm uninstall "test-${chart}" -n "$ns" --wait 2>/dev/null || true + delete_ns "$ns" + + # PodMonitor test + label="$chart: install (PodMonitor enabled)" + ns=$(make_ns "$chart" "podmon") + create_ns "$ns" + + # shellcheck disable=SC2086 + if helm install "test-${chart}" "$REPO_ROOT/$chart" \ + --namespace "$ns" \ + --wait --timeout "${TIMEOUT}s" \ + --set controller.podMonitor.enabled=true \ + $extra_args 2>&1 | sed 's/^/ /'; then + + # Verify PodMonitor exists + if kubectl get podmonitor -n "$ns" --no-headers 2>/dev/null | grep -q .; then + log_pass "$label" + else + log_fail "$label (PodMonitor resource not found)" + fi + + # Verify no metrics Service (PodMonitor shouldn't create one) + local no_metrics_label="$chart: install (no metrics Service with PodMonitor)" + if kubectl get svc -n "$ns" --no-headers 2>/dev/null | grep -q metrics; then + log_fail "$no_metrics_label" + else + log_pass "$no_metrics_label" + fi + else + log_fail "$label (helm install failed)" + dump_debug "$ns" + fi + + helm uninstall "test-${chart}" -n "$ns" --wait 2>/dev/null || true + delete_ns "$ns" +} + +# Test: verify HugConf is deleted after helm uninstall +test_hugconf_cleanup() { + local chart="$1" + local values_yaml="$REPO_ROOT/$chart/values.yaml" + + if ! grep -q 'hugconf:' "$values_yaml" 2>/dev/null; then + return + fi + + local label="$chart: HugConf deleted on uninstall" + local ns + ns=$(make_ns "$chart" "hugconf-cleanup") + + create_ns "$ns" + + local extra_args + extra_args=$(chart_test_args "$chart") + # shellcheck disable=SC2086 + if ! helm install "test-${chart}" "$REPO_ROOT/$chart" \ + --namespace "$ns" \ + --wait --timeout "${TIMEOUT}s" \ + $extra_args 2>&1 | sed 's/^/ /'; then + log_fail "$label (helm install failed)" + dump_debug "$ns" + delete_ns "$ns" + return + fi + + if ! wait_for_pods "$ns"; then + log_fail "$label (pods not ready within ${TIMEOUT}s)" + dump_debug "$ns" + helm uninstall "test-${chart}" -n "$ns" --wait 2>/dev/null || true + delete_ns "$ns" + return + fi + + # Verify HugConf exists before uninstall + if ! kubectl get hugconfs.gate.v3.haproxy.org -n "$ns" --no-headers 2>/dev/null | grep -q .; then + log_fail "$label (HugConf not found after install)" + helm uninstall "test-${chart}" -n "$ns" --wait 2>/dev/null || true + delete_ns "$ns" + return + fi + + # Uninstall and verify HugConf is cleaned up + if ! helm uninstall "test-${chart}" -n "$ns" --wait --timeout "${TIMEOUT}s" 2>&1 | sed 's/^/ /'; then + log_fail "$label (helm uninstall failed)" + delete_ns "$ns" + return + fi + + # Check that HugConf is gone + if kubectl get hugconfs.gate.v3.haproxy.org -n "$ns" --no-headers 2>/dev/null | grep -q .; then + log_fail "$label (HugConf still exists after uninstall)" + else + log_pass "$label" + fi + + delete_ns "$ns" +} + +# --- cleanup trap --- + +cleanup() { + if [ "$KEEP_NS" = "1" ]; then + if [ ${#NAMESPACES[@]} -gt 0 ]; then + echo -e "\n${YELLOW}KEEP_NS=1, these namespaces were left:${NC}" + printf ' %s\n' "${NAMESPACES[@]}" + fi + else + # In case of early exit, clean up any remaining namespaces + for ns in "${NAMESPACES[@]}"; do + kubectl delete namespace "$ns" --wait=false --ignore-not-found >/dev/null 2>&1 || true + done + fi + + delete_kind_cluster +} +trap cleanup EXIT + +# --- main --- + +main() { + local filter="${1:-}" + + echo -e "${BOLD}Helm Chart Integration Test${NC}" + echo "Repo: $REPO_ROOT" + echo "Helm: $(helm version --short 2>/dev/null)" + echo "Timeout: ${TIMEOUT}s per install" + [ -n "$CI_FILTER" ] && echo "CI filter: $CI_FILTER" + [ -n "$TEST_FILTER" ] && echo "Test: $TEST_FILTER" + + # Create or reuse Kind cluster + create_kind_cluster + + check_prerequisites + + echo "Cluster: $(kubectl config current-context 2>/dev/null || echo 'unknown')" + echo "K8s server: $(kubectl version 2>/dev/null | grep 'Server Version' | head -1 || true)" + + local charts + read -ra charts <<< "$(find_charts "$filter")" + echo "Charts: ${charts[*]}" + + for chart in "${charts[@]}"; do + log_section "$chart" + + if [ -z "$TEST_FILTER" ] || [ "$TEST_FILTER" = "defaults" ]; then test_install_defaults "$chart"; fi + if [ -z "$TEST_FILTER" ] || [ "$TEST_FILTER" = "daemonset" ]; then test_install_daemonset "$chart"; fi + if [ -z "$TEST_FILTER" ] || [ "$TEST_FILTER" = "hpa" ]; then test_install_hpa "$chart"; fi + if [ -z "$TEST_FILTER" ] || [ "$TEST_FILTER" = "pdb" ]; then test_install_pdb "$chart"; fi + if [ -z "$TEST_FILTER" ] || [ "$TEST_FILTER" = "metrics-port" ]; then test_install_metrics_port "$chart"; fi + if [ -z "$TEST_FILTER" ] || [ "$TEST_FILTER" = "monitoring" ]; then test_install_monitoring "$chart"; fi + if [ -z "$TEST_FILTER" ] || [ "$TEST_FILTER" = "hugconf-cleanup" ]; then test_hugconf_cleanup "$chart"; fi + if [ -z "$TEST_FILTER" ] || [ "$TEST_FILTER" = "ci" ]; then test_install_ci_values "$chart"; fi + done + + # Summary + log_section "Summary" + echo -e " ${GREEN}Passed:${NC} $PASSED" + echo -e " ${RED}Failed:${NC} $FAILED" + echo -e " ${YELLOW}Skipped:${NC} $SKIPPED" + + if [ ${#FAILURES[@]} -gt 0 ]; then + echo -e "\n${RED}Failures:${NC}" + for f in "${FAILURES[@]}"; do + echo " - $f" + done + exit 1 + fi + + echo -e "\n${GREEN}All integration tests passed.${NC}" +} + +main "$@" diff --git a/test/local-test.sh b/test/local-test.sh new file mode 100755 index 0000000..26ce0ab --- /dev/null +++ b/test/local-test.sh @@ -0,0 +1,401 @@ +#!/bin/bash +# +# Local Helm chart testing script. +# Runs lint and template rendering for each ci/ values file. +# +# Usage: +# ./test/local-test.sh # test all charts +# ./test/local-test.sh haproxy-unified-gateway # test only HUG +# ./test/local-test.sh kubernetes-ingress # test only ingress +# + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +PASSED=0 +FAILED=0 +SKIPPED=0 +FAILURES=() + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BOLD='\033[1m' +NC='\033[0m' + +log_pass() { echo -e " ${GREEN}PASS${NC} $1"; PASSED=$((PASSED + 1)); } +log_fail() { echo -e " ${RED}FAIL${NC} $1"; FAILED=$((FAILED + 1)); FAILURES+=("$1"); } +log_skip() { echo -e " ${YELLOW}SKIP${NC} $1"; SKIPPED=$((SKIPPED + 1)); } +log_section() { echo -e "\n${BOLD}=== $1 ===${NC}"; } + +# Determine which charts to test +find_charts() { + local filter="${1:-}" + local charts=() + + for dir in "$REPO_ROOT"/*/; do + [ -f "$dir/Chart.yaml" ] || continue + local name + name="$(basename "$dir")" + if [ -n "$filter" ] && [ "$name" != "$filter" ]; then + continue + fi + charts+=("$name") + done + + if [ ${#charts[@]} -eq 0 ]; then + echo "No charts found${filter:+ matching '$filter'}." >&2 + exit 1 + fi + + echo "${charts[@]}" +} + +# 1. helm lint with default values +test_lint() { + local chart="$1" + local label="$chart: helm lint (defaults)" + + if helm lint "$REPO_ROOT/$chart" --quiet 2>/dev/null; then + log_pass "$label" + else + log_fail "$label" + fi +} + +# 2. helm template with default values +test_template_defaults() { + local chart="$1" + local label="$chart: helm template (defaults)" + + if helm template test-release "$REPO_ROOT/$chart" >/dev/null 2>&1; then + log_pass "$label" + else + log_fail "$label" + fi +} + +# 3. helm lint + template for each ci/ values file +test_ci_values() { + local chart="$1" + local ci_dir="$REPO_ROOT/$chart/ci" + + if [ ! -d "$ci_dir" ]; then + log_skip "$chart: no ci/ directory" + return + fi + + local count=0 + for values_file in "$ci_dir"/*.yaml "$ci_dir"/*.yml; do + [ -f "$values_file" ] || continue + count=$((count + 1)) + + local fname + fname="$(basename "$values_file")" + + # lint + local lint_label="$chart: lint ci/$fname" + if helm lint "$REPO_ROOT/$chart" -f "$values_file" --quiet 2>/dev/null; then + log_pass "$lint_label" + else + log_fail "$lint_label" + fi + + # template + local tmpl_label="$chart: template ci/$fname" + local output + if output=$(helm template test-release "$REPO_ROOT/$chart" -f "$values_file" 2>&1); then + log_pass "$tmpl_label" + else + log_fail "$tmpl_label" + echo "$output" | head -5 | sed 's/^/ /' + fi + done + + if [ "$count" -eq 0 ]; then + log_skip "$chart: ci/ directory is empty" + fi +} + +# 4. Check Chart.yaml required fields +test_chart_metadata() { + local chart="$1" + local chart_yaml="$REPO_ROOT/$chart/Chart.yaml" + local label="$chart: Chart.yaml metadata" + + local ok=true + for field in name version appVersion description; do + if ! grep -q "^${field}:" "$chart_yaml"; then + echo -e " ${RED}FAIL${NC} $chart: Chart.yaml missing '$field'" + FAILED=$((FAILED + 1)) + FAILURES+=("$chart: Chart.yaml missing '$field'") + ok=false + fi + done + + if $ok; then + log_pass "$label" + fi +} + +# 5. Verify template renders different output for Deployment vs DaemonSet (if applicable) +test_kind_switch() { + local chart="$1" + local values_yaml="$REPO_ROOT/$chart/values.yaml" + + # Only test if chart has both Deployment and DaemonSet templates + if ! grep -q 'kind: Deployment' "$values_yaml" 2>/dev/null; then + return + fi + if [ ! -f "$REPO_ROOT/$chart/templates/controller-daemonset.yaml" ]; then + return + fi + + local label="$chart: Deployment vs DaemonSet renders differently" + + local deploy_out daemon_out + deploy_out=$(helm template test-release "$REPO_ROOT/$chart" --set controller.kind=Deployment 2>&1) + daemon_out=$(helm template test-release "$REPO_ROOT/$chart" --set controller.kind=DaemonSet 2>&1) + + if [ $? -ne 0 ]; then + log_fail "$label (DaemonSet template failed)" + return + fi + + if echo "$deploy_out" | grep -q 'kind: Deployment' && echo "$daemon_out" | grep -q 'kind: DaemonSet'; then + log_pass "$label" + else + log_fail "$label" + fi +} + +# 6. Verify HugConf cleanup hook renders correctly +test_hugconf_cleanup() { + local chart="$1" + local values_yaml="$REPO_ROOT/$chart/values.yaml" + + if ! grep -q 'hugconf:' "$values_yaml" 2>/dev/null; then + return + fi + + # Cleanup resources should render when hugconf.create=true (default) + local label="$chart: HugConf cleanup hook renders when hugconf.create=true" + local output + output=$(helm template test-release "$REPO_ROOT/$chart" \ + --set hugconf.create=true 2>&1) + if echo "$output" | grep -q 'hugconf-cleanup' && echo "$output" | grep -q '"helm.sh/hook": pre-delete'; then + log_pass "$label" + else + log_fail "$label" + fi + + # Cleanup resources should NOT render when hugconf.create=false + label="$chart: HugConf cleanup hook skipped when hugconf.create=false" + output=$(helm template test-release "$REPO_ROOT/$chart" \ + --set hugconf.create=false 2>&1) + if echo "$output" | grep -q 'hugconf-cleanup'; then + log_fail "$label" + else + log_pass "$label" + fi + + # Verify the cleanup Job uses the correct HugConf name + label="$chart: HugConf cleanup Job targets correct resource name" + output=$(helm template test-release "$REPO_ROOT/$chart" \ + --set hugconf.create=true \ + --set hugconf.name=my-custom-hugconf 2>&1) + if echo "$output" | grep -q 'hugconfs/my-custom-hugconf'; then + log_pass "$label" + else + log_fail "$label" + fi +} + +# 7. Verify controller metrics port and metricsAuth flag render correctly +test_metrics_port() { + local chart="$1" + local values_yaml="$REPO_ROOT/$chart/values.yaml" + + if ! grep -q 'metricsAuth:' "$values_yaml" 2>/dev/null; then + return + fi + + # Verify metrics container port renders + local label="$chart: metrics container port (31060) renders" + local output + output=$(helm template test-release "$REPO_ROOT/$chart" 2>&1) + if echo "$output" | grep -q 'containerPort: 31060'; then + log_pass "$label" + else + log_fail "$label" + fi + + # Verify --metrics-auth=kube-rbac is the default arg + label="$chart: --metrics-auth=kube-rbac in default args" + if echo "$output" | grep -q '\-\-metrics-auth=kube-rbac'; then + log_pass "$label" + else + log_fail "$label" + fi + + # Verify metricsAuth=none omits kube-rbac and sets none + label="$chart: --metrics-auth=none when metricsAuth=none" + output=$(helm template test-release "$REPO_ROOT/$chart" \ + --set controller.metricsAuth=none 2>&1) + if echo "$output" | grep -q '\-\-metrics-auth=none' && \ + ! echo "$output" | grep -q '\-\-metrics-auth=kube-rbac'; then + log_pass "$label" + else + log_fail "$label" + fi + + # Verify metrics Service includes both stat and metrics ports when ServiceMonitor is enabled + label="$chart: metrics Service has stat and metrics ports" + output=$(helm template test-release "$REPO_ROOT/$chart" \ + --set controller.serviceMonitor.enabled=true \ + --api-versions monitoring.coreos.com/v1 2>&1) + if echo "$output" | grep -A1 'name: stat' | grep -q 'port: 31024' && \ + echo "$output" | grep -A1 'name: metrics' | grep -q 'port: 31060'; then + log_pass "$label" + else + log_fail "$label" + fi + + # Verify DaemonSet also gets the metrics port and --metrics-auth flag + if [ -f "$REPO_ROOT/$chart/templates/controller-daemonset.yaml" ]; then + label="$chart: DaemonSet renders metrics port and --metrics-auth" + output=$(helm template test-release "$REPO_ROOT/$chart" \ + --set controller.kind=DaemonSet 2>&1) + if echo "$output" | grep -q 'containerPort: 31060' && \ + echo "$output" | grep -q '\-\-metrics-auth=kube-rbac'; then + log_pass "$label" + else + log_fail "$label" + fi + fi +} + +# 8. Verify ServiceMonitor/PodMonitor render correctly when API is available +test_monitoring() { + local chart="$1" + local values_yaml="$REPO_ROOT/$chart/values.yaml" + local tmpl_dir="$REPO_ROOT/$chart/templates" + + if ! grep -q 'serviceMonitor:' "$values_yaml" 2>/dev/null; then + return + fi + + # Determine the values path for serviceMonitor.enabled + # Some charts use controller.serviceMonitor.enabled, others use serviceMonitor.enabled + local sm_set="controller.serviceMonitor.enabled=true" + if grep -q '^serviceMonitor:' "$values_yaml" 2>/dev/null; then + sm_set="serviceMonitor.enabled=true" + fi + + # ServiceMonitor: should render when API version is available + local label="$chart: ServiceMonitor renders with monitoring.coreos.com/v1 API" + local output + output=$(helm template test-release "$REPO_ROOT/$chart" \ + --set "$sm_set" \ + --api-versions monitoring.coreos.com/v1 2>&1) + if echo "$output" | grep -q 'kind: ServiceMonitor'; then + log_pass "$label" + else + log_fail "$label" + fi + + # ServiceMonitor: should NOT render without the API + label="$chart: ServiceMonitor skipped without monitoring.coreos.com/v1 API" + output=$(helm template test-release "$REPO_ROOT/$chart" \ + --set "$sm_set" 2>&1) + if echo "$output" | grep -q 'kind: ServiceMonitor'; then + log_fail "$label" + else + log_pass "$label" + fi + + # Metrics service: only test if chart has a metrics service template + if ls "$tmpl_dir"/*service-metrics* &>/dev/null; then + label="$chart: metrics Service created with ServiceMonitor" + output=$(helm template test-release "$REPO_ROOT/$chart" \ + --set "$sm_set" \ + --api-versions monitoring.coreos.com/v1 2>&1) + if echo "$output" | grep -q 'name: test-release.*-metrics'; then + log_pass "$label" + else + log_fail "$label" + fi + fi + + # PodMonitor: only test if chart has a podmonitor template + if ls "$tmpl_dir"/*podmonitor* &>/dev/null; then + local pm_set="controller.podMonitor.enabled=true" + if grep -q '^podMonitor:' "$values_yaml" 2>/dev/null; then + pm_set="podMonitor.enabled=true" + fi + + label="$chart: PodMonitor renders with monitoring.coreos.com/v1 API" + output=$(helm template test-release "$REPO_ROOT/$chart" \ + --set "$pm_set" \ + --api-versions monitoring.coreos.com/v1 2>&1) + if echo "$output" | grep -q 'kind: PodMonitor'; then + log_pass "$label" + else + log_fail "$label" + fi + + # PodMonitor: should NOT create metrics service + label="$chart: no metrics Service with PodMonitor only" + output=$(helm template test-release "$REPO_ROOT/$chart" \ + --set "$pm_set" \ + --api-versions monitoring.coreos.com/v1 2>&1) + if echo "$output" | grep -q 'name: test-release.*-metrics'; then + log_fail "$label" + else + log_pass "$label" + fi + fi +} + +main() { + local filter="${1:-}" + + echo -e "${BOLD}Helm Chart Local Test${NC}" + echo "Repo: $REPO_ROOT" + echo "Helm: $(helm version --short 2>/dev/null)" + + local charts + read -ra charts <<< "$(find_charts "$filter")" + echo "Charts: ${charts[*]}" + + for chart in "${charts[@]}"; do + log_section "$chart" + + test_chart_metadata "$chart" + test_lint "$chart" + test_template_defaults "$chart" + test_kind_switch "$chart" + test_hugconf_cleanup "$chart" + test_metrics_port "$chart" + test_monitoring "$chart" + test_ci_values "$chart" + done + + # Summary + log_section "Summary" + echo -e " ${GREEN}Passed:${NC} $PASSED" + echo -e " ${RED}Failed:${NC} $FAILED" + echo -e " ${YELLOW}Skipped:${NC} $SKIPPED" + + if [ ${#FAILURES[@]} -gt 0 ]; then + echo -e "\n${RED}Failures:${NC}" + for f in "${FAILURES[@]}"; do + echo " - $f" + done + exit 1 + fi + + echo -e "\n${GREEN}All tests passed.${NC}" +} + +main "$@"