bindcar 0.8.2

HTTP REST API for managing BIND9 zones via rndc
# Copyright (c) 2025 Erick Bourgeois, firestoned
# SPDX-License-Identifier: MIT
#
# Reusable end-to-end test workflow.
#
# One job per suite (mirrors bindy/banlieue). The bindcar image and its
# musl-static binary are built ONCE in the `build` job and handed to the
# suite jobs as artifacts, so each suite pays for its own assertions but not
# for a repeated Rust build. Each suite maps 1:1 to a Makefile target
# (workflows delegate logic to the Makefile per repo convention) and owns its
# own runner, which is why they run concurrently:
#
#   e2e-tls    TLS/mTLS transport against the real binary: handshake, client
#              CA verification, hot-reload, fail-closed misconfiguration
#              (integration-test/tls-transport.sh; no docker, kind or BIND9)
#   e2e-drone  bindcar binary in drone mode against a dockerized external
#              BIND9 (integration-test/drone-external-bind9.sh)
#   e2e-kind   BIND9 + bindcar sidecar Pod on a kind cluster: health/auth,
#              zone + record lifecycle, the full DNSSEC lifecycle (ADR-0001:
#              enable → signed → DS → refused removal → insecure → removal),
#              and server-side validation of the shipped deploy/ manifests
#              (integration-test/kind-e2e.sh)
#
# Before this split the whole gate was a single `make ci-e2e` job: serial
# work that reported one red X with no indication of which suite broke.
# `make ci-e2e` still runs everything serially for local use.
#
# Callable via `workflow_call` (e.g. from dependabot-auto-merge.yaml) or run
# manually via `workflow_dispatch`.

name: E2E Tests

on:
  pull_request:
    paths:
      - '.github/workflows/e2e.yaml'
      - 'Makefile'
      - 'integration-test/**'
      - 'docker/Dockerfile.chef'
      # Only observable against a real handshake / a real BIND9 / a real API
      # server, so changes to these must run the gate.
      - 'src/tls.rs'
      - 'src/dnssec.rs'
      - 'src/nsupdate.rs'
      - 'deploy/rbac.yaml'
      - 'deploy/networkpolicy.yaml'
  workflow_call: {}
  workflow_dispatch: {}

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: 1
  # Every suite job runs against the image/binary the build job produced.
  E2E_IMAGE: bindcar:ci-e2e

jobs:
  build:
    name: Build e2e image + binary
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

      - name: Build and package the image and binary
        run: make e2e-image

      - name: Upload the image tarball
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
        with:
          name: e2e-image
          path: dist/bindcar-e2e-image.tar
          retention-days: 1
          compression-level: 0

      - name: Upload the binary
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
        with:
          name: e2e-binary
          path: dist/bindcar
          retention-days: 1

  suite:
    name: ${{ matrix.name }}
    needs: build
    runs-on: ubuntu-latest
    strategy:
      # One suite breaking should not hide the state of the others — that is
      # the whole point of splitting them up.
      fail-fast: false
      matrix:
        include:
          - target: e2e-tls
            name: TLS transport
            artifact: e2e-binary
            needs_kind: false
          - target: e2e-drone
            name: Drone integration (docker BIND9)
            artifact: e2e-binary
            needs_kind: false
          - target: e2e-kind
            name: Sidecar + DNSSEC lifecycle (kind)
            artifact: e2e-image
            needs_kind: true
    steps:
      - name: Checkout code
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

      - name: Install Kind
        if: matrix.needs_kind
        uses: helm/kind-action@06c1ae10762d3b9c1644e7fe69596ae519e015a2 # v1.15.0
        with:
          install_only: true
          version: v0.24.0

      - name: Install kubectl
        if: matrix.needs_kind
        uses: azure/setup-kubectl@829323503d1be3d00ca8346e5391ca0b07a9ab0d # v5
        with:
          version: 'v1.31.0'

      - name: Download the build artifact
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: ${{ matrix.artifact }}
          path: dist

      - name: Run ${{ matrix.target }}
        run: make ${{ matrix.target }}

      - name: Dump docker diagnostics on failure
        if: failure() && !matrix.needs_kind
        run: |
          echo "::group::docker containers"
          docker ps -a || true
          echo "::endgroup::"
          for c in $(docker ps -aq); do
            echo "::group::logs $(docker inspect --format '{{.Name}}' "$c" 2>/dev/null || echo "$c")"
            docker logs --tail=200 "$c" 2>&1 || true
            echo "::endgroup::"
          done

      - name: Dump Kubernetes diagnostics on failure
        # kind-e2e.sh leaves the cluster up on failure — capture its state
        # before the runner is torn down.
        if: failure() && matrix.needs_kind
        run: |
          CTX=kind-bindcar-e2e
          NS=bindcar-e2e
          echo "::group::cluster summary ($CTX)"
          kubectl --context "$CTX" get nodes -o wide 2>/dev/null || true
          kubectl --context "$CTX" get pods -A -o wide 2>/dev/null || true
          echo "::endgroup::"

          echo "::group::events ($CTX)"
          kubectl --context "$CTX" get events -A --sort-by=.lastTimestamp 2>/dev/null || true
          echo "::endgroup::"

          echo "::group::bindcar e2e pod ($CTX / $NS)"
          kubectl --context "$CTX" -n "$NS" get pod,svc,secret,configmap -o wide 2>/dev/null || true
          kubectl --context "$CTX" -n "$NS" describe pod bindcar-e2e 2>/dev/null || true
          kubectl --context "$CTX" -n "$NS" logs bindcar-e2e -c bindcar --tail=300 2>/dev/null || true
          kubectl --context "$CTX" -n "$NS" logs bindcar-e2e -c bind9 --tail=300 2>/dev/null || true
          echo "::endgroup::"

      - name: Delete the kind cluster
        if: always() && matrix.needs_kind
        run: kind delete cluster --name bindcar-e2e 2>/dev/null || true

  # Single required check for branch protection and for callers of this
  # workflow: green only when every suite is green.
  e2e:
    name: E2E gate
    needs: suite
    if: always()
    runs-on: ubuntu-latest
    steps:
      - name: Check suite results
        env:
          RESULT: ${{ needs.suite.result }}
        run: |
          echo "Suite matrix result: $RESULT"
          [ "$RESULT" = "success" ] || exit 1