Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b499064
chore(deps): add renovate.json
renovate-interworks[bot] Aug 25, 2026
263951f
chore: pin CLI tool versions with mise and add CLAUDE.md guidance
henryjarend Aug 28, 2026
3fc31d5
feat: add TypesenseApiKey CRD and reconciler
henryjarend Aug 28, 2026
9a7d5ca
fix: correct always-true condition in StatefulSet scale guard
henryjarend Aug 28, 2026
c7d73bc
test: add envtest coverage for TypesenseApiKeyReconciler
henryjarend Aug 28, 2026
7661c7e
chore: rename container registry to ghcr.io/interworks, pin golangci-…
henryjarend Aug 28, 2026
1fcee26
chore: regenerate helm chart for TypesenseApiKey CRD and registry rename
henryjarend Aug 28, 2026
5f8434c
fix: cap Typesense thread pool size in the cluster-1 sample
henryjarend Aug 28, 2026
24c5f3e
test: add e2e coverage for TypesenseCluster quorum and TypesenseApiKe…
henryjarend Aug 28, 2026
6b038b8
docs: document TypesenseApiKey in README and CLAUDE.md
henryjarend Aug 28, 2026
e15eaf8
chore: bump version to 0.5.0 for the TypesenseApiKey feature release
henryjarend Aug 28, 2026
cc7bfae
ci: run lint and test workflows automatically on push/PR
henryjarend Aug 28, 2026
5d13a32
chore: empty push test trigger
henryjarend Aug 28, 2026
90def93
Merge pull request #1 from InterWorks/renovate/configure
henryjarend Aug 28, 2026
c33e1a8
fix: wrap long lines in e2e test to satisfy lll
henryjarend Aug 28, 2026
2aa008f
Merge pull request #2 from InterWorks/feature/typesense-api-key
henryjarend Aug 28, 2026
ee75d8a
fix: clean up pre-existing golangci-lint findings
henryjarend Aug 28, 2026
65a2cd7
Merge pull request #22 from InterWorks/lint/cleanup-42-findings-issue-5
henryjarend Aug 28, 2026
fd76a4d
chore(deps): update dependency kubectl to v1.36.4
renovate-interworks[bot] Aug 28, 2026
3a6eca2
Merge pull request #7 from InterWorks/renovate/kubectl-1.x
henryjarend Aug 31, 2026
79227e2
ci: add go dep to mise
henryjarend Aug 31, 2026
9c45c58
ci: update to use mise to install tooling (#23)
henryjarend Aug 31, 2026
d6cd4b9
215 use ldflags for internal versioning (#276)
akyriako Aug 26, 2026
4e64435
Merge branch 'main' into merge-from-source
henryjarend Aug 31, 2026
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
10 changes: 6 additions & 4 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
name: Lint

on:
push:
branches:
- main
pull_request:
workflow_dispatch:

jobs:
Expand All @@ -11,10 +15,8 @@ jobs:
- name: Clone the code
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Set up tools via mise
uses: jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 # v4.2.4

- name: Run linter
uses: golangci/golangci-lint-action@v8
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/releases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ jobs:
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"

- name: Install Deps
uses: jdx/mise-action@v4.3.0

- name: Run chart-releaser
uses: helm/chart-releaser-action@v1.6.0
env:
Expand Down
17 changes: 6 additions & 11 deletions .github/workflows/test-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,20 @@ name: E2E Tests

on:
workflow_dispatch:
pull_request:
types: [ labeled ]

jobs:
test-e2e:
name: Run on Ubuntu
name: Run End to End Tests
if: ${{ github.event.label.name == 'e2e' }}
runs-on: ubuntu-latest
steps:
- name: Clone the code
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Install the latest version of kind
run: |
curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
- name: Set up tools via mise
uses: jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 # v4.2.4

- name: Verify kind installation
run: kind version
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
name: Tests

on:
push:
branches:
- main
pull_request:
workflow_dispatch:

jobs:
Expand All @@ -11,10 +15,8 @@ jobs:
- name: Clone the code
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Set up tools via mise
uses: jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 # v4.2.4

- name: Running Tests
run: |
Expand Down
56 changes: 30 additions & 26 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,33 +1,15 @@
version: "2"
run:
timeout: 5m
allow-parallel-runners: true

issues:
# don't skip warning about doc comments
# don't exclude the default set of lint
exclude-use-default: false
# restore some of the defaults
# (fill in the rest as needed)
exclude-rules:
- path: "api/*"
linters:
- lll
- path: "internal/*"
linters:
- dupl
- lll
linters:
disable-all: true
default: none
enable:
- copyloopvar
- dupl
- errcheck
- copyloopvar
- ginkgolinter
- goconst
- gocyclo
- gofmt
- goimports
- gosimple
- govet
- ineffassign
- lll
Expand All @@ -36,12 +18,34 @@ linters:
- prealloc
- revive
- staticcheck
- typecheck
- unconvert
- unparam
- unused

linters-settings:
revive:
settings:
revive:
rules:
- name: comment-spacings
exclusions:
generated: lax
rules:
- name: comment-spacings
- linters:
- lll
path: api/*
- linters:
- dupl
- lll
path: internal/*
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
92 changes: 92 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

TyKO (Typesense Kubernetes Operator) — a Kubernetes operator, built with Operator SDK / kubebuilder (go.kubebuilder.io/v4), that manages the full lifecycle of highly-available Typesense clusters via two CRDs (group `ts.opentelekomcloud.com/v1alpha1`): `TypesenseCluster` and `TypesenseApiKey`. It automates ConfigMaps, Secrets, PVCs, StatefulSets, Services, Ingress/HTTPRoute, metrics scrapers, Typesense API key issuance/rotation, and — most notably — Raft quorum discovery/recovery without sidecars.

## Common commands

```bash
# Build & code hygiene
make manifests generate # regenerate CRDs/RBAC (controller-gen) and deepcopy code — run after editing api/v1alpha1 types or +kubebuilder markers
make fmt vet # go fmt / go vet
make build # manifests generate fmt vet + go build -> bin/manager
make run # run the operator against the current kubeconfig context (out-of-cluster)

# Lint
make lint # golangci-lint run
make lint-fix # golangci-lint run --fix

# Tests
make test # runs manifests generate fmt vet setup-envtest, then envtest-based unit/integration tests for all packages except test/
# Run a single package's tests directly once envtest assets are set up:
KUBEBUILDER_ASSETS="$(setup-envtest use -p path)" go test ./internal/controller/... -run TestSomething -v

make test-e2e # spins up a local Kind cluster, runs test/e2e (ginkgo), tears the cluster down
# CERT_MANAGER_INSTALL_SKIP=true to skip cert-manager install

# CRDs / deployment (kustomize-based)
make install # install CRDs into the cluster in ~/.kube/config
make deploy # deploy the controller via config/default kustomize overlay
make deploy-with-samples # install + apply sample CRs
make undeploy / uninstall # tear down

# Helm chart (charts/typesense-operator) is generated from kustomize output — see `make helmify`
```

Tests use Ginkgo/Gomega (`internal/controller/suite_test.go`, `test/e2e`), driven through envtest for controller-level tests and a real Kind cluster for e2e.

## Architecture

### Single controller, many reconcile phases

There is one reconciler, `TypesenseClusterReconciler` (`internal/controller/typesensecluster_controller.go`), driving one CRD (`TypesenseCluster`). `Reconcile()` runs a fixed sequence of phases, each in its own file, each with its own idempotent update strategy (documented as a comment above the call site in `typesensecluster_controller.go`):

1. `ReconcileSecret` (`typesensecluster_secret.go`) — admin API key Secret; **immutable**, never updated after creation.
2. `ReconcileConfigMap` (`typesensecluster_configmap.go`) — the peer-nodes ConfigMap (`ClusterNodesConfigMap`); updated in place when the node list changes, and its return value (`configMapUpdated *bool`) drives whether the rest of reconcile treats this pass as bootstrap vs. a quorum-affecting change.
3. `ReconcileServices` (`typesensecluster_services.go`)
4. `ReconcileIngress` (`typesensecluster_ingress.go`)
5. `ReconcileHttpRoute` (`typesensecluster_httproute.go`) — Gateway API HTTPRoute, alternative to Ingress
6. `ReconcileScraper` (`typesensecluster_scraper.go`) — drops and recreates on change
7. `ReconcilePodMonitor` (`typesensecluster_podmonitor.go`) — Prometheus PodMonitor/metrics exporter
8. `ReconcileStatefulSet` (`typesensecluster_statefulset.go`) — the Typesense StatefulSet itself; full spec diff/update. `typesensecluster_statefulset_hash.go` computes a hash of the pod template to detect when a rolling update is actually needed.
9. `ReconcileQuorum` (`typesensecluster_quorum.go`, helpers in `typesensecluster_quorum_helpers.go`, types in `typesensecluster_quorum_types.go`) — talks to the Typesense HTTP health/stats API on each pod to compute Raft quorum health (available vs. min-required nodes, write/read lag), restarts unscheduled pods, and derives a `ConditionQuorum` status.

Each phase failure sets a "NotReady" status condition (`typesensecluster_condition_types.go`, `setConditionNotReady`/`setConditionReady`) and short-circuits the reconcile loop by returning early — phases are strictly sequential and later phases assume earlier ones succeeded.

### Bootstrapping vs. reconciling, and the ConfigMap-triggered requeue dance

After the StatefulSet phase, the controller distinguishes two actions based on whether `ReconcileConfigMap` reported a change:
- `configMapUpdated == nil`: nothing changed — either steady-state (`Reconciling`) or first-ever creation (`Bootstrapping`, short 15s requeue).
- `configMapUpdated != nil` and `true`: the peer list changed, so `forcePodsConfigMapUpdate` force-restarts pods to pick up the new mounted ConfigMap (kubelet syncs configmaps ~every 60s on its own), the condition is set to `QuorumNotReadyWaitATerm`, and reconcile requeues after `configMapRequeuePeriod` (2 min) to give kubelet time to propagate before checking quorum again.

Only once the ConfigMap has settled does the controller call `ReconcileQuorum` and fold its `ConditionQuorum` result into the CR's status/events (`QuorumNeedsAttention*` conditions surface as Warning events requiring manual intervention — lagging writes or out-of-memory/disk; anything else not-ready is retried).

### Second controller: TypesenseApiKey

`TypesenseApiKeyReconciler` (`internal/controller/typesenseapikey_controller.go`) drives the `TypesenseApiKey` CRD, independently of `TypesenseClusterReconciler`. It talks directly to a target `TypesenseCluster`'s Typesense `/keys` HTTP API (`typesenseapikey_client.go`, resolved via the `%s-svc.<namespace>.svc.cluster.local` Service DNS name and the cluster's admin key Secret) rather than reconciling Kubernetes sub-resources:

- **Create** (`status.keyId == nil`): calls `POST /keys` with the spec's `actions`/`collections`/`description`/`expiresAt`/`value`, then writes the plaintext value to a Secret named `<key-name>-typesense-key` (`typesenseapikey_secret.go`) and records `status.keyId`/`status.valuePrefix`/`status.observedGeneration`/`status.secretRef`.
- **Rotate** (`key.Generation != status.observedGeneration`, i.e. spec changed): Typesense keys are immutable, so this deletes the old remote key by id and creates a new one (same Secret name, so consumers don't need to change references).
- **Drift check** (steady state, every `apiKeyReconcileRequeuePeriod` = 5 min): re-fetches the remote key; a key deleted out-of-band is recreated, a key edited out-of-band is rotated.
- **Delete**: finalizer-guarded (`ApiKeyFinalizer`); deletes the remote Typesense key before removing the finalizer.

Condition type is `Ready` (`typesenseapikey_condition_types.go`), same convention as `TypesenseCluster`, so `kubectl wait --for=condition=Ready` works uniformly on both CRDs.

### API types layout (`api/v1alpha1/`)

`typesensecluster_types.go` holds the root `TypesenseClusterSpec`/`Status`; the sub-structs for each concern live in their own `typesensecluster_types_*.go` files (`_storage`, `_service`, `_ingress`, `_httproute`, `_scraper`, `_metrics`, `_healthcheck`, `_securitycontexts`), with helper methods in `typesensecluster_types_helpers.go`. `typesenseapikey_types.go` holds `TypesenseApiKeySpec`/`Status` for the second CRD. `zz_generated.deepcopy.go` is generated — never hand-edit it; run `make generate` instead.

### Config entry point

`cmd/main.go` wires up the manager: scheme registration, leader election, health/readiness probes, metrics server, and controller setup (`SetupWithManager`). It also builds the extra clients the reconciler needs beyond the controller-runtime client (`DiscoveryClient`, `ClientSet`, `InCluster` detection) since quorum health checks and pod restarts need direct API access.

## Making changes to the CRD

Any change to `api/v1alpha1/typesensecluster_types*.go` or `api/v1alpha1/typesenseapikey_types.go` (new field, changed `+kubebuilder:` marker, etc.) requires `make manifests generate` before building/testing — this regenerates CRD YAML under `config/crd/` and `zz_generated.deepcopy.go`. The Helm chart under `charts/typesense-operator` is produced from the kustomize output via `make helmify` and should be regenerated alongside CRD/manifest changes, not edited by hand.

## golangci-lint notes

Config in `.golangci.yml`: `api/*` is exempt from `lll` (long lines — CRD marker comments run long); `internal/*` is exempt from `dupl` and `lll`. Keep new code lint-clean under the enabled linter set (see file) rather than adding new exemptions.
11 changes: 6 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ BUNDLE_METADATA_OPTS ?= $(BUNDLE_CHANNELS) $(BUNDLE_DEFAULT_CHANNEL)
#
# For example, running 'make bundle-build bundle-push catalog-build catalog-push' will build and push both
# opentelekomcloud.com/typesense-operator-bundle:$VERSION and opentelekomcloud.com/typesense-operator-catalog:$VERSION.
IMAGE_TAG_BASE ?= quay.io/akyriako/typesense-operator
IMAGE_TAG_BASE ?= ghcr.io/interworks/typesense-operator

# BUNDLE_IMG defines the image:tag used for the bundle.
# You can use it as an arg. (E.g make bundle-build BUNDLE_IMG=<some-registry>/<project-name-bundle>:<tag>)
Expand All @@ -50,9 +50,9 @@ endif
# This is useful for CI or a project to utilize a specific version of the operator-sdk toolkit.
OPERATOR_SDK_VERSION ?= v1.39.0
# Image URL to use all building/pushing image targets
DOCKER_HUB_NAME ?= quay.io/akyriako#$(shell docker info | sed '/Username:/!d;s/.* //')
DOCKER_HUB_NAME ?= ghcr.io/interworks#$(shell docker info | sed '/Username:/!d;s/.* //')
IMG_NAME ?= typesense-operator
IMG_TAG ?= 0.5.0-dev.1
IMG_TAG ?= 0.5.0
IMG ?= $(DOCKER_HUB_NAME)/$(IMG_NAME):$(IMG_TAG)

# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
Expand Down Expand Up @@ -137,7 +137,8 @@ test: manifests generate fmt vet setup-envtest ## Run tests.
# The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally.
# CertManager is installed by default; skip with:
# - CERT_MANAGER_INSTALL_SKIP=true
KIND_CLUSTER ?= memcached-operator-test-e2e
KIND_CLUSTER ?= typesense-operator-test-e2e
KIND ?= kind

.PHONY: setup-test-e2e
setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist
Expand Down Expand Up @@ -344,7 +345,7 @@ bundle: manifests kustomize operator-sdk ## Generate bundle manifests and metada

.PHONY: bundle-build
bundle-build: ## Build the bundle image.
docker build -f bundle.Dockerfile -t $(BUNDLE_IMG) .
$(CONTAINER_TOOL) build -f bundle.Dockerfile -t $(BUNDLE_IMG) .

.PHONY: bundle-push
bundle-push: ## Push the bundle image.
Expand Down
9 changes: 9 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,13 @@ resources:
kind: TypesenseCluster
path: github.com/akyriako/typesense-operator/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: opentelekomcloud.com
group: ts
kind: TypesenseApiKey
path: github.com/akyriako/typesense-operator/api/v1alpha1
version: v1alpha1
version: "3"
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,44 @@ spec:

You can find more examples and analytical installation instructions in the [Installation](https://akyriako.github.io/typesense-operator-docs/docs/installation/) and [Configuration](https://akyriako.github.io/typesense-operator-docs/docs/crds) guides.

## 🔑 Issuing Scoped API Keys

Beyond provisioning the cluster itself, TyKO can issue and manage Typesense API keys declaratively
via the `TypesenseApiKey` CRD, so scoped keys for your applications (e.g. a search-only key for a
public frontend) live in Git next to the cluster they belong to, instead of being generated by hand
and passed around out-of-band.

```yaml
apiVersion: ts.opentelekomcloud.com/v1alpha1
kind: TypesenseApiKey
metadata:
name: search-only-key
spec:
clusterRef:
name: ts-kind
description: read-only key for the search frontend
actions:
- documents:search
collections:
- "*"
```

The operator creates the key against the referenced cluster's Typesense API using its admin key,
writes the plaintext value to a Secret named `<name>-typesense-key`, and records the key's Typesense
id and status in `.status`:

```bash
kubectl get typesenseapikey search-only-key
# NAME CLUSTER KEY ID PHASE READY
# search-only-key ts-kind 1 Ready True
```

Editing `spec` (actions, collections, description, expiry) rotates the key: the old Typesense key is
deleted and a new one is created and written to the same Secret, so consumers keep referencing the
same Secret name across a rotation. The key is also re-checked periodically for drift (e.g. deleted
or edited directly through the Typesense API) and healed automatically. Deleting the
`TypesenseApiKey` CR deletes the corresponding key from Typesense.

## 📚 Documentation

- [Getting Started](https://akyriako.github.io/typesense-operator-docs/docs/getting-started)
Expand Down
Loading