Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
name: release

# Create the GitHub Release page on every `v*.*.*` tag push. Notes
# are auto-generated from PRs merged since the previous tag; a small
# header points readers at the CHANGELOG (which is the source of
# truth for the *operator-facing* summary) and at the matching
# container image published by image.yml.
#
# This workflow does NOT build artifacts — the container image is
# the canonical artifact and image.yml owns it. If a future release
# needs binary tarballs, add a separate matrix-build job here.

on:
push:
tags: ['v*.*.*']
workflow_dispatch:
inputs:
tag:
description: 'Existing tag to (re)create a release for'
required: true

permissions:
contents: write

jobs:
release:
name: Create GitHub Release
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- uses: actions/checkout@v6
with:
# Fetch the full history so the auto-notes generator can
# diff against the previous tag.
fetch-depth: 0

- name: Resolve tag ref
id: tag
run: |
# workflow_dispatch passes `inputs.tag`; tag pushes use
# GITHUB_REF_NAME. Either way we end up with a clean
# `v1.2.3`-style identifier in the output.
if [[ -n "${{ inputs.tag }}" ]]; then
echo "name=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
else
echo "name=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
fi

- name: Compose release body header
id: body
run: |
# The header pins readers to the canonical sources of
# truth: the CHANGELOG (operator-facing summary) and the
# container image (the canonical artifact). The
# auto-generated PR list shows up directly underneath
# via softprops's `generate_release_notes: true`.
tag="${{ steps.tag.outputs.name }}"
{
echo "## hypercache ${tag}"
echo ""
echo "**Container image:** \`ghcr.io/${{ github.repository }}/hypercache-server:${tag}\`"
echo ""
echo "See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/${tag}/CHANGELOG.md) for the operator-facing summary."
echo ""
echo "---"
} > /tmp/release-body.md

echo "path=/tmp/release-body.md" >> "$GITHUB_OUTPUT"

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.tag.outputs.name }}
name: ${{ steps.tag.outputs.name }}
body_path: ${{ steps.body.outputs.path }}
# Append the auto-generated PR list to the body above.
generate_release_notes: true
# Pre-release detection: any tag with a `-` (e.g.
# `v1.2.3-rc1`, `v1.2.3-beta`) is flagged as pre-release.
# Stable `v1.2.3` tags get the green "Latest" badge.
prerelease: ${{ contains(steps.tag.outputs.name, '-') }}
47 changes: 0 additions & 47 deletions .pre-commit-ci-config.yaml

This file was deleted.

19 changes: 15 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,27 @@ repos:
- id: debug-statements
- id: check-yaml
files: .*\.(yaml|yml)$
exclude: mkdocs.yml
args: [--allow-multiple-documents]
# mkdocs.yml uses custom !! tags PyYAML doesn't grok.
# chart/**/templates/ contains Helm templates whose
# `{{ ... }}` Go-template syntax PyYAML can't parse —
# `helm lint` is the right validator for those.
exclude: ^(mkdocs\.yml|chart/.*/templates/.*)$
args: [ --allow-multiple-documents ]
- id: requirements-txt-fixer
- id: no-commit-to-branch
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.0
hooks:
- id: gitleaks
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.38.0
hooks:
- id: yamllint
files: \.(yaml|yml)$
types: [file, yaml]
# Same exclusion as check-yaml above — Helm templates
# have their own validator (`helm lint`).
exclude: ^chart/.*/templates/.*$
types: [ file, yaml ]
entry: yamllint --strict -f parsable
- repo: https://github.com/hadolint/hadolint
rev: v2.14.0
Expand All @@ -43,7 +54,7 @@ repos:
- --no-summary
- --files
- .git/COMMIT_EDITMSG
stages: [commit-msg]
stages: [ commit-msg ]
always_run: true
- repo: https://github.com/markdownlint/markdownlint.git
rev: v0.15.0
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,36 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
(replica path), `json.RawMessage` (non-owner-GET path), and the
base64-heuristic length floors. Runs without docker for tight
feedback during development.
- **GitHub Release automation** —
[.github/workflows/release.yml](.github/workflows/release.yml)
triggers on `v*.*.*` tag pushes and creates the GitHub Release
page via `softprops/action-gh-release@v2`. The release body
pins readers to the matching container image tag in GHCR and
the CHANGELOG.md at that ref; PR-since-previous-tag notes are
appended automatically. Pre-release tags (`v1.2.3-rc1`,
`v1.2.3-beta`) are flagged via the `prerelease` field;
`workflow_dispatch` lets operators (re-)create a release for
an existing tag without re-tagging.
- **Helm chart for k8s deployment** at
[chart/hypercache/](chart/hypercache). Renders into a
StatefulSet (stable per-pod hostnames so the `id@addr` seed
list resolves deterministically), a headless Service for peer
DNS, separate client and management Services, an optional
chart-managed Secret for the auth token (or external Secret
reference for production rotation), a PodDisruptionBudget
(default `minAvailable: 4`), pod anti-affinity, and a
hardened pod security context (non-root, read-only rootfs,
all caps dropped). The ServiceAccount + Service + StatefulSet
composition matches what `helm install` emits via `helm lint`
and `helm template` against any kube-version. Configure cluster
size, replication factor, capacity, heartbeat, hint TTL,
rebalance interval, and resources via standard Helm values —
see [chart/hypercache/values.yaml](chart/hypercache/values.yaml)
for the full surface.
- **Pre-commit excludes Helm templates** from `check-yaml` and
`yamllint`. Both validators choke on Go-template `{{ ... }}`
syntax inside the chart manifests; `helm lint` is the right
validator for those, and CI runs that separately.
- **Multi-arch container image workflow** —
[.github/workflows/image.yml](.github/workflows/image.yml) builds
the `hypercache-server` Docker image for `linux/amd64` and
Expand Down
25 changes: 25 additions & 0 deletions chart/hypercache/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
apiVersion: v2
name: hypercache
description: Distributed in-memory cache running the hypercache-server binary as a StatefulSet.
type: application

# version is the chart's own version (bumped per chart change).
# appVersion tracks the upstream container image tag — keep in sync
# with the repo's release tags so `helm upgrade` reflects what's
# running.
version: 0.1.0
appVersion: "v0.5.0"

home: https://github.com/hyp3rd/hypercache
sources:
- https://github.com/hyp3rd/hypercache

keywords:
- cache
- distributed
- go

maintainers:
- name: hyp3rd
url: https://github.com/hyp3rd
39 changes: 39 additions & 0 deletions chart/hypercache/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{{ .Chart.Name }} v{{ .Chart.AppVersion }} installed as release "{{ .Release.Name }}" in namespace "{{ .Release.Namespace }}".

Cluster size: {{ .Values.replicaCount }} pods, replication factor {{ .Values.cluster.replicationFactor }}.

Endpoints (from inside the cluster):

Client API : http://{{ include "hypercache.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.client.port }}
Management : http://{{ include "hypercache.fullname" . }}-mgmt.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.mgmt.port }}

Per-pod hostnames (for direct dist-HTTP debugging):
{{- range $i, $_ := until (.Values.replicaCount | int) }}
{{ include "hypercache.fullname" $ }}-{{ $i }}.{{ include "hypercache.headlessServiceName" $ }}.{{ $.Release.Namespace }}.svc.cluster.local:{{ $.Values.ports.dist }}
{{- end }}

Quick verification:

# Wait for all pods to become Ready.
kubectl -n {{ .Release.Namespace }} rollout status statefulset/{{ include "hypercache.fullname" . }}

# Port-forward the client API to your workstation.
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ include "hypercache.fullname" . }} 8080:{{ .Values.service.client.port }} &

# PUT a value and read it back from a different pod via the
# service round-robin.
curl -X PUT --data 'world' http://localhost:8080/v1/cache/greeting
curl http://localhost:8080/v1/cache/greeting # should print: world

{{- if or .Values.auth.token.value .Values.auth.token.existingSecret }}

Auth is ENABLED — every request must carry an
`Authorization: Bearer <token>` header. The token is mounted
{{- if .Values.auth.token.existingSecret }} from existing secret `{{ .Values.auth.token.existingSecret }}` (key `{{ .Values.auth.token.existingSecretKey }}`).
{{- else }} from the chart-managed secret `{{ include "hypercache.fullname" . }}-auth`.
{{- end }}
{{- end }}

Operations runbook: see docs/operations.md in the upstream
repository for split-brain handling, hint-queue overflow,
rebalance under load, and replica loss procedures.
72 changes: 72 additions & 0 deletions chart/hypercache/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{{/*
Standard Helm helpers — name + fullname trimmed to k8s's 63-char
limit, common labels block, headless-service name + per-pod DNS
helper used by the seed-list template in the StatefulSet.
*/}}

{{- define "hypercache.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{- define "hypercache.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}

{{- define "hypercache.headlessServiceName" -}}
{{- printf "%s-headless" (include "hypercache.fullname" .) | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{- define "hypercache.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{- default (include "hypercache.fullname" .) .Values.serviceAccount.name -}}
{{- else -}}
{{- default "default" .Values.serviceAccount.name -}}
{{- end -}}
{{- end -}}

{{- define "hypercache.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
app.kubernetes.io/name: {{ include "hypercache.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/component: cache
{{- end -}}

{{- define "hypercache.selectorLabels" -}}
app.kubernetes.io/name: {{ include "hypercache.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}

{{/*
hypercache.seedList builds the comma-separated `id@addr` value
the dist backend needs to bootstrap a multi-process ring. Every
pod gets the FULL list (including itself); the dist code's
parseSeedSpec drops the self-entry by ID match. This means the
SAME env value applies to every replica, so a StatefulSet (which
only supports a single pod template) can express it.

Format: `<podname>@<podname>.<headless>.<ns>.svc.cluster.local:<port>`
*/}}
{{- define "hypercache.seedList" -}}
{{- $fullname := include "hypercache.fullname" . -}}
{{- $svc := include "hypercache.headlessServiceName" . -}}
{{- $ns := .Release.Namespace -}}
{{- $port := .Values.ports.dist | int -}}
{{- $count := .Values.replicaCount | int -}}
{{- $entries := list -}}
{{- range $i, $_ := until $count -}}
{{- $entry := printf "%s-%d@%s-%d.%s.%s.svc.cluster.local:%d" $fullname $i $fullname $i $svc $ns $port -}}
{{- $entries = append $entries $entry -}}
{{- end -}}
{{- join "," $entries -}}
{{- end -}}
22 changes: 22 additions & 0 deletions chart/hypercache/templates/poddisruptionbudget.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{{- if .Values.podDisruptionBudget.enabled }}
---
# PodDisruptionBudget keeps quorum reachable during voluntary
# disruptions (node drains, rolling node upgrades, kubectl
# evict). With replicaCount=5 and replicationFactor=3, the
# default `minAvailable: 4` keeps every key reachable: at most
# one pod can be voluntarily down at a time, and any single
# pod down still leaves a quorum of 2-of-3 owners for every
# key. Operators on smaller clusters should override
# minAvailable accordingly.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "hypercache.fullname" . }}
labels:
{{- include "hypercache.labels" . | nindent 4 }}
spec:
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
selector:
matchLabels:
{{- include "hypercache.selectorLabels" . | nindent 6 }}
{{- end }}
17 changes: 17 additions & 0 deletions chart/hypercache/templates/secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{{- if and .Values.auth.token.value (not .Values.auth.token.existingSecret) }}
---
# Chart-managed Secret holding the bearer token. Created only
# when `auth.token.value` is set AND `auth.token.existingSecret`
# is empty — operators using sealed-secrets / external-secrets /
# vault should provide an existing secret instead so the chart
# stays out of the secret-rotation loop.
apiVersion: v1
kind: Secret
metadata:
name: {{ include "hypercache.fullname" . }}-auth
labels:
{{- include "hypercache.labels" . | nindent 4 }}
type: Opaque
stringData:
{{ .Values.auth.token.existingSecretKey }}: {{ .Values.auth.token.value | quote }}
{{- end }}
Loading
Loading