Skip to content

Commit 5de2daf

Browse files
authored
Merge pull request #110 from hyp3rd/feat/dist-mem-cache
feat: add Helm chart for k8s deployment and GitHub Release workflow
2 parents c81a342 + 3c00f3c commit 5de2daf

15 files changed

Lines changed: 728 additions & 51 deletions

.github/workflows/release.yml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---
2+
name: release
3+
4+
# Create the GitHub Release page on every `v*.*.*` tag push. Notes
5+
# are auto-generated from PRs merged since the previous tag; a small
6+
# header points readers at the CHANGELOG (which is the source of
7+
# truth for the *operator-facing* summary) and at the matching
8+
# container image published by image.yml.
9+
#
10+
# This workflow does NOT build artifacts — the container image is
11+
# the canonical artifact and image.yml owns it. If a future release
12+
# needs binary tarballs, add a separate matrix-build job here.
13+
14+
on:
15+
push:
16+
tags: ['v*.*.*']
17+
workflow_dispatch:
18+
inputs:
19+
tag:
20+
description: 'Existing tag to (re)create a release for'
21+
required: true
22+
23+
permissions:
24+
contents: write
25+
26+
jobs:
27+
release:
28+
name: Create GitHub Release
29+
runs-on: ubuntu-latest
30+
timeout-minutes: 10
31+
32+
steps:
33+
- uses: actions/checkout@v6
34+
with:
35+
# Fetch the full history so the auto-notes generator can
36+
# diff against the previous tag.
37+
fetch-depth: 0
38+
39+
- name: Resolve tag ref
40+
id: tag
41+
run: |
42+
# workflow_dispatch passes `inputs.tag`; tag pushes use
43+
# GITHUB_REF_NAME. Either way we end up with a clean
44+
# `v1.2.3`-style identifier in the output.
45+
if [[ -n "${{ inputs.tag }}" ]]; then
46+
echo "name=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
47+
else
48+
echo "name=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
49+
fi
50+
51+
- name: Compose release body header
52+
id: body
53+
run: |
54+
# The header pins readers to the canonical sources of
55+
# truth: the CHANGELOG (operator-facing summary) and the
56+
# container image (the canonical artifact). The
57+
# auto-generated PR list shows up directly underneath
58+
# via softprops's `generate_release_notes: true`.
59+
tag="${{ steps.tag.outputs.name }}"
60+
{
61+
echo "## hypercache ${tag}"
62+
echo ""
63+
echo "**Container image:** \`ghcr.io/${{ github.repository }}/hypercache-server:${tag}\`"
64+
echo ""
65+
echo "See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/${tag}/CHANGELOG.md) for the operator-facing summary."
66+
echo ""
67+
echo "---"
68+
} > /tmp/release-body.md
69+
70+
echo "path=/tmp/release-body.md" >> "$GITHUB_OUTPUT"
71+
72+
- name: Create GitHub Release
73+
uses: softprops/action-gh-release@v2
74+
with:
75+
tag_name: ${{ steps.tag.outputs.name }}
76+
name: ${{ steps.tag.outputs.name }}
77+
body_path: ${{ steps.body.outputs.path }}
78+
# Append the auto-generated PR list to the body above.
79+
generate_release_notes: true
80+
# Pre-release detection: any tag with a `-` (e.g.
81+
# `v1.2.3-rc1`, `v1.2.3-beta`) is flagged as pre-release.
82+
# Stable `v1.2.3` tags get the green "Latest" badge.
83+
prerelease: ${{ contains(steps.tag.outputs.name, '-') }}

.pre-commit-ci-config.yaml

Lines changed: 0 additions & 47 deletions
This file was deleted.

.pre-commit-config.yaml

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,27 @@ repos:
1414
- id: debug-statements
1515
- id: check-yaml
1616
files: .*\.(yaml|yml)$
17-
exclude: mkdocs.yml
18-
args: [--allow-multiple-documents]
17+
# mkdocs.yml uses custom !! tags PyYAML doesn't grok.
18+
# chart/**/templates/ contains Helm templates whose
19+
# `{{ ... }}` Go-template syntax PyYAML can't parse —
20+
# `helm lint` is the right validator for those.
21+
exclude: ^(mkdocs\.yml|chart/.*/templates/.*)$
22+
args: [ --allow-multiple-documents ]
1923
- id: requirements-txt-fixer
2024
- id: no-commit-to-branch
25+
- repo: https://github.com/gitleaks/gitleaks
26+
rev: v8.30.0
27+
hooks:
28+
- id: gitleaks
2129
- repo: https://github.com/adrienverge/yamllint.git
2230
rev: v1.38.0
2331
hooks:
2432
- id: yamllint
2533
files: \.(yaml|yml)$
26-
types: [file, yaml]
34+
# Same exclusion as check-yaml above — Helm templates
35+
# have their own validator (`helm lint`).
36+
exclude: ^chart/.*/templates/.*$
37+
types: [ file, yaml ]
2738
entry: yamllint --strict -f parsable
2839
- repo: https://github.com/hadolint/hadolint
2940
rev: v2.14.0
@@ -43,7 +54,7 @@ repos:
4354
- --no-summary
4455
- --files
4556
- .git/COMMIT_EDITMSG
46-
stages: [commit-msg]
57+
stages: [ commit-msg ]
4758
always_run: true
4859
- repo: https://github.com/markdownlint/markdownlint.git
4960
rev: v0.15.0

CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,36 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
3939
(replica path), `json.RawMessage` (non-owner-GET path), and the
4040
base64-heuristic length floors. Runs without docker for tight
4141
feedback during development.
42+
- **GitHub Release automation**
43+
[.github/workflows/release.yml](.github/workflows/release.yml)
44+
triggers on `v*.*.*` tag pushes and creates the GitHub Release
45+
page via `softprops/action-gh-release@v2`. The release body
46+
pins readers to the matching container image tag in GHCR and
47+
the CHANGELOG.md at that ref; PR-since-previous-tag notes are
48+
appended automatically. Pre-release tags (`v1.2.3-rc1`,
49+
`v1.2.3-beta`) are flagged via the `prerelease` field;
50+
`workflow_dispatch` lets operators (re-)create a release for
51+
an existing tag without re-tagging.
52+
- **Helm chart for k8s deployment** at
53+
[chart/hypercache/](chart/hypercache). Renders into a
54+
StatefulSet (stable per-pod hostnames so the `id@addr` seed
55+
list resolves deterministically), a headless Service for peer
56+
DNS, separate client and management Services, an optional
57+
chart-managed Secret for the auth token (or external Secret
58+
reference for production rotation), a PodDisruptionBudget
59+
(default `minAvailable: 4`), pod anti-affinity, and a
60+
hardened pod security context (non-root, read-only rootfs,
61+
all caps dropped). The ServiceAccount + Service + StatefulSet
62+
composition matches what `helm install` emits via `helm lint`
63+
and `helm template` against any kube-version. Configure cluster
64+
size, replication factor, capacity, heartbeat, hint TTL,
65+
rebalance interval, and resources via standard Helm values —
66+
see [chart/hypercache/values.yaml](chart/hypercache/values.yaml)
67+
for the full surface.
68+
- **Pre-commit excludes Helm templates** from `check-yaml` and
69+
`yamllint`. Both validators choke on Go-template `{{ ... }}`
70+
syntax inside the chart manifests; `helm lint` is the right
71+
validator for those, and CI runs that separately.
4272
- **Multi-arch container image workflow**
4373
[.github/workflows/image.yml](.github/workflows/image.yml) builds
4474
the `hypercache-server` Docker image for `linux/amd64` and

chart/hypercache/Chart.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
apiVersion: v2
3+
name: hypercache
4+
description: Distributed in-memory cache running the hypercache-server binary as a StatefulSet.
5+
type: application
6+
7+
# version is the chart's own version (bumped per chart change).
8+
# appVersion tracks the upstream container image tag — keep in sync
9+
# with the repo's release tags so `helm upgrade` reflects what's
10+
# running.
11+
version: 0.1.0
12+
appVersion: "v0.5.0"
13+
14+
home: https://github.com/hyp3rd/hypercache
15+
sources:
16+
- https://github.com/hyp3rd/hypercache
17+
18+
keywords:
19+
- cache
20+
- distributed
21+
- go
22+
23+
maintainers:
24+
- name: hyp3rd
25+
url: https://github.com/hyp3rd
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{{ .Chart.Name }} v{{ .Chart.AppVersion }} installed as release "{{ .Release.Name }}" in namespace "{{ .Release.Namespace }}".
2+
3+
Cluster size: {{ .Values.replicaCount }} pods, replication factor {{ .Values.cluster.replicationFactor }}.
4+
5+
Endpoints (from inside the cluster):
6+
7+
Client API : http://{{ include "hypercache.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.client.port }}
8+
Management : http://{{ include "hypercache.fullname" . }}-mgmt.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.mgmt.port }}
9+
10+
Per-pod hostnames (for direct dist-HTTP debugging):
11+
{{- range $i, $_ := until (.Values.replicaCount | int) }}
12+
{{ include "hypercache.fullname" $ }}-{{ $i }}.{{ include "hypercache.headlessServiceName" $ }}.{{ $.Release.Namespace }}.svc.cluster.local:{{ $.Values.ports.dist }}
13+
{{- end }}
14+
15+
Quick verification:
16+
17+
# Wait for all pods to become Ready.
18+
kubectl -n {{ .Release.Namespace }} rollout status statefulset/{{ include "hypercache.fullname" . }}
19+
20+
# Port-forward the client API to your workstation.
21+
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ include "hypercache.fullname" . }} 8080:{{ .Values.service.client.port }} &
22+
23+
# PUT a value and read it back from a different pod via the
24+
# service round-robin.
25+
curl -X PUT --data 'world' http://localhost:8080/v1/cache/greeting
26+
curl http://localhost:8080/v1/cache/greeting # should print: world
27+
28+
{{- if or .Values.auth.token.value .Values.auth.token.existingSecret }}
29+
30+
Auth is ENABLED — every request must carry an
31+
`Authorization: Bearer <token>` header. The token is mounted
32+
{{- if .Values.auth.token.existingSecret }} from existing secret `{{ .Values.auth.token.existingSecret }}` (key `{{ .Values.auth.token.existingSecretKey }}`).
33+
{{- else }} from the chart-managed secret `{{ include "hypercache.fullname" . }}-auth`.
34+
{{- end }}
35+
{{- end }}
36+
37+
Operations runbook: see docs/operations.md in the upstream
38+
repository for split-brain handling, hint-queue overflow,
39+
rebalance under load, and replica loss procedures.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{{/*
2+
Standard Helm helpers — name + fullname trimmed to k8s's 63-char
3+
limit, common labels block, headless-service name + per-pod DNS
4+
helper used by the seed-list template in the StatefulSet.
5+
*/}}
6+
7+
{{- define "hypercache.name" -}}
8+
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
9+
{{- end -}}
10+
11+
{{- define "hypercache.fullname" -}}
12+
{{- if .Values.fullnameOverride -}}
13+
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
14+
{{- else -}}
15+
{{- $name := default .Chart.Name .Values.nameOverride -}}
16+
{{- if contains $name .Release.Name -}}
17+
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
18+
{{- else -}}
19+
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
20+
{{- end -}}
21+
{{- end -}}
22+
{{- end -}}
23+
24+
{{- define "hypercache.headlessServiceName" -}}
25+
{{- printf "%s-headless" (include "hypercache.fullname" .) | trunc 63 | trimSuffix "-" -}}
26+
{{- end -}}
27+
28+
{{- define "hypercache.serviceAccountName" -}}
29+
{{- if .Values.serviceAccount.create -}}
30+
{{- default (include "hypercache.fullname" .) .Values.serviceAccount.name -}}
31+
{{- else -}}
32+
{{- default "default" .Values.serviceAccount.name -}}
33+
{{- end -}}
34+
{{- end -}}
35+
36+
{{- define "hypercache.labels" -}}
37+
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
38+
app.kubernetes.io/name: {{ include "hypercache.name" . }}
39+
app.kubernetes.io/instance: {{ .Release.Name }}
40+
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
41+
app.kubernetes.io/managed-by: {{ .Release.Service }}
42+
app.kubernetes.io/component: cache
43+
{{- end -}}
44+
45+
{{- define "hypercache.selectorLabels" -}}
46+
app.kubernetes.io/name: {{ include "hypercache.name" . }}
47+
app.kubernetes.io/instance: {{ .Release.Name }}
48+
{{- end -}}
49+
50+
{{/*
51+
hypercache.seedList builds the comma-separated `id@addr` value
52+
the dist backend needs to bootstrap a multi-process ring. Every
53+
pod gets the FULL list (including itself); the dist code's
54+
parseSeedSpec drops the self-entry by ID match. This means the
55+
SAME env value applies to every replica, so a StatefulSet (which
56+
only supports a single pod template) can express it.
57+
58+
Format: `<podname>@<podname>.<headless>.<ns>.svc.cluster.local:<port>`
59+
*/}}
60+
{{- define "hypercache.seedList" -}}
61+
{{- $fullname := include "hypercache.fullname" . -}}
62+
{{- $svc := include "hypercache.headlessServiceName" . -}}
63+
{{- $ns := .Release.Namespace -}}
64+
{{- $port := .Values.ports.dist | int -}}
65+
{{- $count := .Values.replicaCount | int -}}
66+
{{- $entries := list -}}
67+
{{- range $i, $_ := until $count -}}
68+
{{- $entry := printf "%s-%d@%s-%d.%s.%s.svc.cluster.local:%d" $fullname $i $fullname $i $svc $ns $port -}}
69+
{{- $entries = append $entries $entry -}}
70+
{{- end -}}
71+
{{- join "," $entries -}}
72+
{{- end -}}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{{- if .Values.podDisruptionBudget.enabled }}
2+
---
3+
# PodDisruptionBudget keeps quorum reachable during voluntary
4+
# disruptions (node drains, rolling node upgrades, kubectl
5+
# evict). With replicaCount=5 and replicationFactor=3, the
6+
# default `minAvailable: 4` keeps every key reachable: at most
7+
# one pod can be voluntarily down at a time, and any single
8+
# pod down still leaves a quorum of 2-of-3 owners for every
9+
# key. Operators on smaller clusters should override
10+
# minAvailable accordingly.
11+
apiVersion: policy/v1
12+
kind: PodDisruptionBudget
13+
metadata:
14+
name: {{ include "hypercache.fullname" . }}
15+
labels:
16+
{{- include "hypercache.labels" . | nindent 4 }}
17+
spec:
18+
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
19+
selector:
20+
matchLabels:
21+
{{- include "hypercache.selectorLabels" . | nindent 6 }}
22+
{{- end }}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{{- if and .Values.auth.token.value (not .Values.auth.token.existingSecret) }}
2+
---
3+
# Chart-managed Secret holding the bearer token. Created only
4+
# when `auth.token.value` is set AND `auth.token.existingSecret`
5+
# is empty — operators using sealed-secrets / external-secrets /
6+
# vault should provide an existing secret instead so the chart
7+
# stays out of the secret-rotation loop.
8+
apiVersion: v1
9+
kind: Secret
10+
metadata:
11+
name: {{ include "hypercache.fullname" . }}-auth
12+
labels:
13+
{{- include "hypercache.labels" . | nindent 4 }}
14+
type: Opaque
15+
stringData:
16+
{{ .Values.auth.token.existingSecretKey }}: {{ .Values.auth.token.value | quote }}
17+
{{- end }}

0 commit comments

Comments
 (0)