Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
1566 lines (1389 loc) · 81.3 KB
/
Copy pathMakefile
File metadata and controls
1566 lines (1389 loc) · 81.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Every target in this file is a command, not a file on disk. The list used to
# be maintained by hand across two long lines and had drifted — 31 targets were
# missing, so a file of the same name in the repo would have silently shadowed
# them. Deriving it from the file itself cannot drift.
.PHONY: $(shell awk -F: '/^[a-zA-Z0-9_-]+:([^=]|$$)/ {print $$1}' $(MAKEFILE_LIST))
.DEFAULT_GOAL := help
TIMER := $(shell date +%s)
# ── Load version pins ─────────────────────────────────────────────────────────
include versions.env
# ── Resolve kates binary (installed CLI > local build) ────────────────────────
KATES_BIN := $(shell command -v kates 2>/dev/null || echo "./build/kates")
DETECTED_VALUES := .build/values-detected.yaml
# ── Configuration ────────────────────────────────────────────────────────────
#
# Every tunable lives here rather than scattered through the file, where a
# variable could be defined hundreds of lines below the first target that used
# it. Override any of them on the command line:
#
# make kates-local KATES_NS=staging
# make kafka-deploy ENV=prod
# Target environment overlay: kind | dev | staging | prod
ENV ?= kind
# Extra values file layered on top of an overlay (kafka-deploy-generic-custom)
VALUES_FILE ?=
# Kind cluster and image registry
CLUSTER_NAME ?= panda
REGISTRY ?= ghcr.io/bmscomp
# Kates release. KATES_RELEASE is the Helm release name and therefore the
# prefix of every resource the chart creates, so the local targets derive
# `deployment/$(KATES_RELEASE)` and `$(KATES_RELEASE)-postgresql` from it
# rather than hardcoding names that break under a different release.
KATES_NS ?= kates
KATES_RELEASE ?= kates
KATES_IMAGE ?= kates:latest
# Chart locations. Versions are read FROM the charts, never duplicated here —
# a hardcoded pull tag once sat five releases behind appVersion, so "pulled the
# image" quietly meant "pulled a stale one".
CHART_DIR := charts/kates
UI_CHART_DIR := charts/kafka-ui
KAFKA_CHART_DIR := charts/kafka-cluster
CONNECT_CHART_DIR := charts/connect-cluster
CHAOS_CHART_DIR := charts/kates-chaos
STRIMZI_CHART_DIR := charts/strimzi-operator
PLATFORM_CHART_DIR := charts/kates-platform
MM2_CHART_DIR := charts/mirror-maker2
KAFKA_COMMON_DIR := charts/kafka-common
LEGACY_KAFKA_CHART_DIR := charts/legacy-kafka
# The release name every MM2 document installs as. Override per invocation:
# make mm2-deploy MM2_RELEASE=mm2-dr
MM2_RELEASE ?= mm2
MM2_NAMESPACE ?= kafka
# The TARGET cluster the mirror writes to (and the helpers below read from).
KAFKA_NAMESPACE ?= kafka
KAFKA_CLUSTER ?= krafter
TOPIC ?= kates.orders
chart_version = $(shell grep '^version:' $(1)/Chart.yaml | awk '{print $$2}')
CHART_VERSION := $(call chart_version,charts/kates)
KAFKA_CHART_VERSION := $(call chart_version,charts/kafka-cluster)
CONNECT_CHART_VERSION := $(call chart_version,charts/connect-cluster)
CHAOS_CHART_VERSION := $(call chart_version,charts/kates-chaos)
STRIMZI_CHART_VERSION := $(call chart_version,charts/strimzi-operator)
PLATFORM_CHART_VERSION := $(call chart_version,charts/kates-platform)
MM2_CHART_VERSION := $(call chart_version,charts/mirror-maker2)
KATES_APP_VERSION := $(shell grep '^appVersion:' $(CHART_DIR)/Chart.yaml | awk '{print $$2}' | tr -d '"')
# ── Cluster detection (single source of truth) ───────────────────────────────
##@ Cluster & Infrastructure
detect: check-prerequisites ## Detect cluster capabilities into .build/values-detected.yaml
@mkdir -p .build
@echo "🔍 Detecting cluster configuration..."
@if [ ! -x "$(KATES_BIN)" ]; then \
echo "⚠️ kates binary not found, building it now..."; \
$(MAKE) cli-build; \
if [ ! -x "$(KATES_BIN)" ]; then cp cli/dist/kates $(KATES_BIN) 2>/dev/null || true; fi; \
fi
@$(KATES_BIN) detect --generate-values --values-output $(DETECTED_VALUES) --quiet
@echo "✅ Detection complete → $(DETECTED_VALUES)"
# ── Main deployment pipeline ─────────────────────────────────────────────────
all: check-prerequisites ## Complete setup (cluster, all services)
@echo "🚀 Launching complete cluster setup via Kates Unified Orchestrator..."
@echo ""
@# ── Step 1: Cluster connectivity ──
@if kubectl cluster-info >/dev/null 2>&1; then \
CONTEXT=$$(kubectl config current-context); \
echo "✅ Kubernetes cluster reachable (context: $$CONTEXT)"; \
else \
echo "⚠️ No Kubernetes cluster reachable — creating Kind cluster..."; \
$(MAKE) cluster; \
fi
@echo ""
@if [ ! -x "$(KATES_BIN)" ]; then \
echo "⚠️ kates binary not found, building it now..."; \
$(MAKE) cli-build >/dev/null; \
fi
@echo "Select Deployment Topology:"
@echo " 1) Single Namespace (dev mode, everything in 'kates-stack')"
@echo " 2) Isolated Namespaces (production mode, separate namespaces for kafka/kates/chaos)"
@# KATES_CHECKOUT: an installed release build would deploy its published charts, not this checkout's.
@read -p "Enter choice [1/2]: " choice; \
if [ "$$choice" = "1" ]; then \
KATES_CHECKOUT=$(CURDIR) $(KATES_BIN) deploy --topology single --namespace kates-stack --with-schema-registry apicurio; \
else \
KATES_CHECKOUT=$(CURDIR) $(KATES_BIN) deploy --topology isolated --with-schema-registry apicurio; \
fi
@echo ""
@echo "Step: Exposing service ports..."
@./scripts/port-forward.sh || true
@echo ""
@echo "✅ Complete setup finished in $$(( $$(date +%s) - $(TIMER) ))s"
@echo ""
@echo "🔗 Access points:"
@echo " - Apicurio Registry: http://localhost:30082"
@echo " - Kates: http://localhost:30083"
@echo " - Chaos: execution plane only (no UI) — 'make chaos-status'"
@echo ""
# Every cheap gate CI · Lint and CI · Backend run, in one command, so "make
# sure CI passes" has a local answer before the push. Each tool is skipped
# with a notice when it is not installed (brew install actionlint shellcheck
# hadolint yamllint golangci-lint); CI never skips.
check: check-help check-versions check-chart-tests ## Run every cheap CI gate locally (lint, format, pins)
@if command -v actionlint >/dev/null 2>&1; then actionlint && echo "OK: actionlint"; else echo "⚠️ actionlint not installed — skipping"; fi
@if command -v shellcheck >/dev/null 2>&1; then shellcheck -x -S warning -e SC2317,SC2034,SC2155 scripts/*.sh kates-ci.sh && echo "OK: shellcheck"; else echo "⚠️ shellcheck not installed — skipping"; fi
@if command -v hadolint >/dev/null 2>&1; then hadolint --failure-threshold error kates/Dockerfile kates/Dockerfile.native Dockerfile.connect Dockerfile.legacy-kafka tester/Dockerfile && echo "OK: hadolint"; else echo "⚠️ hadolint not installed — skipping"; fi
@if command -v yamllint >/dev/null 2>&1; then yamllint -c .yamllint.yaml .github config charts && echo "OK: yamllint"; else echo "⚠️ yamllint not installed — skipping"; fi
@if command -v golangci-lint >/dev/null 2>&1; then (cd cli && go vet ./... && golangci-lint run ./...) && echo "OK: go vet, golangci-lint"; else echo "⚠️ golangci-lint not installed — skipping"; fi
@(cd kates && ./mvnw -B -ntp -q spotless:check) && echo "OK: spotless"
@./scripts/check-versions.sh --workflows
@python3 scripts/chart_bumps.py
# Assert the Strimzi pins agree across all five places that declare one, and the
# kind toolchain pins between versions.env and config/cluster.yaml.
# Also run in CI (.github/workflows/ci.yml, Helm Lint job).
check-versions: ## Verify the Strimzi and kind version pins agree
@./scripts/check-versions.sh
# Terminal-compatibility gate: runs the real binary piped, with NO_COLOR,
# TERM=dumb, --plain, KATES_ASCII, and no TTY. Also run in CI (CLI Tests job).
check-cli-compat: ## Verify CLI output degrades correctly across terminals
@./scripts/check-cli-compat.sh
# The help text is generated from the `## ` comments on each target, so it
# cannot list a target that no longer exists — but it CAN miss a new one that
# was added without a comment. This is what stops that from creeping back:
# the previous hand-written help had drifted to 39 undocumented targets.
# NOTE: a description must not itself contain the comment marker — help splits
# on the LAST one, so it would print only the tail of the line.
check-chart-tests: ## Fail if a chart test calls an endpoint that does not exist
@./scripts/check-chart-test-paths.sh
# Every series a chart's alerts, recording rules and dashboard read must be
# one its JMX exporter rules can produce — simulated over the MBean catalogue
# in scripts/metric-contract/<chart>.yaml. An alert on a name no rule emits
# installs fine and never fires. Also run in CI (ci-mirror-maker2.yml).
# Every board in dashboards/ is generated from its board.py and synced into the
# charts that deliver it, and into the two chartless install bundles under
# dashboards/bundle/ (see dashboards/README.md, "Installing these anywhere").
# --check fails when a board was edited without regenerating, or when a chart
# copy or a bundle file was edited by hand.
check-dashboards: ## Verify the dashboards are in sync and their layout holds
@./scripts/gen-dashboards.py --check
@./scripts/check-dashboards.py
gen-dashboards: ## Regenerate every dashboard, its chart copies and the install bundles
@./scripts/gen-dashboards.py
# dashboards/install.py is one of the four documented install routes and is
# called from ci-kafka-charts.yml, and it had no test at all — the only route
# whose failures would be found by a person running it. The suite talks to a
# real http.server that answers like Grafana rather than to a mock, because the
# things most likely to be wrong in an HTTP client (the base64 in a basic-auth
# header, overwrite:true, an empty 200 body) are exactly what a mock removes.
# Standard library only: no pytest, like every other gate here.
check-install-script: ## Test dashboards/install.py against a fake Grafana
@python3 -m unittest discover -s dashboards -p 'test_*.py' -q
# Every other dashboard check is static: the contract asks whether a NAME is
# producible, promtool asks whether a query PARSES, check-dashboards.py asks
# about layout and documentation. None of them runs a query, and a correct
# name in a query that selects on a label the series does not carry draws an
# empty panel — which looks exactly like a healthy idle cluster. Point this at
# a Prometheus with real data, or hand it captured /metrics bodies:
#
# make check-dashboards-live PROMETHEUS_URL=http://localhost:9090
# scripts/check-dashboards-live.py --scrape /tmp/scrape/broker.txt
#
# CI runs it in the weekly metrics-live job over that job's own captures.
check-dashboards-live: ## Run the boards' selectors against a live Prometheus
@test -n "$(PROMETHEUS_URL)" || { echo "set PROMETHEUS_URL=http://host:9090"; exit 2; }
@./scripts/check-dashboards-live.py --prometheus "$(PROMETHEUS_URL)"
# Everything above treats a board as JSON. This starts a real Grafana of each
# version, provisions all twelve, and drives a headless browser over them, so
# it answers a question the JSON cannot: does the FRONTEND draw this. A panel
# type a given Grafana does not have renders "Panel plugin not found" and the
# API is perfectly happy about it.
#
# make check-grafana-compat # the pinned version
# make check-grafana-compat GRAFANA_VERSIONS="9.5.21 10.4.19 11.6.6 12.3.1"
#
# Needs docker; the browser half additionally needs node with playwright and
# skips itself with a notice when that is absent.
check-grafana-compat: ## Load and render every board on one or more Grafana versions
@./scripts/check-grafana-compat.sh $(GRAFANA_VERSIONS)
check-metric-contract: kafka-chart-deps connect-chart-deps mm2-chart-deps strimzi-chart-deps monitoring-chart-deps ## Verify the alerts and dashboards read series the exporter rules produce
@for c in $$(./scripts/check-metric-contract.sh --list); do ./scripts/check-metric-contract.sh "$$c" --quiet || exit 1; done
# Render each Strimzi chart across every overlay and toggle in
# scripts/chart-matrix/<chart>.yaml and check the output against the pinned
# Strimzi CRDs (pruned fields, missing required fields, enums, duplicate keys),
# the render's assertions, and the rails. Also run in CI (ci-kafka-charts.yml).
check-chart-matrix: mm2-chart-deps kafka-chart-deps connect-chart-deps strimzi-chart-deps monitoring-chart-deps ## Render the Strimzi charts across their overlays and check every render
@for c in strimzi-operator kafka-cluster connect-cluster mirror-maker2 monitoring; do ./scripts/check-chart-matrix.py "$$c" --offline || exit 1; done
# kafka-cluster ships Strimzi's own JMX exporter rules unchanged; this compares
# them with upstream for STRIMZI_VERSION (needs network).
check-strimzi-metrics: ## Verify the vendored Strimzi metrics rules match upstream
@./scripts/check-strimzi-metrics.sh
check-help: ## Fail if any target is missing its help description
@undocumented=$$(awk -F: '/^[a-zA-Z0-9_-]+:([^=]|$$)/ && $$0 !~ /##/ {print " " $$1}' $(MAKEFILE_LIST)); \
if [ -n "$$undocumented" ]; then \
echo "❌ These targets have no '## description', so 'make help' will not list them:"; \
echo "$$undocumented"; \
exit 1; \
fi; \
echo "OK: all $$(awk -F: '/^[a-zA-Z0-9_-]+:([^=]|$$)/ {print $$1}' $(MAKEFILE_LIST) | sort -u | wc -l | tr -d ' ') targets are documented"
# Check prerequisites — only kubectl and helm are strictly required for generic clusters
check-prerequisites: ## Verify docker, kind, kubectl and helm are installed
@echo "🔍 Checking prerequisites..."
@command -v kubectl >/dev/null 2>&1 || { echo "❌ kubectl not found"; exit 1; }
@command -v helm >/dev/null 2>&1 || { echo "❌ helm not found"; exit 1; }
@echo "✅ All prerequisites met"
# Start Kind cluster only
cluster: ## Start Kind cluster only
@echo "🎯 Starting Kind cluster..."
./scripts/start-cluster.sh
# Deploy monitoring stack (auto-detect provider)
monitoring-chart-deps: ## Fetch monitoring chart dependencies (kube-prometheus-stack)
@helm dependency build charts/monitoring > /dev/null 2>&1 || helm dependency update charts/monitoring > /dev/null
monitoring: ## Deploy Prometheus & Grafana
@echo "📊 Deploying monitoring stack..."
@helm dependency build charts/monitoring 2>/dev/null || true
@PROVIDER="generic"; \
if [ -f "$(DETECTED_VALUES)" ]; then \
PROVIDER=$$(grep '^# Provider:' $(DETECTED_VALUES) | awk '{print $$3}'); \
fi; \
MON_VALUES="charts/monitoring/values-generic.yaml"; \
if [ "$$PROVIDER" = "kind" ] && [ -f "charts/monitoring/values-kind.yaml" ]; then \
MON_VALUES="charts/monitoring/values-kind.yaml"; \
fi; \
echo " Provider: $$PROVIDER → $$MON_VALUES"; \
helm upgrade --install monitoring charts/monitoring \
--namespace kafka --create-namespace \
-f "$$MON_VALUES" \
--timeout 10m --wait
@echo "✅ Monitoring stack deployed"
monitoring-generic: ## Deploy Prometheus & Grafana (generic K8s overlay)
@echo "📊 Deploying monitoring stack (Generic)..."
helm dependency build charts/monitoring
helm upgrade --install monitoring charts/monitoring \
--namespace kafka --create-namespace \
-f charts/monitoring/values-generic.yaml \
--timeout 10m --wait
monitoring-undeploy: ## Remove the monitoring stack (keeps the namespace)
@echo "🗑️ Undeploying monitoring stack..."
helm uninstall monitoring -n kafka || true
@# Neither the namespace nor its PVCs are deleted: both are shared with Kafka
@# (data-0-krafter-*). Only kafka-undeploy removes broker volumes, by label.
cert-manager: ## Deploy cert-manager
@echo "🔐 Deploying cert-manager..."
./scripts/deploy-cert-manager.sh
kyverno: ## Deploy Kyverno policy engine
@echo "🛡️ Deploying Kyverno policy engine..."
helm repo add kyverno https://kyverno.github.io/kyverno/ 2>/dev/null || true
helm repo update kyverno 2>/dev/null || true
helm upgrade --install kyverno kyverno/kyverno \
-n kyverno --create-namespace \
--set admissionController.replicas=1 \
--timeout 5m --wait
@echo "✅ Kyverno deployed"
@kubectl get pods -n kyverno
kyverno-undeploy: ## Remove Kyverno
@echo "🗑️ Removing Kyverno..."
helm uninstall kyverno -n kyverno || true
kubectl delete namespace kyverno --ignore-not-found
@echo "✅ Kyverno removed"
# Deploy full stack (monitoring, Kafka, UI, Litmus) — without cluster/images
deploy-all: ## Deploy the full stack onto an existing cluster
@echo "🚀 Deploying full stack..."
$(MAKE) monitoring
./scripts/deploy-kafka-generic.sh --yes
./scripts/deploy-kafka-ui.sh
./scripts/deploy-apicurio.sh
./scripts/deploy-jaeger.sh
$(MAKE) litmus
./scripts/port-forward.sh
@echo "✅ Full stack deployed!"
# Deploy Kafka (shorthand for kafka-deploy)
kafka: kafka-deploy ## Deploy Kafka (shorthand for kafka-deploy)
# Deploy Kafka UI only (legacy script — applies raw manifests)
ui: ## Deploy Kafka UI (raw manifests)
@echo "🖥️ Deploying Kafka UI (raw manifests)..."
./scripts/deploy-kafka-ui.sh
ui-chart-lint: ## Lint the kafka-ui chart
@echo "🔍 Linting Kafka UI chart..."
helm lint $(UI_CHART_DIR)
@echo "✅ Kafka UI chart lint passed"
ui-chart-template: ## Render kafka-ui templates (ENV=...)
@echo "📄 Rendering Kafka UI templates (ENV=$(ENV))..."
@OVERLAY=""; \
if [ -f "$(UI_CHART_DIR)/values-$(ENV).yaml" ]; then \
OVERLAY="-f $(UI_CHART_DIR)/values-$(ENV).yaml"; \
fi; \
helm template kafka-ui $(UI_CHART_DIR) \
--namespace kafka $$OVERLAY
ui-deploy: ## Deploy Kafka UI via Helm (ENV=kind|dev|staging|prod)
@echo "🖥️ Deploying Kafka UI via Helm (ENV=$(ENV))..."
@OVERLAY=""; \
if [ -f "$(UI_CHART_DIR)/values-$(ENV).yaml" ]; then \
OVERLAY="-f $(UI_CHART_DIR)/values-$(ENV).yaml"; \
echo " Using overlay: values-$(ENV).yaml"; \
fi; \
helm upgrade --install kafka-ui $(UI_CHART_DIR) \
--namespace kafka --create-namespace \
$$OVERLAY \
--timeout 5m --wait
@echo "✅ Kafka UI deployed"
ui-upgrade: ## Upgrade Kafka UI Helm release (ENV=...)
@echo "🔄 Upgrading Kafka UI (ENV=$(ENV))..."
@OVERLAY=""; \
if [ -f "$(UI_CHART_DIR)/values-$(ENV).yaml" ]; then \
OVERLAY="-f $(UI_CHART_DIR)/values-$(ENV).yaml"; \
echo " Using overlay: values-$(ENV).yaml"; \
fi; \
helm upgrade kafka-ui $(UI_CHART_DIR) \
--namespace kafka --reuse-values \
$$OVERLAY \
--timeout 5m --wait
@echo "✅ Kafka UI upgraded"
ui-undeploy: ## Remove Kafka UI Helm release
@echo "🗑️ Removing Kafka UI..."
helm uninstall kafka-ui -n kafka 2>/dev/null || true
@echo "✅ Kafka UI removed"
# Deploy Apicurio Registry
apicurio: ## Deploy Apicurio Registry
@echo "📝 Deploying Apicurio Registry..."
./scripts/deploy-apicurio.sh
jaeger: ## Deploy Jaeger (distributed tracing)
@echo "🔍 Deploying Jaeger (distributed tracing)..."
./scripts/deploy-jaeger.sh
# ── Source test suites ───────────────────────────────────────────────────────
# Note: `make test` and friends below drive Kafka performance runs against a
# LIVE cluster — they are not the source test suite. These targets are.
##@ Tests
tests: test-unit ## Run every source test suite (Java + CLI)
@echo "✅ Source test suites passed."
test-unit: test-java test-cli ## Run unit tests only (Java + CLI, no Docker)
@echo "✅ Unit tests passed (Java + CLI)."
test-java: ## Run the Java unit tests
@echo "🧪 Running Java unit tests (kates/)..."
cd kates && ./mvnw test -B
# Testcontainers-backed ITs (engine lifecycle, outbox, persistence). Needs a
# working Docker daemon; skipped tests are worse than a clear failure here.
test-java-it: ## Run the Java integration tests (requires Docker)
@echo "🧪 Running Java integration tests (requires Docker)..."
cd kates && ./mvnw verify -B
test-cli: ## Run the Go CLI tests
@echo "🧪 Running Go CLI tests (cli/)..."
cd cli && go test ./... -timeout 300s
# Run Performance Test
test: ## Run baseline 1M-message perf test
@echo "🧪 Running Performance Test..."
$(KATES_BIN) test create --type LOAD --records 1000000 --wait
test-load: ## Run load test (concurrent producers)
@echo "🧪 Running Load Test..."
$(KATES_BIN) test create --type LOAD --wait
test-stress: ## Run stress test (ramp to breaking point)
@echo "🧪 Running Stress Test..."
$(KATES_BIN) test create --type STRESS --wait
test-spike: ## Run spike test (flash sale simulation)
@echo "🧪 Running Spike Test..."
$(KATES_BIN) test create --type SPIKE --wait
test-endurance: ## Run endurance/soak test (sustained load)
@echo "🧪 Running Endurance (Soak) Test..."
$(KATES_BIN) test create --type ENDURANCE --wait
test-volume: ## Run volume test (large data)
@echo "🧪 Running Volume Test..."
$(KATES_BIN) test create --type VOLUME --wait
test-capacity: ## Run capacity test (find max throughput)
@echo "🧪 Running Capacity Test..."
$(KATES_BIN) test create --type CAPACITY --wait
test-net-kafka: ## Test TCP connectivity to Kafka from default namespace
@domain=$$(source scripts/common.sh && get_cluster_domain); \
echo "🌐 Testing TCP connectivity to Kafka from 'default' namespace (Domain: $$domain)..."; \
kubectl run -i --tty --rm debug-nc --image=busybox:1.36 --namespace=default --restart=Never -- nc -vz krafter-kafka-bootstrap.kafka.svc.$$domain 9092
test-net-api: ## Test HTTP connectivity to Kates API from default namespace
@domain=$$(source scripts/common.sh && get_cluster_domain); \
echo "🌐 Testing HTTP connectivity to Kates API from 'default' namespace (Domain: $$domain)..."; \
kubectl run -i --tty --rm debug-curl --image=curlimages/curl:8.7.1 --namespace=default --restart=Never -- curl -sv http://kates.kafka.svc.$$domain:8080/api/health
test-net: test-net-kafka test-net-api ## Run cross-namespace network connectivity tests
@echo "✅ Cross-namespace network tests complete."
cluster-domain: ## Print the detected cluster DNS domain
@domain=$$(source scripts/common.sh && get_cluster_domain); \
echo "🌐 Cluster domain: $$domain"
# Kates CLI (standalone install)
##@ Kates CLI
cli-build: ## Cross-compile CLI (macOS + Linux)
@echo "🔨 Building Kates CLI locally..."
cd cli && go build -ldflags="-s -w" -o dist/kates .
@echo "🔨 Cross-compiling Kates CLI for all platforms..."
cd cli && bash build.sh
# Where the binary goes. Override when another kates wins on PATH:
# make cli-install CLI_INSTALL_DIR=/opt/homebrew/bin
CLI_INSTALL_DIR ?= /usr/local/bin
CLI_INSTALL_PATH := $(CLI_INSTALL_DIR)/kates
cli-install: ## Build and install CLI on this machine (CLI_INSTALL_DIR to choose where)
@echo "🔨 Building Kates CLI from source..."
@# The version is stamped in, as build.sh and `kates upgrade` do it.
@# Without it every locally installed binary reports "dev/unknown" and a
@# stale copy shadowing it on PATH is impossible to tell apart.
cd cli && go build -ldflags="-s -w \
-X github.com/bmscomp/kates/cli/cmd.Version=$$(git rev-parse --abbrev-ref HEAD)-$$(git rev-parse --short HEAD) \
-X github.com/bmscomp/kates/cli/cmd.Commit=$$(git rev-parse --short HEAD) \
-X github.com/bmscomp/kates/cli/cmd.BuildDate=$$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-o dist/kates .
@echo "📦 Installing to $(CLI_INSTALL_PATH)..."
@# Refuse to write through a symlink that leaves this directory: Homebrew
@# links /opt/homebrew/bin/<x> into its Cellar, and cp would overwrite the
@# formula's own binary — undone by its next upgrade, and invisible until.
@if [ -L "$(CLI_INSTALL_PATH)" ]; then \
target=$$(readlink -f "$(CLI_INSTALL_PATH)" 2>/dev/null || readlink "$(CLI_INSTALL_PATH)"); \
if [ "$$(dirname "$$target")" != "$(CLI_INSTALL_DIR)" ]; then \
echo "❌ $(CLI_INSTALL_PATH) is a symlink to $$target, owned by another package."; \
echo " Installing here would overwrite it. Choose a directory you own:"; \
echo " make cli-install CLI_INSTALL_DIR=\$$HOME/.local/bin"; \
case "$$target" in *"/Cellar/"*) echo " Or remove the formula first: brew uninstall kates";; esac; \
exit 1; \
fi; \
fi
sudo cp cli/dist/kates $(CLI_INSTALL_PATH)
@if [ "$$(uname -s)" = "Darwin" ]; then \
sudo xattr -dr com.apple.provenance $(CLI_INSTALL_PATH) 2>/dev/null || true; \
sudo xattr -dr com.apple.quarantine $(CLI_INSTALL_PATH) 2>/dev/null || true; \
sudo codesign -f -s - $(CLI_INSTALL_PATH); \
fi
@echo "✅ Installed:"
@# Run the file just written, by absolute path. Asking `kates` would
@# report whatever PATH resolves to, which is exactly the thing this
@# step has to verify rather than assume.
@$(CLI_INSTALL_PATH) version 2>/dev/null | sed 's/^/ /' || true
@# Does the shell agree? A kates earlier in PATH (Homebrew's bin comes
@# before /usr/local/bin on Apple Silicon; ~/go/bin and ~/.local/bin are
@# common too) keeps winning silently, and every new command looks missing.
@found=$$(command -v kates 2>/dev/null || true); \
if [ -z "$$found" ]; then \
echo "⚠️ $(CLI_INSTALL_DIR) is not on your PATH — typing 'kates' will not find it."; \
echo " export PATH=\"$(CLI_INSTALL_DIR):\$$PATH\""; \
elif [ "$$(readlink -f "$$found" 2>/dev/null || echo "$$found")" != "$$(readlink -f $(CLI_INSTALL_PATH) 2>/dev/null || echo $(CLI_INSTALL_PATH))" ]; then \
echo "⚠️ 'kates' still runs $$found, not the binary just installed."; \
echo " It reports: $$($$found version 2>/dev/null | grep -i 'kates cli' | awk '{print $$NF}' || echo unknown)"; \
echo " Fix with one of:"; \
echo " make cli-install CLI_INSTALL_DIR=$$(dirname "$$found")"; \
echo " rm $$found"; \
echo " export PATH=\"$(CLI_INSTALL_DIR):\$$PATH\""; \
echo " then: hash -r"; \
fi
cli-clean: ## Remove CLI build artifacts
@echo "🧹 Removing CLI build artifacts..."
rm -rf cli/dist
editors-sync: ## Sync canonical JSON schemas and TextMate grammar into editor extensions
@./scripts/package-editors.sh --sync
editors-check: ## Verify schema parity, editor manifests, and VS Code TypeScript build
@./scripts/package-editors.sh --check
editors-package: ## Package VS Code VSIX and Visual Studio extension bundle into dist/
@./scripts/package-editors.sh --package $(KATES_APP_VERSION)
# Kates Application (Docker + Kind)
##@ Kates Application (Docker + Kind)
kates: kates-build kates-deploy ## Build + deploy Kates (full pipeline)
@echo "✅ Kates deployed! Run 'make ports' to access at http://localhost:30083"
kates-build: ## Build Kates JVM image and load into Kind
@if docker image inspect kates:latest >/dev/null 2>&1; then \
echo "✅ Kates image already exists locally (kates:latest)."; \
elif docker pull ghcr.io/bmscomp/kates:$(KATES_APP_VERSION); then \
echo "✅ Pulled Kates image from registry."; \
docker tag ghcr.io/bmscomp/kates:$(KATES_APP_VERSION) kates:latest; \
else \
echo "🔨 Building Kates (JVM + CLI) from source..."; \
cd kates && ./mvnw package -DskipTests -B && \
cd .. && docker build -f kates/Dockerfile -t kates:latest .; \
fi
kind load docker-image kates:latest --name $(CLUSTER_NAME)
@echo "✅ Kates image loaded into Kind"
@echo "ℹ️ This target prefers a cached or published image. To guarantee your"
@echo " working tree is what runs, use 'make kates-local' instead."
# ── Local-only image: always built from the working tree ─────────────────────
#
# Deliberately NOT `kates-build`. That target prefers an existing local tag,
# then a published image, and only builds as a last resort — so a source fix
# can silently never run, and you end up debugging the registry's build while
# reading your own diff. Everything below always compiles what is on disk,
# tags it kates:local, and pins the deployment to it with pullPolicy: Never so
# the kubelet cannot substitute a registry image on any later restart.
kates-image-local: ## Build kates:local from the working tree (JVM)
@echo "🔨 Building kates:local from the working tree (no pull, no cache reuse)..."
docker build -f kates/Dockerfile -t kates:local .
@echo "📦 Loading kates:local into Kind cluster '$(CLUSTER_NAME)'..."
kind load docker-image kates:local --name $(CLUSTER_NAME)
@echo "✅ kates:local is on the node. Digest:"
@docker image inspect kates:local --format ' {{.Id}} ({{.Created}})'
kates-local: kates-image-local ## Deploy kates:local, pinned with pullPolicy: Never
@echo "🚀 Deploying kates:local (namespace: $(KATES_NS))..."
@helm upgrade --install $(KATES_RELEASE) $(CHART_DIR) \
-n $(KATES_NS) --create-namespace \
-f $(CHART_DIR)/values-local.yaml \
--timeout 8m \
|| { \
echo ""; \
echo "❌ helm upgrade failed. If it complained that 'updates to statefulset"; \
echo " spec ... are forbidden', an immutable field on kates-postgresql"; \
echo " changed (volumeClaimTemplates, serviceName, selector). Recreate the"; \
echo " StatefulSet without touching the data, then retry:"; \
echo ""; \
echo " make kates-local-recreate-db"; \
echo ""; \
exit 1; \
}
@echo "⏳ Waiting for rollout..."
kubectl rollout status deployment/$(KATES_RELEASE) -n $(KATES_NS) --timeout=300s
@echo "✅ kates:local running. Verify the pod is on YOUR image:"
@kubectl get pod -n $(KATES_NS) -l app.kubernetes.io/instance=$(KATES_RELEASE) \
-o jsonpath='{range .items[*]} {.metadata.name} {.spec.containers[0].image} {.spec.containers[0].imagePullPolicy}{"\n"}{end}'
# Force a fresh pod even when the tag is unchanged: Kubernetes sees no spec
# change for the same tag, so a plain `helm upgrade` would keep the old pod
# (running the old bytes) very much alive.
kates-local-restart: kates-image-local ## Rebuild kates:local and restart the running pod
kubectl rollout restart deployment/$(KATES_RELEASE) -n $(KATES_NS)
kubectl rollout status deployment/$(KATES_RELEASE) -n $(KATES_NS) --timeout=300s
# Escape hatch for immutable-field conflicts on the bundled PostgreSQL.
# --cascade=orphan is the whole point: it removes only the StatefulSet object,
# leaving the running pod AND the PVC in place, so Helm can recreate the spec
# and adopt them. The database survives. A plain delete would take the pod and
# (depending on the retention policy) the volume with it.
kates-local-recreate-db: ## Recreate the PostgreSQL StatefulSet, keeping pod + PVC
@echo "🗄 Recreating the $(KATES_RELEASE)-postgresql StatefulSet, keeping pod + PVC..."
kubectl delete statefulset $(KATES_RELEASE)-postgresql -n $(KATES_NS) \
--cascade=orphan --ignore-not-found
@echo "✅ StatefulSet removed (data intact). Re-run: make kates-local"
# Same pull-then-build fallback as kates-build, and the same reason for reading
# the tag from Chart.yaml: this was pinned to 1.16.0-native long after appVersion
# moved on, so "pulled the image" quietly meant "pulled a stale one".
kates-native: ## Build Kates native image and load into Kind
@if docker image inspect kates:native >/dev/null 2>&1; then \
echo "✅ Kates native image already exists locally (kates:native)."; \
elif docker pull ghcr.io/bmscomp/kates:$(KATES_APP_VERSION)-native; then \
echo "✅ Pulled Kates native image from registry."; \
docker tag ghcr.io/bmscomp/kates:$(KATES_APP_VERSION)-native kates:native; \
else \
echo "🔨 Building Kates (native) from source (needs ~8GB RAM for the compiler)..."; \
docker build -f kates/Dockerfile.native -t kates:native .; \
fi
kind load docker-image kates:native --name $(CLUSTER_NAME)
@echo "✅ Kates native image loaded into Kind"
# Native image built from the working tree, never pulled — the native
# counterpart of kates-image-local. Use it to verify that a change which works
# on the JVM also survives ahead-of-time compilation, before a release tag finds
# out for you.
kates-image-native-local: ## Build kates:native-local from the working tree
@echo "🔨 Building kates:native-local from the working tree..."
@echo " The GraalVM compiler needs ~8GB of memory; on Docker Desktop raise"
@echo " the VM memory limit first or the build dies with an opaque OOM."
docker build -f kates/Dockerfile.native -t kates:native-local .
@echo "📦 Loading kates:native-local into Kind cluster '$(CLUSTER_NAME)'..."
kind load docker-image kates:native-local --name $(CLUSTER_NAME)
@echo "✅ kates:native-local is on the node. Digest:"
@docker image inspect kates:native-local --format ' {{.Id}} ({{.Created}})'
# Smoke-test a locally built native image without a cluster: boots it against
# throwaway Postgres and asserts the endpoints that AOT compilation most often
# breaks — health, OpenAPI, and the playbook catalog, which is loaded from
# classpath YAML and silently empties out if the resource pattern is wrong.
kates-native-smoke: ## Smoke-test a native image (IMAGE=... to pick one)
@./scripts/native-smoke-test.sh $(if $(IMAGE),$(IMAGE),kates:native-local)
# Deploy the locally built NATIVE image, pinned so the kubelet cannot silently
# substitute the published one — the native counterpart of kates-local.
kates-native-local: kates-image-native-local ## Deploy kates:native-local, pinned with pullPolicy Never
@echo "🚀 Deploying kates:native-local (namespace: $(KATES_NS))..."
@# The tag never changes, so without the image-id annotation the manifest is
@# identical between builds, nothing rolls, and the old pod keeps serving the
@# old binary — the same trap kates-local-restart exists for.
@helm upgrade --install $(KATES_RELEASE) $(CHART_DIR) \
-n $(KATES_NS) --create-namespace \
-f $(CHART_DIR)/values-native-local.yaml \
--set-string podAnnotations.kates-image-id="$$(docker image inspect --format '{{.Id}}' kates:native-local)" \
--timeout 8m
@echo "⏳ Waiting for rollout..."
kubectl rollout status deployment/$(KATES_RELEASE) -n $(KATES_NS) --timeout=300s
@echo "✅ Running. Verify the pod is on YOUR image:"
@kubectl get pod -n $(KATES_NS) -l app.kubernetes.io/instance=$(KATES_RELEASE) \
-o jsonpath='{range .items[*]} {.metadata.name} {.spec.containers[0].image} {.spec.containers[0].imagePullPolicy}{"\n"}{end}'
# The whole loop in one command: CLI + native image from the working tree,
# Kafka, the chart pinned to that image, chart tests, the endpoints AOT
# compilation breaks, and a real benchmark driven through the CLI.
native-e2e: ## Build CLI + native image, deploy and test the whole stack
@./scripts/native-e2e.sh $(NATIVE_E2E_ARGS)
tester-build: ## Build Kates Tester image and load into Kind
@echo "🔨 Building Kates Tester image..."
docker build -f tester/Dockerfile -t kates-tester:latest tester/
kind load docker-image kates-tester:latest --name $(CLUSTER_NAME) 2>/dev/null || true
@echo "✅ Kates Tester image built and available"
connect-build: ## Build the Kafka Connect image with its open-source connector plugins
@echo "🔌 Building Kafka Connect image with its open-source connector plugins..."
@DBZ_VERSION=$$(grep '^ARG DEBEZIUM_VERSION=' Dockerfile.connect | head -n1 | cut -d= -f2); \
TAG=$${DBZ_VERSION%.Final}; \
KAFKA=$$(grep -E '^kafkaVersion:' charts/kafka-cluster/values.yaml | head -n1 | sed -E 's/.*"([^"]+)".*/\1/'); \
QUALIFIED=ghcr.io/bmscomp/connect:$${TAG}-kafka-$${KAFKA}; \
echo " Debezium: $${DBZ_VERSION} → connect:$${TAG} ($${QUALIFIED})"; \
docker build -t connect:$${TAG} -t connect:latest \
-t ghcr.io/bmscomp/connect:$${TAG} \
-t ghcr.io/bmscomp/connect:latest \
-t $${QUALIFIED} \
-f Dockerfile.connect . && \
if kind get clusters 2>/dev/null | grep -q "$(CLUSTER_NAME)"; then \
echo "Loading into Kind cluster ($(CLUSTER_NAME))..."; \
kind load docker-image connect:$${TAG} --name $(CLUSTER_NAME); \
kind load docker-image $${QUALIFIED} --name $(CLUSTER_NAME); \
fi && \
echo "✅ connect:$${TAG} built successfully" && \
echo " Also tagged and loaded as $${QUALIFIED} — the tag charts/connect-cluster asks for." && \
echo " Plugins: debezium-postgres, debezium-mysql, debezium-mongodb, debezium-sqlserver, apicurio-converter, debezium-jdbc, debezium-scripting, aiven-jdbc, aiven-s3-sink, aiven-s3-source"
test-full: ## Run every local check: guards, charts, unit tests, images, Connect and Kates smoke tests
scripts/full-local-test.sh $(PHASES)
connect-smoke-test: ## Boot the Connect image against throwaway Kafka + S3 and verify it
@DBZ_VERSION=$$(grep '^ARG DEBEZIUM_VERSION=' Dockerfile.connect | head -n1 | cut -d= -f2); \
TAG=$${DBZ_VERSION%.Final}; \
scripts/connect-smoke-test.sh connect:$${TAG}
connect-push: ## Push the Kafka Connect image to $(REGISTRY)
@echo "🚀 Pushing Kafka Connect image to $(REGISTRY)..."
@DBZ_VERSION=$$(grep '^ARG DEBEZIUM_VERSION=' Dockerfile.connect | head -n1 | cut -d= -f2); \
TAG=$${DBZ_VERSION%.Final}; \
docker push $(REGISTRY)/connect:$${TAG} && \
docker push $(REGISTRY)/connect:latest && \
echo "✅ Pushed: $(REGISTRY)/connect:$${TAG}"
push-images: ## Push kates and tester images to remote registry
@echo "🚀 Pushing images to $(REGISTRY)..."
docker tag kates:latest $(REGISTRY)/kates:latest
docker push $(REGISTRY)/kates:latest
docker tag kates-tester:latest $(REGISTRY)/kates-tester:latest
docker push $(REGISTRY)/kates-tester:latest
@echo "✅ Images pushed successfully to $(REGISTRY)!"
kates-deploy: ## Apply Kates K8s manifests
@echo "🚀 Deploying Kates to Kubernetes..."
kubectl apply -f kates/k8s/namespace.yaml
kubectl apply -f kates/k8s/rbac.yaml
kubectl apply -f kates/k8s/configmap.yaml
@echo "Copying Kafka SASL credentials to kates namespace..."
@kubectl get secret kates-backend -n kafka -o json \
| jq 'del(.metadata.namespace,.metadata.resourceVersion,.metadata.uid,.metadata.creationTimestamp,.metadata.annotations,.metadata.labels,.metadata.managedFields,.metadata.ownerReferences)' \
| kubectl apply -n kafka -f -
kubectl apply -f kates/k8s/postgres.yaml
@echo "Waiting for PostgreSQL to be ready..."
@kubectl wait --for=condition=Ready pod -l app=postgres -n kafka --timeout=120s
kubectl apply -f kates/k8s/deployment.yaml
kubectl apply -f kates/k8s/service.yaml
kubectl rollout status deployment/kates -n kafka --timeout=300s
@echo "✅ Kates is running"
@echo "Configuring local Kates CLI context..."
@if command -v kates >/dev/null 2>&1; then \
kates ctx set local --url "http://localhost:30083" --api-key "changeme" >/dev/null 2>&1 || true; \
kates ctx use local >/dev/null 2>&1 || true; \
echo "✅ Kates CLI context 'local' configured automatically!"; \
elif [ -x "cli/dist/kates" ]; then \
cli/dist/kates ctx set local --url "http://localhost:30083" --api-key "changeme" >/dev/null 2>&1 || true; \
cli/dist/kates ctx use local >/dev/null 2>&1 || true; \
echo "✅ Kates CLI context 'local' configured automatically!"; \
else \
echo "⚠️ Kates CLI not found. To configure manually run:"; \
echo " kates ctx set local --url http://localhost:30083 --api-key changeme"; \
fi
kates-redeploy: ## Restart Kates deployment
@echo "🔄 Redeploying Kates..."
kubectl rollout restart deployment/kates -n kafka
kubectl rollout status deployment/kates -n kafka --timeout=300s
kates-secret: ## Create the Kafka SASL credentials secret for Kates
@echo "🔐 Setting up Kafka SASL credentials in kates namespace..."
@./scripts/ensure-kafka-user.sh || true
@if kubectl get secret kates-backend -n kafka >/dev/null 2>&1; then \
echo "Copying from kafka namespace..."; \
kubectl get secret kates-backend -n kafka -o json \
| jq 'del(.metadata.namespace,.metadata.resourceVersion,.metadata.uid,.metadata.creationTimestamp,.metadata.annotations,.metadata.labels,.metadata.managedFields,.metadata.ownerReferences)' \
| kubectl apply -n $(KATES_NS) -f -; \
echo "✅ Secret copied successfully"; \
else \
PWD="$(PASSWORD)"; \
if [ -z "$$PWD" ]; then \
PWD="changeme"; \
echo "⚠️ No password provided. Using default password 'changeme'."; \
echo " To set a custom password, use: make kates-secret PASSWORD=your_password"; \
fi; \
echo "Creating secret manually..."; \
kubectl create secret generic kates-backend -n $(KATES_NS) --from-literal=password="$$PWD" --dry-run=client -o yaml | kubectl apply -f -; \
echo "✅ Secret created successfully"; \
fi
kates-logs: ## Stream Kates logs
@echo "📋 Streaming Kates logs..."
kubectl logs -f -l app=kates -n kafka
kates-undeploy: ## Remove Kates namespace
@echo "🗑️ Removing Kates..."
helm uninstall $(KATES_RELEASE) -n $(KATES_NS) --ignore-not-found || true
@# The namespace itself is left in place on purpose — it holds the database
@# PVC, and dropping it silently destroys every stored run. Delete it by hand
@# when that is what you actually want:
@# kubectl delete namespace $(KATES_NS)
@echo "✅ Kates removed (namespace $(KATES_NS) and its data kept)"
##@ Kates Application (Helm chart)
kates-helm: kates-helm-deploy ## Deploy via Helm (shorthand)
kates-helm-deploy: ## Deploy via Helm (ENV=kind|dev|staging|prod)
@echo "📦 Deploying Kates via Helm (ENV=$(ENV))..."
ENV=$(ENV) ./scripts/deploy-kates.sh
kates-helm-upgrade: ## Upgrade existing release (ENV=...)
@echo "🔄 Upgrading Kates via Helm (ENV=$(ENV))..."
ENV=$(ENV) ./scripts/deploy-kates.sh
kates-generic: ## Deploy Kates via Helm (generic Kubernetes overlay)
@echo "📦 Deploying Kates via Helm (generic Kubernetes)..."
ENV=generic ./scripts/deploy-kates.sh
kates-prod: ## Deploy Kates via Helm (production overlay)
@echo "📦 Deploying Kates via Helm (production)..."
ENV=prod ./scripts/deploy-kates.sh
kates-helm-undeploy: ## Remove Kates Helm release
@echo "🗑️ Removing Kates (Helm release)..."
helm uninstall kates -n $(KATES_NS) 2>/dev/null || true
@# The namespace itself is left in place on purpose — it holds the database
@# PVC, and dropping it silently destroys every stored run. Delete it by hand
@# when that is what you actually want:
@# kubectl delete namespace $(KATES_NS)
@echo "✅ Kates Helm release removed (namespace $(KATES_NS) and its data kept)"
##@ Helm Charts
chart-lint: ## Lint the Helm chart
@echo "🔍 Linting Kates chart..."
helm lint $(CHART_DIR) --strict
@if command -v ct >/dev/null 2>&1; then \
ct lint --config ct.yaml --charts $(CHART_DIR); \
else \
echo "⚠️ chart-testing (ct) not found — skipping ct lint"; \
fi
@echo "✅ Chart lint passed"
readme-check: ## Verify README chart table matches Chart.yaml sources
@echo "🔍 Checking README chart table against charts/*/Chart.yaml..."
@./scripts/gen-chart-table.sh --check
chart-package: ## Package the Helm chart
@echo "📦 Packaging Kates chart v$(CHART_VERSION)..."
helm package $(CHART_DIR) --destination .build/
@echo "✅ Chart packaged: .build/kates-$(CHART_VERSION).tgz"
# `update`, not `build`: a Chart.lock left by kafka-cluster 0.4 (seaweedfs
# only) makes `build` refuse the kafka-common dependency 1.0 added.
kafka-chart-deps: ## Fetch kafka-cluster chart dependencies
@helm dependency build $(KAFKA_CHART_DIR) > /dev/null 2>&1 || helm dependency update $(KAFKA_CHART_DIR)
kafka-chart-lint: kafka-chart-deps ## Lint the kafka-cluster chart (all environments)
@echo "🔍 Linting kafka-cluster chart (all environments)..."
helm lint $(KAFKA_CHART_DIR)
helm lint $(KAFKA_CHART_DIR) -f $(KAFKA_CHART_DIR)/values-dev.yaml
helm lint $(KAFKA_CHART_DIR) -f $(KAFKA_CHART_DIR)/values-staging.yaml
helm lint $(KAFKA_CHART_DIR) -f $(KAFKA_CHART_DIR)/values-prod.yaml
helm lint $(KAFKA_CHART_DIR) -f $(KAFKA_CHART_DIR)/values-platform.yaml -f $(KAFKA_CHART_DIR)/values-prod.yaml
helm lint $(KAFKA_CHART_DIR) -f $(KAFKA_CHART_DIR)/values-dev.yaml -f $(KAFKA_CHART_DIR)/values-kind.yaml
helm lint $(KAFKA_CHART_DIR) -f $(KAFKA_CHART_DIR)/values-ci.yaml -f $(KAFKA_CHART_DIR)/values-additional.yaml
@echo "✅ Kafka chart lint passed"
kafka-chart-unittest: kafka-chart-deps ## Run the kafka-cluster helm-unittest suites
@helm plugin list | grep -q unittest || { echo "❌ helm-unittest is not installed: helm plugin install https://github.com/helm-unittest/helm-unittest --version $(HELM_UNITTEST_VERSION)"; exit 1; }
helm unittest $(KAFKA_CHART_DIR)
kafka-chart-template: kafka-chart-deps ## Render kafka-cluster templates into .build/
@mkdir -p .build
helm template kafka-cluster $(KAFKA_CHART_DIR) \
--namespace kafka \
-f $(KAFKA_CHART_DIR)/values-platform.yaml \
> .build/kafka-rendered.yaml
@echo "Rendered $$(grep -c '^kind:' .build/kafka-rendered.yaml) resources → .build/kafka-rendered.yaml"
kafka-chart-package: kafka-chart-deps ## Package the kafka-cluster chart into .build/
@mkdir -p .build
helm package $(KAFKA_CHART_DIR) --destination .build/
@echo "✅ Kafka chart packaged: .build/kafka-cluster-$(KAFKA_CHART_VERSION).tgz"
##@ Helm Test Suite
kafka-chart-test: ## Run Helm tests for Kafka cluster (KAFKA_RELEASE=... KAFKA_NAMESPACE=...)
@echo ""
@echo "╭────────────────────────────────────────────────────────────────╮"
@echo "│ 🧪 Helm Test · Kafka Cluster │"
@echo "╰────────────────────────────────────────────────────────────────╯"
@echo ""
@KAFKA_RELEASE=$${KAFKA_RELEASE:-kafka-cluster}; \
KAFKA_NAMESPACE=$${KAFKA_NAMESPACE:-kafka}; \
TIMEOUT=$${TIMEOUT:-180s}; \
echo " Release: $$KAFKA_RELEASE"; \
echo " Namespace: $$KAFKA_NAMESPACE"; \
echo " Timeout: $$TIMEOUT"; \
echo ""; \
helm test $$KAFKA_RELEASE --namespace $$KAFKA_NAMESPACE --timeout $$TIMEOUT --logs 2>&1; \
EXIT=$$?; \
echo ""; \
if [ $$EXIT -eq 0 ]; then \
echo " ✅ Kafka cluster tests passed"; \
else \
echo " ❌ Kafka cluster tests failed (exit $$EXIT)"; \
echo ""; \
echo " Diagnostics:"; \
echo " kubectl get pods -n $$KAFKA_NAMESPACE -l helm.sh/hook=test --no-headers"; \
kubectl get pods -n $$KAFKA_NAMESPACE -l helm.sh/hook=test --no-headers 2>/dev/null | sed 's/^/ /'; \
echo ""; \
echo " View failed pod logs:"; \
for POD in $$(kubectl get pods -n $$KAFKA_NAMESPACE -l helm.sh/hook=test --field-selector=status.phase=Failed -o name 2>/dev/null); do \
echo " kubectl logs $$POD -n $$KAFKA_NAMESPACE"; \
done; \
fi; \
exit $$EXIT
kates-helm-test: ## Run Helm tests for Kates API (KATES_RELEASE=... KATES_NAMESPACE=...)
@echo ""
@echo "╭────────────────────────────────────────────────────────────────╮"
@echo "│ 🧪 Helm Test · Kates API │"
@echo "╰────────────────────────────────────────────────────────────────╯"
@echo ""
@KATES_RELEASE=$${KATES_RELEASE:-kates}; \
KATES_NAMESPACE=$${KATES_NAMESPACE:-$(KATES_NS)}; \
TIMEOUT=$${TIMEOUT:-120s}; \
echo " Release: $$KATES_RELEASE"; \
echo " Namespace: $$KATES_NAMESPACE"; \
echo " Timeout: $$TIMEOUT"; \
echo ""; \
helm test $$KATES_RELEASE --namespace $$KATES_NAMESPACE --timeout $$TIMEOUT --logs 2>&1; \
EXIT=$$?; \
echo ""; \
if [ $$EXIT -eq 0 ]; then \
echo " ✅ Kates API tests passed"; \
else \
echo " ❌ Kates API tests failed (exit $$EXIT)"; \
echo ""; \
echo " Diagnostics:"; \
kubectl get pods -n $$KATES_NAMESPACE -l helm.sh/hook=test --no-headers 2>/dev/null | sed 's/^/ /'; \
fi; \
exit $$EXIT
chaos-helm-test: ## Run Helm tests for Chaos stack (CHAOS_RELEASE=... CHAOS_NAMESPACE=...)
@echo ""
@echo "╭────────────────────────────────────────────────────────────────╮"
@echo "│ 🧪 Helm Test · Chaos (LitmusChaos) │"
@echo "╰────────────────────────────────────────────────────────────────╯"
@echo ""
@CHAOS_RELEASE=$${CHAOS_RELEASE:-chaos}; \
CHAOS_NAMESPACE=$${CHAOS_NAMESPACE:-kafka}; \
TIMEOUT=$${TIMEOUT:-120s}; \
echo " Release: $$CHAOS_RELEASE"; \
echo " Namespace: $$CHAOS_NAMESPACE"; \
echo " Timeout: $$TIMEOUT"; \
echo ""; \
helm test $$CHAOS_RELEASE --namespace $$CHAOS_NAMESPACE --timeout $$TIMEOUT --logs 2>&1; \
EXIT=$$?; \
echo ""; \
if [ $$EXIT -eq 0 ]; then \
echo " ✅ Chaos tests passed"; \
else \
echo " ❌ Chaos tests failed (exit $$EXIT)"; \
fi; \
exit $$EXIT
# ── Unified Helm Test Suite ──────────────────────────────────────────────────
# Run all Helm tests across every component, with a final summary.
# Configurable via environment variables:
# KAFKA_RELEASE Kafka Helm release name (default: kafka-cluster)
# KAFKA_NAMESPACE Kafka namespace (default: kafka)
# KATES_RELEASE Kates Helm release name (default: kates)
# KATES_NAMESPACE Kates namespace (default: kafka)
# CHAOS_RELEASE Chaos Helm release name (default: chaos)
# CHAOS_NAMESPACE Chaos namespace (default: kafka)
# TIMEOUT Helm test timeout per suite (default: 180s)
# SKIP_CHAOS Set to 1 to skip chaos tests (default: 0)
# SKIP_KATES Set to 1 to skip kates tests (default: 0)
helm-test-all: ## Run all Helm tests across all components with summary
@echo ""
@echo "╔════════════════════════════════════════════════════════════════╗"
@echo "║ 🧪 Helm Test Suite · All Components ║"
@echo "╚════════════════════════════════════════════════════════════════╝"
@echo ""
@TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0; \
KAFKA_RELEASE=$${KAFKA_RELEASE:-kafka-cluster}; \
KAFKA_NAMESPACE=$${KAFKA_NAMESPACE:-kafka}; \
KATES_RELEASE=$${KATES_RELEASE:-kates}; \
KATES_NAMESPACE=$${KATES_NAMESPACE:-$(KATES_NS)}; \
CHAOS_RELEASE=$${CHAOS_RELEASE:-chaos}; \
CHAOS_NAMESPACE=$${CHAOS_NAMESPACE:-kafka}; \
TIMEOUT=$${TIMEOUT:-180s}; \
SKIP_CHAOS=$${SKIP_CHAOS:-0}; \
SKIP_KATES=$${SKIP_KATES:-0}; \
echo " Configuration:"; \
echo " Kafka: $$KAFKA_RELEASE (ns: $$KAFKA_NAMESPACE)"; \
echo " Kates: $$KATES_RELEASE (ns: $$KATES_NAMESPACE)"; \
echo " Chaos: $$CHAOS_RELEASE (ns: $$CHAOS_NAMESPACE)"; \
echo " Timeout: $$TIMEOUT"; \
echo ""; \
echo "──────────────────────────────────────────────────────────────────"; \
echo " ▸ Kafka Cluster"; \
TOTAL=$$((TOTAL+1)); \
if helm status $$KAFKA_RELEASE -n $$KAFKA_NAMESPACE >/dev/null 2>&1; then \
if helm test $$KAFKA_RELEASE -n $$KAFKA_NAMESPACE --timeout $$TIMEOUT --logs 2>&1; then \
PASSED=$$((PASSED+1)); \
echo " ✅ Kafka: PASSED"; \
else \