Repository navigation
186 lines (171 loc) · 8.14 KB
/
Copy pathdocker.yml
File metadata and controls
186 lines (171 loc) · 8.14 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
name: Build and push image
on:
# Rebuild weekly even with no commits, so the image picks up base-OS security
# patches. `python:3.13-slim` is rebuilt upstream as its Debian base is
# patched, but a `FROM` alone pulls that in only when something triggers a
# build — so a quiet period leaves the deployed image accumulating known
# unpatched CVEs it would otherwise have shed for free.
#
# Safe to do on a schedule only because the dependency locks make an unattended
# rebuild reproducible: it re-resolves nothing, so the sole difference from the
# previous build is the patched base layer. Adding this while the requirements
# were lower-bound only would have made an unreviewed dependency bump land in
# production every Sunday. Mirrors the reservation app's workflow.
schedule:
- cron: '0 0 * * 0'
push:
# Manual trigger from the Actions tab. Kept because a push is not always
# enough on its own: during the August 2026 Actions outage, pushes were
# accepted and recorded but created no workflow runs at all, leaving no way
# to build a commit that was already on the remote short of an empty commit.
# Note this button only appears once the workflow file carrying it is on the
# default branch — GitHub reads the trigger list from there, not from the
# branch being dispatched. Mirrors the reservation app's workflow.
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# Renders the Helm chart with the real `helm` binary. This is the only place
# that happens: tests/test_chart_image.py parses the chart's YAML instead,
# because it also runs inside the Dockerfile's `test` stage where helm is not
# installed. The two check different things and neither replaces the other —
# the pytest guard pins values-level properties (registry-qualified
# repository, a tag the pipeline publishes), while this catches template
# syntax, bad indentation, and undefined values, none of which are visible
# without rendering.
chart:
name: Render Helm chart
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
# Deliberately floating: a chart linter *should* track the newest helm so
# deprecations surface here rather than at deploy time. The tradeoff is
# that a helm release can turn this red with no commit of ours — pin an
# exact version here if that becomes disruptive. (The opposite of the
# image tag policy in values.yaml, and for the opposite reason: this is a
# build-time check, not a deployed artifact.)
#
# Note that "latest" now resolves to Helm 4, not 3.
- name: Install Helm (latest stable)
uses: azure/setup-helm@v4
with:
version: latest
- name: Show Helm version
run: helm version
- name: Lint chart
run: |
helm lint helm/gpu-reservation-controller \
--set reservationApiUrl=https://gpures.example.edu
# The default path: every value at its shipped default apart from the one
# the chart marks required. `helm template` parses its own output into
# Kubernetes objects, so a rendering that produces malformed YAML fails
# here rather than at `kubectl apply` time.
- name: Render with defaults
run: |
helm template gpu-reservation-controller helm/gpu-reservation-controller \
--set reservationApiUrl=https://gpures.example.edu \
> /tmp/rendered-defaults.yaml
# Assert the object set rather than a count: a template that silently
# stopped rendering would still leave a plausible-looking number.
for kind in ServiceAccount ClusterRole ClusterRoleBinding Deployment Service; do
if ! grep -qx "kind: ${kind}" /tmp/rendered-defaults.yaml; then
echo "::error::chart rendered no ${kind}"
exit 1
fi
done
# The defect rank 11 was about: an unqualified repository silently
# becoming a docker.io/library reference.
grep -q 'image: "ghcr.io/' /tmp/rendered-defaults.yaml
# The optional blocks are empty by default, so the render above proves
# nothing about them. imagePullSecrets in particular is new and is exactly
# what a private GHCR reference depends on.
- name: Render with optional blocks populated
run: |
helm template gpu-reservation-controller helm/gpu-reservation-controller \
--set reservationApiUrl=https://gpures.example.edu \
--set imagePullSecrets[0].name=ghcr-pull-secret \
--set inboundApiTokenSecret.name=controller-inbound-token \
--set config.requiredGroupLabel=dsmlp/course \
--set config.defaultUsageGroup=cse151b \
--set config.podSchedulingGateName=gpu-reservation \
--set config.timezone=America/Los_Angeles \
--set config.supportContact=gpu-help@example.edu \
> /tmp/rendered-full.yaml
grep -q 'imagePullSecrets:' /tmp/rendered-full.yaml
grep -q 'name: ghcr-pull-secret' /tmp/rendered-full.yaml
grep -q 'INBOUND_API_TOKEN' /tmp/rendered-full.yaml
grep -q 'REQUIRED_GROUP_LABEL' /tmp/rendered-full.yaml
grep -q 'DEFAULT_USAGE_GROUP' /tmp/rendered-full.yaml
grep -q 'SUPPORT_CONTACT' /tmp/rendered-full.yaml
# The chart's one `required` guard. Asserting it fires keeps it from
# decaying into a default that silently deploys a controller pointed at
# no API at all.
- name: Reject a render missing the required value
run: |
if helm template gpu-reservation-controller helm/gpu-reservation-controller \
> /dev/null 2>&1; then
echo "::error::chart rendered without reservationApiUrl; the required guard is not firing"
exit 1
fi
echo "required guard fired as expected"
build-and-push-image:
runs-on: ubuntu-latest
# An image whose chart cannot render is not deployable, so it should not be
# published — the same reasoning as running the test suite before the
# registry login below.
needs: chart
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
# Builds the Dockerfile's `test` stage, which ends in `RUN pytest`, so a
# failing suite fails the job. This step is required: `test` is not in the
# build graph of the default `final` target, so BuildKit prunes it and the
# push build below never runs a single test.
#
# Deliberately before the registry login — a broken commit should fail
# without the job ever authenticating, let alone publishing.
- name: Run tests
uses: docker/build-push-action@v6
with:
context: .
target: test
push: false
- name: Log in to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate tags and labels
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# The first four are metadata-action's own defaults, restated because
# supplying `tags` replaces them rather than adding to them.
#
# `latest` is the addition. The action's `flavor.latest=auto` default
# emits it only for a semver git-tag push, and this repository pushes
# no git tags — so nothing ever published `latest`, while the Helm
# chart asked for exactly that. The chart's default was unpullable
# regardless of registry.
tags: |
type=schedule
type=ref,event=branch
type=ref,event=tag
type=ref,event=pr
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}