A Kubernetes-native cascade scenario controller that orchestrates sequential execution of containerized processing modules. The controller receives HTTP requests, creates Kubernetes Jobs for each module in a scenario pipeline, and tracks their execution status via Custom Resource Definitions (CRDs).
┌──────────────────────────────────────────────────────────────────┐
│ CascadeScenarioController │
│ │
│ ┌──────────┐ ┌──────────┐ ┌────────────┐ ┌──────────┐ │
│ │ HTTP │───▶│ Handlers │───▶│ Process │───▶│ k8sclient│ │
│ │ Server │ │ package │ │ package │ │ package │ │
│ └──────────┘ └──────────┘ └────────────┘ └──────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Prometheus Exporter │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ External │ │ Kubernetes │ │ CRDs: │
│ Modules │ │ Jobs (Batch) │ │ - CascadeRun │
│ (POST /) │ │ │ │ - CascadeAuto│
└──────────┘ └──────────────┘ │ Operator │
└──────────────┘
POST /run request with processing parameters (TURL, IUID, OName, SName).process.ImageProcessing().k8sclient package.POST / with module status (run name, module name, result).CascadeRun CRD status.├── main.go # Application entry point, DI setup, HTTP server
├── handlers/
│ ├── handlers.go # HTTP handlers, request validation, rate limiting
│ └── handlers_test.go # Handler unit tests
├── process/
│ ├── process.go # Scenario orchestration logic
│ ├── retry.go # Reusable retry helper with exponential backoff
│ ├── retry_test.go # Retry logic tests
│ └── retry_test.go # Process package tests
├── k8sclient/
│ ├── k8sclient.go # Kubernetes client operations (Jobs, CRDs)
│ └── k8sclient_test.go # K8s client unit tests
├── cascadescenario/
│ ├── cascadeScenario.go # Config parsing (JSON scenario definitions)
│ ├── cascadeScenario_test.go # Scenario config tests
│ └── test/
│ ├── test_success.json # Test config: 3 modules (success path)
│ └── test_fail_first.json # Test config: fail on 2nd module
├── api/v1alpha1/
│ ├── cascadeautooperator_types.go # CascadeAutoOperator CRD types
│ ├── cascaderun_types.go # CascadeRun CRD types
│ └── groupversion_info.go # API group version registration
├── prometheus-exporter/
│ ├── prometheus-exporter.go # Prometheus metrics, middleware
│ └── prometheus-exporter_test.go # Metrics tests
├── webhook/
│ ├── webhook.go # Generic webhook sender
│ └── webhook_test.go # Webhook tests
├── logger/
│ └── logger.go # Structured logging (zap)
├── Dockerfile # Multi-stage distroless Docker build
├── Makefile # Build automation
├── go.mod / go.sum # Go modules
└── .releaserc.yaml # Semantic release configuration
POST / — Receive Module StatusReceives execution results from individual processing modules and forwards them to the scenario controller's channel for CRD status updates.
{
"runname": "cascadeautooperator-ip-abc12",
"modulename": "grayscale",
"moduleresult": "success"
}
| Field | Type | Required | Description |
|---|---|---|---|
runname |
string | ✅ | Name of the CascadeRun CR (generated) |
modulename |
string | ✅ | Name of the module that completed |
moduleresult |
string | ✅ | Result of the module execution |
| Status Code | Description |
|---|---|
200 OK |
Module status received and forwarded |
400 Bad Request |
Invalid JSON or missing required fields |
runname is required (non-empty)modulename is required (non-empty)moduleresult is required (non-empty)POST /run — Start Scenario RunInitiates a new cascade scenario execution. The controller validates the input and launches the scenario pipeline.
{
"turlinfo": "s3://bucket/path/to/input.tgz",
"iuid": "33e65374-ee8e-4da6-a2e7-71a9cc39d222",
"oname": "organization-name",
"sname": "source-name"
}
| Field | Type | Required | Description |
|---|---|---|---|
turlinfo |
string | ✅ | S3 path to the input data |
iuid |
string | ✅ | Unique UUID for the transfer/run |
oname |
string | ❌ | Organization name (tagged in metrics) |
sname |
string | ❌ | Source name (tagged in metrics) |
| Status Code | Description |
|---|---|
200 OK |
Scenario run initiated (async processing) |
400 Bad Request |
Invalid JSON or missing required fields |
503 Service Unavailable |
Server is shutting down |
turlinfo is required (non-empty)iuid is required (non-empty)503 Service UnavailableGET /healthz — Health CheckReturns the application health status including build metadata.
{
"app_name": "cascadescenariocontroller-auto",
"status": "OK",
"tag": "v1.2.3",
"hash": "abcdef1234567890",
"date": "2026-07-20.17:30:00"
}
The tag, hash, and date fields are injected at build time via LD flags in the Makefile.
| Status Code | Description |
|---|---|
200 OK |
Application is healthy |
GET /ready — Readiness ProbeSimple readiness check for Kubernetes liveness/readiness probes.
| Status Code | Description |
|---|---|
200 OK |
Application is ready |
GET /metrics — Prometheus MetricsExposes Prometheus metrics in the standard text format. Served via the standard promhttp.Handler.
| Status Code | Description |
|---|---|
200 OK |
Prometheus metrics output |
The controller reads a JSON configuration file that defines the cascade scenario modules. Default path: /tmp/configuration. Can be overridden via the CONFIG_PATH environment variable.
cascadescenario/test/test_success.json):{
"cascademodules": [
{
"modulename": "grayscale",
"configuration": {
"foo": "bar",
"spamm": "eggs"
},
"backoffLimit": 0,
"template": {
"spec": {
"containers": [
{
"name": "grayscale",
"image": "ghcr.io/randsw/grayscale:0.1.1",
"imagePullPolicy": "IfNotPresent"
}
],
"restartPolicy": "OnFailure"
}
}
}
]
}
| Field | Type | Description |
|---|---|---|
cascademodules |
array | Ordered list of modules to execute sequentially |
├ modulename |
string | Module name (appended with UUID for uniqueness) |
├ configuration |
map[string]string | Key-value pairs injected as pod environment vars |
├ activeDeadlineSeconds |
int64 (optional) | Job active deadline in seconds |
├ backoffLimit |
int32 (optional) | Number of retries before marking job failed |
├ ttlSecondsAfterFinished |
int32 (optional) | Seconds after which to auto-delete finished job |
└ template |
PodTemplateSpec | Standard Kubernetes PodTemplateSpec for the job |
| Variable | Default | Description |
|---|---|---|
CONFIG_PATH |
/tmp/configuration |
Path to scenario configuration JSON file |
POD_NAMESPACE |
cascade-operator |
Kubernetes namespace for Jobs and CRDs |
SCENARIO_NAME |
cascadeautooperator-ip |
Name of the CascadeAutoOperator CRD resource |
SID |
UnderTest |
Scenario identifier |
OUT_MINIO_ADDRESS |
http://example.com/test-out/ |
Base URL for output S3/Minio storage |
KUBECONFIG |
(in-cluster or ~/.kube/config) |
Path to kubeconfig file (for out-of-cluster) |
# Build the binary
make build
# The output binary: cascadescenariocontroller_auto
Build flags include:
CGO_ENABLED=0 — static binary for Alpine/distrolessGOOS=linux GOARCH=amd64 — Linux AMD64 targettag, hash, date into the binary at handlers/handlers.go:21-23# Build Docker image
docker build -t ghcr.io/randsw/cascadescenariocontroller:latest .
# Push to registry
docker push ghcr.io/randsw/cascadescenariocontroller:latest
The Dockerfile uses a multi-stage build:
golang:1.26 — compiles the binary via make buildgcr.io/distroless/static:nonroot — minimal, secure runtimeThe controller requires two CRDs to be installed in the cluster:
CascadeAutoOperatorcascade.cascade.net/v1alpha1cascadeautooperatorsactive, succeeded, failed, resultCascadeRuncascade.cascade.net/v1alpha1cascaderunsob, src, pid, scenarioname, modulesresult (module-level results), info (final output address or "Failed")Apply the CRD manifests to your cluster:
kubectl apply -f config/crd/
kubectl create namespace cascade-operator
Example deployment manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: cascade-scenario-controller
namespace: cascade-operator
spec:
replicas: 1
selector:
matchLabels:
app: cascade-scenario-controller
template:
metadata:
labels:
app: cascade-scenario-controller
spec:
serviceAccountName: cascade-controller
containers:
- name: controller
image: ghcr.io/randsw/cascadescenariocontroller:latest
ports:
- containerPort: 8080
env:
- name: CONFIG_PATH
value: "/etc/cascade/config.json"
- name: POD_NAMESPACE
value: "cascade-operator"
- name: OUT_MINIO_ADDRESS
value: "http://minio-service:9000/output/"
- name: SCENARIO_NAME
value: "cascadeautooperator-ip"
- name: SID
value: "production"
volumeMounts:
- name: config
mountPath: /etc/cascade
volumes:
- name: config
configMap:
name: cascade-scenario-config
---
apiVersion: v1
kind: Service
metadata:
name: cascade-scenario-controller
namespace: cascade-operator
spec:
selector:
app: cascade-scenario-controller
ports:
- port: 8080
targetPort: 8080
type: ClusterIP
kubectl create configmap cascade-scenario-config \
--namespace cascade-operator \
--from-file=config.json=./scenario-config.json
# Check pod status
kubectl get pods -n cascade-operator
# Check endpoints
kubectl get svc -n cascade-operator
# Test health endpoint
kubectl port-forward -n cascade-operator svc/cascade-scenario-controller 8080:8080 &
curl http://localhost:8080/healthz
The controller exposes comprehensive Prometheus metrics via the GET /metrics endpoint.
| Metric | Type | Labels | Description |
|---|---|---|---|
http_requests_total |
Counter | path |
Total HTTP requests by path |
response_status |
Counter | status, path |
HTTP response status codes |
http_response_time_seconds |
Histogram | path |
HTTP request duration |
| Metric | Type | Labels | Description |
|---|---|---|---|
success_scenario_total |
Counter | scenarioName, OName, SName |
Successful scenario executions |
failed_scenario_total |
Counter | scenarioName, OName, SName |
Failed scenario executions |
scenario_current_runs |
Gauge | scenarioName, OName, SName |
Currently running scenarios |
scenarion_execution_time_seconds |
Histogram | scenarioName, OName, SName |
Full scenario execution duration |
job_execution_time_seconds |
Histogram | scenarioName, moduleName, OName, SName, status |
Per-job execution duration |
The PrometheusMiddleware wraps all HTTP routes to automatically capture:
You can create a Grafana dashboard using the metric names above. Key panels:
The controller implements graceful shutdown on SIGTERM / SIGINT signals (see main.go:71):
SetShutDown(true) causes POST /run to return 503ProcessManager.Load() until all goroutines completeshutdown.cascade.cascade.net/finalizer from the CRDsrv.Shutdown() with 10-second timeoutAdditionally, a background goroutine (watchDeletionTimestamp) monitors CRD deletion timestamps and triggers the same shutdown sequence if the CRD is deleted externally.
The controller includes a token-bucket rate limiter (golang.org/x/time/rate):
429 Too Many Requests when limit exceededConfigured in NewServer():
rateLimiter: rate.NewLimiter(rate.Limit(100), 200)
The controller uses go.uber.org/zap for structured logging:
Info, Warn, Debug, Error, FatalExample log format:
{"level":"info","ts":"2026-07-20T17:30:00+03:00","func":"main.main","msg":"Start serving http request...","address":":8080"}
The project uses semantic-release with conventional commits for automated versioning and changelog generation:
| Commit Type | Release Impact |
|---|---|
fix |
Patch release |
feat |
Minor release |
breaking |
Major release |
docs, chore, test, refactor, style |
No release |
The changelog is maintained in CHANGELOG.md and updated automatically on each release.
Apache License 2.0 — See api/v1alpha1/cascadeautooperator_types.go for details.