Skip to content

Commit fdd677a

Browse files
committed
create a helm chart and build docker image
1 parent 86daa4e commit fdd677a

File tree

21 files changed

+940
-201
lines changed

21 files changed

+940
-201
lines changed

.envrc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/bash
2+
3+
# Automatically sets up your devbox environment whenever you cd into this
4+
# directory via our direnv integration:
5+
6+
eval "$(devbox generate direnv --print-envrc)"
7+
8+
# check out https://www.jetify.com/docs/devbox/ide_configuration/direnv/
9+
# for more details

.github/workflows/ci.yaml

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
tags: ["*"]
7+
pull_request:
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
outputs:
13+
version: ${{ steps.version.outputs.version }}
14+
is_release: ${{ steps.version.outputs.is_release }}
15+
permissions:
16+
contents: read
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
with:
21+
fetch-depth: 0
22+
23+
- name: Determine version
24+
id: version
25+
run: |
26+
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
27+
RAW="${GITHUB_REF#refs/tags/}"
28+
VERSION="${RAW#v}"
29+
IS_RELEASE="true"
30+
else
31+
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
32+
LAST_TAG="${LAST_TAG#v}"
33+
VERSION="${LAST_TAG}-sha.${GITHUB_SHA::7}"
34+
IS_RELEASE="false"
35+
fi
36+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
37+
echo "is_release=${IS_RELEASE}" >> "$GITHUB_OUTPUT"
38+
39+
- name: Install devbox
40+
uses: jetify-com/devbox-install-action@v0.14.0
41+
42+
- name: Install project dependencies
43+
run: devbox install
44+
45+
- name: Helm lint
46+
run: devbox run lint
47+
48+
- name: Kind end-to-end test
49+
env:
50+
CLUSTER_NAME: ci-evict-rollout
51+
run: devbox run test
52+
53+
publish-image:
54+
needs: build
55+
runs-on: ubuntu-latest
56+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
57+
permissions:
58+
contents: read
59+
packages: write
60+
steps:
61+
- name: Checkout
62+
uses: actions/checkout@v4
63+
64+
- name: Set up Docker Buildx
65+
uses: docker/setup-buildx-action@v3
66+
67+
- name: Log in to GHCR
68+
uses: docker/login-action@v3
69+
with:
70+
registry: ghcr.io
71+
username: ${{ github.actor }}
72+
password: ${{ secrets.GITHUB_TOKEN }}
73+
74+
- name: Build and push kubectl+jq image
75+
env:
76+
IMAGE_BASE: ghcr.io/${{ github.repository }}/kubectl-jq
77+
VERSION: ${{ needs.build.outputs.version }}
78+
run: |
79+
IMAGE_BASE=$(echo "${IMAGE_BASE}" | tr '[:upper:]' '[:lower:]')
80+
docker buildx build \
81+
-f Dockerfile.kubectl-jq \
82+
--platform linux/amd64,linux/arm64 \
83+
-t "${IMAGE_BASE}:${VERSION}" \
84+
-t "${IMAGE_BASE}:latest" \
85+
--push .
86+
87+
publish-chart:
88+
needs: build
89+
runs-on: ubuntu-latest
90+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
91+
permissions:
92+
contents: read
93+
packages: write
94+
steps:
95+
- name: Checkout
96+
uses: actions/checkout@v4
97+
with:
98+
fetch-depth: 0
99+
100+
- name: Install Helm
101+
uses: azure/setup-helm@v4
102+
with:
103+
version: v3.14.4
104+
105+
- name: Update Chart metadata
106+
env:
107+
VERSION: ${{ needs.build.outputs.version }}
108+
run: |
109+
sed -i -E "s/^version:.*/version: ${VERSION}/" chart/evict-to-rollout/Chart.yaml
110+
sed -i -E "s/^appVersion:.*/appVersion: \"${VERSION}\"/" chart/evict-to-rollout/Chart.yaml
111+
112+
- name: Login to GHCR for Helm
113+
run: |
114+
echo "${{ secrets.GITHUB_TOKEN }}" | \
115+
helm registry login ghcr.io \
116+
--username "${{ github.actor }}" \
117+
--password-stdin
118+
119+
- name: Package and push Helm chart
120+
run: |
121+
mkdir -p dist
122+
helm package chart/evict-to-rollout --destination dist
123+
VERSION=$(awk '/^version:/ {print $2}' chart/evict-to-rollout/Chart.yaml | tr -d '"')
124+
REPO=$(echo "${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')
125+
helm push "dist/evict-to-rollout-${VERSION}.tgz" "oci://ghcr.io/${REPO}"
126+

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.devbox

Dockerfile.kubectl-jq

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# syntax=docker/dockerfile:1.6
2+
3+
FROM alpine:3.20
4+
5+
ARG KUBECTL_VERSION=v1.30.2
6+
ARG TARGETARCH
7+
8+
RUN apk add --no-cache bash curl jq ca-certificates \
9+
&& case "${TARGETARCH:-amd64}" in \
10+
amd64|x86_64) ARCH=amd64 ;; \
11+
arm64|aarch64) ARCH=arm64 ;; \
12+
*) echo "Unsupported architecture: ${TARGETARCH}" && exit 1 ;; \
13+
esac \
14+
&& curl -fsSLo /usr/local/bin/kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl" \
15+
&& chmod +x /usr/local/bin/kubectl
16+
17+
ENTRYPOINT ["/bin/bash"]
18+

README.md

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,77 @@ export DRY_RUN=true
5555
./evict_to_rollout.sh
5656
```
5757

58-
### 3. Deploy as CronJob
58+
## Deployment Options
5959

60-
See `cronjob.yaml` for the full manifest including RBAC permissions.
60+
### Helm (recommended)
61+
62+
This repository ships a Helm chart (`chart/evict-to-rollout`) so you can tweak the schedule, annotation selector, and naming without forking the manifest.
63+
64+
```bash
65+
helm upgrade --install evict-to-rollout \
66+
oci://ghcr.io/hivemindtechnologies/evict-to-rollout \
67+
--version 0.1.0 \
68+
--namespace kube-system --create-namespace \
69+
--set schedule="*/2 * * * *" \
70+
--set annotationSelector.key="evict-with-rollout" \
71+
--set annotationSelector.value="true"
72+
```
73+
74+
Key values:
75+
76+
| Value | Description | Default |
77+
| --- | --- | --- |
78+
| `schedule` | Cron expression for how often to scan nodes | `*/1 * * * *` |
79+
| `annotationSelector.key`/`.value` | Annotation pair that marks pods for rollout | `evict-with-rollout` / `true` |
80+
| `image.repository` / `.tag` | Container image that provides `kubectl` + `jq` | `ghcr.io/hivemindtechnologies/evict-to-rollout/kubectl-jq` / *(empty = use chart `appVersion`)* |
81+
| `serviceAccount.create` | Whether to create a dedicated SA | `true` |
82+
| `rbac.create` | Whether to install ClusterRole + binding | `true` |
83+
84+
See `chart/evict-to-rollout/values.yaml` for the full list.
85+
86+
## Development & Testing
87+
88+
This repo ships a `devbox.json` so everyone (including CI) uses the same versions of `helm`, `kubectl`, `kind`, and `jq`.
89+
90+
```bash
91+
# Start a dev shell with all tools:
92+
devbox shell
93+
94+
# Lint the chart:
95+
devbox run lint
96+
97+
# Run the end-to-end test (requires Docker since it spins up kind):
98+
devbox run test
99+
```
100+
101+
The test script (`scripts/test-kind.sh`) creates a 3-node kind cluster, installs the Helm chart, deploys a sample annotated app, cordons a node, runs the controller job manually, and asserts that the deployment was restarted and rescheduled onto a different node.
102+
103+
GitHub Actions mirrors the same flow via `.github/workflows/ci.yaml`:
104+
105+
- on every PR, it runs `helm lint` and the kind-based integration test.
106+
- on pushes to `main`, it additionally publishes:
107+
- the multi-arch `kubectl-jq` image tagged as `latest` and `${LAST_TAG}-sha.${GITHUB_SHA::7}`
108+
- a Helm chart tagged as `${LAST_TAG}-sha.${GITHUB_SHA::7}` to `oci://ghcr.io/hivemindtechnologies/evict-to-rollout`
109+
- on git tag pushes (e.g. `v0.2.0`), the same workflow publishes **stable** artifacts tagged with the release version
110+
111+
### Building the controller image
112+
113+
The CronJob runs a tiny Alpine image containing `kubectl`, `jq`, `bash`, and CA certificates. Build it (multi-arch) and push to GHCR with:
114+
115+
```bash
116+
docker buildx build \
117+
-f Dockerfile.kubectl-jq \
118+
--platform linux/amd64,linux/arm64 \
119+
-t ghcr.io/hivemindtechnologies/evict-to-rollout/kubectl-jq:latest \
120+
--push .
121+
```
122+
### Release workflow
123+
124+
The CI pipeline keeps versions in sync automatically:
125+
126+
- For pushes to `main`, it reads the most recent git tag (or `0.0.0` if none exists) and publishes snapshot artifacts tagged as `<last-tag>-sha.<short-sha>`.
127+
- For pushes to annotated tags (e.g. `v0.3.0`), it strips the `v` prefix and publishes both the Docker image and the Helm chart with the exact release version.
128+
- The pipeline patches `chart/evict-to-rollout/Chart.yaml` on the fly so that `version` and `appVersion` match the artifact tag, and the default image tag in the chart inherits from `appVersion`.
129+
130+
For local testing the kind script (`devbox run test`) builds the image and loads it directly into the cluster, so no registry push is required.
61131

chart/evict-to-rollout/Chart.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
apiVersion: v2
2+
name: evict-to-rollout
3+
description: Trigger rollout restarts for annotated workloads before draining nodes
4+
type: application
5+
version: 0.1.0
6+
appVersion: "0.1.0"
7+
keywords:
8+
- kubernetes
9+
- availability
10+
- eviction
11+
- pdb
12+
- cronjob
13+
maintainers:
14+
- name: Hivemind Technologies
15+
url: https://github.com/HivemindTechnologies
16+
sources:
17+
- https://github.com/HivemindTechnologies/evict-to-rollout
18+
home: https://github.com/HivemindTechnologies/evict-to-rollout
19+
20+

evict_to_rollout.sh renamed to chart/evict-to-rollout/files/evict_to_rollout.sh

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
set -euo pipefail
33

44
# Configuration
5-
ANNOTATION_KEY="evict-with-rollout"
6-
ANNOTATION_VALUE="true"
5+
ANNOTATION_KEY=${ANNOTATION_KEY:-"evict-with-rollout"}
6+
ANNOTATION_VALUE=${ANNOTATION_VALUE:-"true"}
77
DRY_RUN=${DRY_RUN:-false} # Set to true to print actions without executing
88

99
log() {
@@ -116,3 +116,5 @@ for NODE in $NODES; do
116116
done
117117

118118
log "Done."
119+
120+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
To verify your installation:
2+
1. Check the CronJob status:
3+
kubectl get cronjob {{ include "evict-to-rollout.fullname" . }} -n {{ .Release.Namespace }}
4+
2. Inspect the latest job/pod logs:
5+
kubectl logs job/$(kubectl get jobs -n {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }} -o jsonpath='{.items[-1:].metadata.name}') -n {{ .Release.Namespace }}
6+
7+
To trigger a dry run manually:
8+
kubectl create job --from=cronjob/{{ include "evict-to-rollout.fullname" . }} evict-to-rollout-debug -n {{ .Release.Namespace }} \
9+
--dry-run=client -o yaml | kubectl apply -f -
10+
11+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{{- define "evict-to-rollout.name" -}}
2+
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
3+
{{- end -}}
4+
5+
{{- define "evict-to-rollout.fullname" -}}
6+
{{- if .Values.fullnameOverride -}}
7+
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
8+
{{- else -}}
9+
{{- $name := default .Chart.Name .Values.nameOverride -}}
10+
{{- if contains $name .Release.Name -}}
11+
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
12+
{{- else -}}
13+
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
14+
{{- end -}}
15+
{{- end -}}
16+
{{- end -}}
17+
18+
{{- define "evict-to-rollout.serviceAccountName" -}}
19+
{{- if .Values.serviceAccount.create -}}
20+
{{- if .Values.serviceAccount.name -}}
21+
{{- .Values.serviceAccount.name | trunc 63 | trimSuffix "-" -}}
22+
{{- else -}}
23+
{{- printf "%s-sa" (include "evict-to-rollout.fullname" .) | trunc 63 | trimSuffix "-" -}}
24+
{{- end -}}
25+
{{- else -}}
26+
{{- .Values.serviceAccount.name | default "default" -}}
27+
{{- end -}}
28+
{{- end -}}
29+
30+
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{{- if .Values.rbac.create }}
2+
apiVersion: rbac.authorization.k8s.io/v1
3+
kind: ClusterRole
4+
metadata:
5+
name: {{ printf "%s-role" (include "evict-to-rollout.fullname" .) | trunc 63 | trimSuffix "-" }}
6+
labels:
7+
app.kubernetes.io/name: {{ include "evict-to-rollout.name" . }}
8+
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
9+
app.kubernetes.io/instance: {{ .Release.Name }}
10+
app.kubernetes.io/managed-by: {{ .Release.Service }}
11+
rules:
12+
- apiGroups: [""]
13+
resources: ["nodes"]
14+
verbs: ["get", "list", "watch"]
15+
- apiGroups: [""]
16+
resources: ["pods"]
17+
verbs: ["get", "list", "watch"]
18+
- apiGroups: ["apps"]
19+
resources: ["replicasets"]
20+
verbs: ["get", "list"]
21+
- apiGroups: ["apps"]
22+
resources: ["deployments"]
23+
verbs: ["get", "list", "patch"]
24+
{{- end }}
25+
26+

0 commit comments

Comments
 (0)