1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# 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