Skip to content
Open
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
5 changes: 5 additions & 0 deletions operator/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
bin/
*.test
*.o
cover.out
cover.html
17 changes: 17 additions & 0 deletions operator/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Build the manager binary
FROM golang:1.25-alpine AS build
WORKDIR /workspace
COPY go.mod go.sum ./
RUN go mod download
COPY cmd/ cmd/
COPY api/ api/
COPY internal/ internal/
RUN CGO_ENABLED=0 GOOS=linux go build -o manager ./cmd

# Minimal runtime image. Static binary (CGO disabled) runs on alpine; the
# image runs as an unprivileged user to satisfy runAsNonRoot.
FROM alpine:3.20
WORKDIR /
COPY --from=build /workspace/manager .
USER 65532:65532
ENTRYPOINT ["/manager"]
81 changes: 81 additions & 0 deletions operator/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Image URL to use all building/pushing image targets
IMG ?= versus-incident-operator:local
CONTROLLER_GEN ?= $(shell go env GOPATH)/bin/controller-gen

.PHONY: all
all: build

##@ Code generation

.PHONY: manifests
manifests: ## Generate CRD and RBAC manifests from markers.
$(CONTROLLER_GEN) crd paths=./api/... output:crd:artifacts:config=config/crd/bases
$(CONTROLLER_GEN) rbac:roleName=manager-role paths=./internal/... output:rbac:artifacts:config=config/rbac

.PHONY: generate
generate: ## Generate DeepCopy methods.
$(CONTROLLER_GEN) object paths=./api/...

##@ Development (required standard targets)

.PHONY: install
install: ## Install the CRD into the cluster.
kubectl apply -f config/crd/bases

.PHONY: dev
dev: generate ## Run the controller locally against the current kube context.
go run ./cmd

.PHONY: test
test: generate ## Run unit tests.
go test ./... -count=1

.PHONY: lint
lint: ## Static analysis (go vet).
go vet ./...

.PHONY: format
format: ## Format the code.
go fmt ./...

.PHONY: typecheck
typecheck: ## Compile-time type check (no output binary).
go build -o /dev/null ./...

.PHONY: clean
clean: ## Remove build artifacts.
rm -rf bin

##@ Build

.PHONY: build
build: generate format lint ## Build the manager binary.
go build -o bin/manager ./cmd

.PHONY: docker-build
docker-build: ## Build the manager image ($(IMG)).
docker build -t $(IMG) .

##@ Cluster lifecycle (up/down = deploy/teardown of the operator)

.PHONY: deploy
deploy: ## Deploy CRD + RBAC + manager to the cluster.
kubectl apply -f config/crd/bases
kubectl apply -f config/rbac/role.yaml
kubectl apply -f config/rbac/service_account.yaml
kubectl apply -f config/rbac/role_binding.yaml
kubectl apply -f config/manager/manager.yaml

.PHONY: undeploy
undeploy: ## Tear the controller down.
-kubectl delete -f config/manager/manager.yaml
-kubectl delete -f config/rbac/role_binding.yaml
-kubectl delete -f config/rbac/service_account.yaml
-kubectl delete -f config/rbac/role.yaml
-kubectl delete -f config/crd/bases

.PHONY: docker-up
docker-up: deploy ## Bring the operator up in the cluster (alias: deploy).

.PHONY: docker-down
docker-down: undeploy ## Take the operator down from the cluster (alias: undeploy).
16 changes: 16 additions & 0 deletions operator/PROJECT
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
domain: versuscontrol.io
layout:
- go.kubebuilder.io/v4
projectName: versus-incident-operator
repo: github.com/VersusControl/versus-incident/operator
resources:
- api:
crdVersion: v1
namespaced: true
controller: true
domain: versuscontrol.io
group: ops
kind: VersusIncident
path: github.com/VersusControl/versus-incident/operator/api/v1alpha1
version: v1alpha1
version: "3"
99 changes: 99 additions & 0 deletions operator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Versus Incident Operator

A Kubernetes operator (kubebuilder/controller-runtime, group `ops.versuscontrol.io`)
that manages Versus Incident deployments declaratively. One `VersusIncident`
custom resource is reconciled into a **ConfigMap + Deployment + Service**, with
owner references so deleting the CR garbage-collects everything.

It is an alternative to the Helm chart: same app, but driven by a CR the
controller continuously reconciles (self-healing, status reporting) instead of
a one-shot `helm install`.

## Layout (standard kubebuilder)

```
operator/
├── api/v1alpha1/ # VersusIncident types + generated DeepCopy
├── internal/controller/ # the reconciler
├── cmd/main.go # manager entrypoint
├── config/
│ ├── crd/bases/ # generated CRD
│ ├── rbac/ # generated ClusterRole + SA + binding
│ ├── manager/ # operator Deployment + Namespace
│ └── samples/ # sample VersusIncident CR
├── Dockerfile Makefile PROJECT
```

## Quick start (minikube)

```bash
# 0) build the operator image into minikube's docker daemon
eval $(minikube docker-env)
make -C operator docker-build # → versus-incident-operator:local

# 1) install CRD + RBAC + run the manager in-cluster
make -C operator deploy

# 2) create the app namespace + the secret the CR references
kubectl create namespace versus
kubectl -n versus create secret generic versus-operator-secrets \
--from-literal=gateway_secret="$(openssl rand -hex 32)" \
--from-literal=telegram_bot_token='<bot-token>' \
--from-literal=telegram_chat_id='<chat-id>' \
--from-literal=agent_ai_api_key='<google-api-key>'

# 3) create a VersusIncident — the operator builds the workload
kubectl apply -f operator/config/samples/ops_v1alpha1_versusincident.yaml

# 4) observe
kubectl get versusincident -n versus # short name: vi
kubectl get deploy,svc,cm -n versus -l app.kubernetes.io/managed-by=versus-incident-operator
```

Deleting the CR removes the Deployment/Service/ConfigMap automatically:

```bash
kubectl delete vi demo -n versus
```

## CRD shape

```yaml
apiVersion: ops.versuscontrol.io/v1alpha1
kind: VersusIncident
spec:
image: { repository, tag, pullPolicy }
replicas: 1
gatewaySecretName: <secret with key gateway_secret>
telegram:
enabled: true
secretName: <secret with telegram_bot_token, telegram_chat_id>
agent:
enable: true
mode: detect # training | shadow | detect
pollInterval: 15s
ai:
enable: true
provider: gemini # openai | gemini (maps to the endpoint internally)
model: gemini-2.5-flash-lite
apiKeySecretName: <secret with agent_ai_api_key>
sources: # mirrors agent_sources.yaml
- name: demo-app
type: file # file | loki | elasticsearch
enable: true
file: { path: /app/data/app.log, fromBeginning: true }
status:
readyReplicas: <n>
conditions: [ { type: Ready, ... } ]
```

Secrets are **referenced, never embedded** in the CR. The rendered in-pod
`config.yaml` includes the full agent detection config (regex / redaction /
miner / catalog / service_patterns) so the agent matches out of the box.

## Regenerate after API changes

```bash
make -C operator manifests generate # CRD + RBAC + DeepCopy
make -C operator build
```
24 changes: 24 additions & 0 deletions operator/api/v1alpha1/groupversion_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Package v1alpha1 contains API Schema definitions for the ops v1alpha1 API
// group. The single kind, VersusIncident, declaratively describes one Versus
// Incident deployment; the controller reconciles it into a ConfigMap,
// Deployment and Service.
//
// +kubebuilder:object:generate=true
// +groupName=ops.versuscontrol.io
package v1alpha1

import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
// GroupVersion is group version used to register these objects.
GroupVersion = schema.GroupVersion{Group: "ops.versuscontrol.io", Version: "v1alpha1"}

// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}

// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)
Loading