-
Notifications
You must be signed in to change notification settings - Fork 2
327 lines (294 loc) · 14.4 KB
/
Copy pathci.yml
File metadata and controls
327 lines (294 loc) · 14.4 KB
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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
name: CI
on:
push:
branches:
- main
pull_request:
# Cancel superseded runs on the same ref to save runner time.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
# Fast Go gates: format, vet, test (with coverage), build, lint.
# The Go toolchain is read from go.mod so CI can never drift from the
# module's `go` directive (CLAUDE.md no-drift rule, hardening item C3).
gates:
name: build / vet / test / lint
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version-file: go.mod
check-latest: false
- name: verify base-image pins (no digest drift)
run: make verify-pins
- name: gofmt -s (check only)
run: make fmt-check
- name: go vet
run: make vet
- name: go test (with coverage)
run: make test
- name: go build
run: make build
- name: golangci-lint
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9
with:
# Pin to the version validated locally against the v2 config.
version: v2.12.2
# Derive the image-build matrix from the single source of truth for the
# supported Kubernetes window: kubeadm.SupportedMinors in version.go (ADR-3).
# Rolling the window there automatically rolls this CI matrix.
discover:
name: resolve supported version window
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
minors: ${{ steps.minors.outputs.minors }}
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Extract SupportedMinors -> JSON matrix
id: minors
run: |
set -euo pipefail
# SupportedMinors is a static string-literal slice; pull each
# "major.minor" token off its declaration line.
minors="$(grep 'SupportedMinors *=' internal/kubeadm/version.go \
| grep -oE '[0-9]+\.[0-9]+' \
| jq -R . | jq -cs .)"
if [ "$minors" = "[]" ]; then
echo "could not parse SupportedMinors from version.go" >&2
exit 1
fi
echo "Supported window: $minors"
echo "minors=$minors" >> "$GITHUB_OUTPUT"
# Product deliverable: the multi-stage Kairos image bundling the provider
# plus kubeadm/kubelet/kubectl/containerd/runc/CNI (all checksum-verified
# inside the Dockerfile). Built once per supported minor; each minor's patch
# is resolved to the latest upstream release at build time so pins never go
# stale as the window rolls.
image:
name: image build (k8s ${{ matrix.minor }})
needs: discover
runs-on: ubuntu-latest
# The Hadron (musl) image builds containerd AND kubelet static from source,
# which adds several minutes per leg over the old binary-download path.
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
minor: ${{ fromJSON(needs.discover.outputs.minors) }}
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Resolve Kubernetes patch + commit + crictl versions
id: ver
run: |
set -euo pipefail
k8s="$(curl -fsSL "https://dl.k8s.io/release/stable-${{ matrix.minor }}.txt")"
case "$k8s" in
v${{ matrix.minor }}.*) : ;;
*) echo "unexpected patch ${k8s} for minor ${{ matrix.minor }}" >&2; exit 1 ;;
esac
# The Hadron (musl) image builds kubelet static from source, pinning the
# clone to an immutable commit SHA. Resolve the SHA the resolved tag
# points at and pass it to `make image`, or the from-source build's
# commit assertion fails. Prefer the peeled tag (^{} -> the commit the
# annotated tag wraps, i.e. clone HEAD); fall back to the bare ref.
kcommit="$(git ls-remote https://github.com/kubernetes/kubernetes "refs/tags/${k8s}^{}" | cut -f1)"
if [ -z "$kcommit" ]; then
kcommit="$(git ls-remote https://github.com/kubernetes/kubernetes "refs/tags/${k8s}" | cut -f1)"
fi
case "$kcommit" in
?*) : ;;
*) echo "could not resolve commit SHA for ${k8s}" >&2; exit 1 ;;
esac
# cri-tools publishes one release per minor: v<minor>.0.
crictl="v${{ matrix.minor }}.0"
echo "k8s=${k8s} kcommit=${kcommit} crictl=${crictl}"
echo "k8s=${k8s}" >> "$GITHUB_OUTPUT"
echo "kcommit=${kcommit}" >> "$GITHUB_OUTPUT"
echo "crictl=${crictl}" >> "$GITHUB_OUTPUT"
- name: Build Kairos image
run: |
make image \
VERSION=ci-${{ matrix.minor }} \
KUBERNETES_VERSION=${{ steps.ver.outputs.k8s }} \
KUBERNETES_COMMIT=${{ steps.ver.outputs.kcommit }} \
CRICTL_VERSION=${{ steps.ver.outputs.crictl }}
- name: Verify bundled binaries match the target minor
run: |
set -euo pipefail
img="kairos-kubeadm:ci-${{ matrix.minor }}"
kubeadm_v="$(docker run --rm --entrypoint /usr/bin/kubeadm "$img" version -o short)"
crictl_v="$(docker run --rm --entrypoint /usr/bin/crictl "$img" --version)"
echo "bundled kubeadm: ${kubeadm_v}"
echo "bundled crictl: ${crictl_v}"
case "$kubeadm_v" in
v${{ matrix.minor }}.*) ;;
*) echo "kubeadm minor mismatch: want v${{ matrix.minor }}.x, got ${kubeadm_v}" >&2; exit 1 ;;
esac
case "$crictl_v" in
*v${{ matrix.minor }}.*) ;;
*) echo "crictl minor mismatch: want v${{ matrix.minor }}.x, got ${crictl_v}" >&2; exit 1 ;;
esac
echo "OK: image bundles the k8s ${{ matrix.minor }} toolchain"
- name: Verify containerd sandbox (pause) image is version-matched (C4)
run: |
set -euo pipefail
img="kairos-kubeadm:ci-${{ matrix.minor }}"
# The build pins containerd's sandbox_image to the pause kubeadm expects;
# assert they match so a stale/hardcoded pause tag can't regress (C4).
want="$(docker run --rm --entrypoint /usr/bin/kubeadm "$img" \
config images list --image-repository registry.k8s.io | grep -E '/pause:[0-9]')"
got="$(docker run --rm --entrypoint sh "$img" -c \
"grep -oE 'registry.k8s.io/pause:[0-9.]+' /etc/containerd/config.toml")"
echo "kubeadm pause: ${want}; containerd sandbox_image: ${got}"
[ "$got" = "$want" ] || { echo "pause mismatch (C4): containerd ${got} != kubeadm ${want}" >&2; exit 1; }
echo "OK: sandbox_image is version-matched to kubeadm"
- name: Verify kubeadm exposes the upgrade-path surface (ADR-12)
run: |
set -euo pipefail
img="kairos-kubeadm:ci-${{ matrix.minor }}"
ka() { docker run --rm --entrypoint /usr/bin/kubeadm "$img" "$@"; }
need() { # <help-output> <flag> <label>
printf '%s\n' "$1" | grep -q -- "$2" \
|| { echo "MISSING $2 in '$3' (bundled kubeadm ${{ matrix.minor }})" >&2; exit 1; }
echo " ok: $3 supports $2"
}
# The provider drives only these kubeadm subcommands/flags for upgrades.
# Asserting them against the SHIPPED binary per minor catches an upstream
# removal/rename before it breaks an upgrade at runtime (ADR-12 U-N).
# `kubeadm upgrade apply <ver> --yes --certificate-renewal=true`
apply_help="$(ka upgrade apply --help)"
need "$apply_help" --yes "upgrade apply"
need "$apply_help" --certificate-renewal "upgrade apply"
# `kubeadm upgrade node` (follower / worker convergence) must exist.
ka upgrade node --help >/dev/null
echo " ok: upgrade node subcommand exists"
# `kubeadm init phase kubelet-start --config` (ADR-12-R1 API-free repair).
ks_help="$(ka init phase kubelet-start --help)"
need "$ks_help" --config "init phase kubelet-start"
# `kubeadm init phase upload-certs --config` (cert-key flows via config,
# never on argv -- the mint keystone, ADR-11 #3 / ADR-12 B3).
uc_help="$(ka init phase upload-certs --help)"
need "$uc_help" --config "init phase upload-certs"
echo "OK: kubeadm ${{ matrix.minor }} exposes the upgrade-path subcommands/flags"
# ADR-13 Tier-1 e2e suite: real kubeadm in a privileged systemd node container.
# Runs all scenarios in test/e2e/ (init-converge, reset, init-clobber-refusal,
# upgrade-skew-refusal, worker-join) against the real provider binary.
#
# SECURITY NOTE (privileged containers, ADR-13 E3 gate): the e2e node container
# must run --privileged because kubeadm/kubelet/containerd/etcd require real
# cgroup v2 control, mount namespaces, netns creation, and /sys/fs/cgroup write
# access -- unavailable without privilege inside a container. This is the same
# constraint kind (Kubernetes-in-Docker) has. The only image that runs privileged
# is OUR OWN built kairos-kubeadm-e2e-node image, derived from the kairos-kubeadm
# base we build in this same job from the checkout source. No third-party image
# is ever pulled. Ephemeral per-run kubeadm bootstrap tokens (bounded TTL, never
# persisted) are the only secrets; no long-lived credentials are embedded. Teardown
# is guaranteed: t.Cleanup registers docker rm -fv per container, and the failure
# step below sweeps leaked containers by label.
#
# Image strategy: option (a) -- rebuild kairos-kubeadm + e2e-node in this job
# using the same stable-<minor>.txt patch + commit resolution as the image job.
# This is self-contained (no artifact upload/download between jobs),
# fault-isolated per-minor-leg, and avoids docker/actions-artifact size limits.
# The from-source containerd+kubelet build dominates the build overhead; the
# per-leg timeout below carries headroom for it.
#
# GitHub-hosted ubuntu-latest supports --privileged containers, /boot/config-*,
# and /lib/modules. The harness mounts those read-only so kubeadm's
# SystemVerification preflight can read the kernel config without relaxing any
# preflight check.
e2e:
name: e2e (k8s ${{ matrix.minor }})
needs: discover
# SECURITY (ADR-13 E3, security-architect R1): this job runs a --privileged
# node container (real kubeadm/kubelet/containerd) on attacker-influenceable
# PR code. The load-bearing controls are: (1) GitHub-HOSTED ephemeral runner
# only -- NEVER a self-hosted runner; (2) NO secrets and an explicit
# read-only token (below) -- NEVER add secrets or packages:write; (3) the
# `pull_request` trigger (NOT pull_request_target), so fork PRs get no secrets.
# These four conditions, not the container flags, are what contain a hostile PR.
permissions:
contents: read
runs-on: ubuntu-latest
# Adds the from-source containerd+kubelet build of the Hadron base image on
# top of the e2e suite itself; raised from 50 to keep headroom per leg.
timeout-minutes: 65
strategy:
fail-fast: false
matrix:
minor: ${{ fromJSON(needs.discover.outputs.minors) }}
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version-file: go.mod
check-latest: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Resolve Kubernetes patch + commit + crictl versions
id: ver
run: |
set -euo pipefail
k8s="$(curl -fsSL "https://dl.k8s.io/release/stable-${{ matrix.minor }}.txt")"
case "$k8s" in
v${{ matrix.minor }}.*) : ;;
*) echo "unexpected patch ${k8s} for minor ${{ matrix.minor }}" >&2; exit 1 ;;
esac
# The Hadron base builds kubelet static from source and asserts the
# cloned tag resolves to this commit SHA; resolve and forward it (see
# the image job for the full rationale).
kcommit="$(git ls-remote https://github.com/kubernetes/kubernetes "refs/tags/${k8s}^{}" | cut -f1)"
if [ -z "$kcommit" ]; then
kcommit="$(git ls-remote https://github.com/kubernetes/kubernetes "refs/tags/${k8s}" | cut -f1)"
fi
case "$kcommit" in
?*) : ;;
*) echo "could not resolve commit SHA for ${k8s}" >&2; exit 1 ;;
esac
crictl="v${{ matrix.minor }}.0"
echo "k8s=${k8s} kcommit=${kcommit} crictl=${crictl}"
echo "k8s=${k8s}" >> "$GITHUB_OUTPUT"
echo "kcommit=${kcommit}" >> "$GITHUB_OUTPUT"
echo "crictl=${crictl}" >> "$GITHUB_OUTPUT"
- name: Build kairos-kubeadm base image
run: |
make image \
VERSION=ci-${{ matrix.minor }} \
KUBERNETES_VERSION=${{ steps.ver.outputs.k8s }} \
KUBERNETES_COMMIT=${{ steps.ver.outputs.kcommit }} \
CRICTL_VERSION=${{ steps.ver.outputs.crictl }} \
IMAGE=kairos-kubeadm:${{ steps.ver.outputs.k8s }}
- name: Build e2e node image
run: |
make e2e-node-image \
KUBERNETES_VERSION=${{ steps.ver.outputs.k8s }} \
BASE_IMAGE=kairos-kubeadm:${{ steps.ver.outputs.k8s }} \
E2E_NODE_IMAGE=kairos-kubeadm-e2e-node:${{ steps.ver.outputs.k8s }}
- name: Run e2e suite
run: |
E2E_NODE_IMAGE=kairos-kubeadm-e2e-node:${{ steps.ver.outputs.k8s }} \
E2E_KUBERNETES_VERSION=${{ steps.ver.outputs.k8s }} \
go test -tags e2e -count=1 -timeout 45m -v ./test/e2e/...
- name: Prune e2e container volumes and images on failure
if: failure()
run: |
docker ps -aq --filter 'label=provider-kubernetes-e2e=1' \
| xargs -r docker rm -fv || true
docker image rm -f \
"kairos-kubeadm-e2e-node:${{ steps.ver.outputs.k8s }}" \
"kairos-kubeadm:${{ steps.ver.outputs.k8s }}" || true
docker volume prune -f || true