-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathMakefile
More file actions
958 lines (842 loc) · 49.1 KB
/
Copy pathMakefile
File metadata and controls
958 lines (842 loc) · 49.1 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
.DEFAULT_GOAL := help
# Possible envs are gcp, e2e-gcp, kind, e2e-kind
# Default to gcp
HELMFILE_ENV ?= gcp
ifeq ($(findstring gcp,$(HELMFILE_ENV)),)
-include env.kind
else
ifeq ($(HELMFILE_ENV),gcp)
-include generated-values-from-terraform/oidc.env
endif
-include env.gcp
endif
# Only the test environments default to the namespace-local mock issuer. New
# environments fail safe by defaulting to external until explicitly classified.
# ?= preserves explicit CLI and shell-environment overrides.
MOCK_OIDC_ENVIRONMENTS := kind e2e-kind e2e-gcp
ifneq ($(filter $(HELMFILE_ENV),$(MOCK_OIDC_ENVIRONMENTS)),)
OIDC_ISSUER_MODE ?= mock
else
OIDC_ISSUER_MODE ?= external
endif
export
GIT_SHA ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
TF_ENV ?= dev
TF_BACKEND ?= envs/gke/$(TF_ENV).tfbackend
TF_VARS ?= envs/gke/$(TF_ENV).tfvars
DRY_RUN ?=
AUTO_APPROVE ?=
# Derived flags from boolean variables (only true/1 are treated as truthy)
TRUTHY_VALUES := true 1
DRY_RUN_FLAG := $(if $(filter $(TRUTHY_VALUES),$(strip $(DRY_RUN))),--dry-run)
AUTO_APPROVE_FLAG := $(if $(filter $(TRUTHY_VALUES),$(strip $(AUTO_APPROVE))),-auto-approve)
BUILD_IMAGES_ENABLED := $(if $(filter $(TRUTHY_VALUES),$(strip $(BUILD_IMAGES))),1,)
# Default Dirs
MANIFESTS_DIR ?= manifests
HELM_DIR ?= helm
TF_DIR ?= terraform
OCI_TF_DIR ?= terraform/oci
GENERATED_RABBITMQ_DIR ?= generated-values-rabbitmq
GENERATED_DIR ?= generated-values-from-terraform
RABBITMQ_URL ?= "amqp://guest:guest@rabbitmq:5672"
MAESTRO_CONSUMER ?= cluster1
MAESTRO_NAMESPACE ?= maestro
KUBECONFIG ?= $(HOME)/.kube/config
# Human token helper defaults. TOKEN_TENANT is intentionally empty so callers
# must choose the active tenant value rather than accidentally minting a token
# for a shared test tenant.
TOKEN_SUBJECT ?= human@example.com
TOKEN_TENANT ?=
TOKEN_SUBTENANT ?=
TOKEN_MISSING_REQUIRED ?= false
CURL_IMAGE ?= curlimages/curl:8.22.0@sha256:58adaa4e8dca9c988bae2aba4ab3434a0bb2da16bbe3f92dec39ec7785166777
# Authorino operator (Kuadrant). Cluster-singleton prerequisite for the gateway
# ext_authz auth boundary; installs AuthConfig / Authorino CRDs when
# EXT_AUTHZ_ENABLED=true. Pinned by commit (not the mutable version tag) and
# checksum-verified before being applied. Bumping AUTHORINO_OPERATOR_VERSION
# requires updating COMMIT and SHA256 together.
AUTHORINO_OPERATOR_VERSION ?= v0.26.0
AUTHORINO_OPERATOR_COMMIT ?= 852f24e703c4a21ade6400e8fec7248e2dd562f6
AUTHORINO_OPERATOR_NAMESPACE ?= authorino-operator
AUTHORINO_OPERATOR_MANIFEST ?= https://raw.githubusercontent.com/Kuadrant/authorino-operator/$(AUTHORINO_OPERATOR_COMMIT)/config/deploy/manifests.yaml
AUTHORINO_OPERATOR_MANIFEST_SHA256 ?= ce2bef459d1456cbe462754cad571f87150fc1ad8bee4f1d010eb0db5b0aabdd
LIFECYCLE_DIR ?= functions/lifecycle-enforcer
OCI_SWEEP_DIR ?= functions/oci-ci-sweep
CLEANER_NAMESPACE ?= $(NAMESPACE)
CLEANER_SCHEDULE ?= 0 * * * *
CLEANER_LABEL_SELECTOR ?= hyperfleet.io/cluster-id hyperfleet.io/test-run e2e/hyperfleet.io/run-id
CLEANER_AGE_MINUTES ?= 180
CLEANER_MAESTRO_URL ?= http://maestro.$(MAESTRO_NAMESPACE).svc.cluster.local:8000
# ==== Terraform Targets ====
.PHONY: install-terraform
install-terraform: check-terraform check-tf-files ## Run Terraform init and apply
cd $(TF_DIR) && terraform init -backend-config=$(TF_BACKEND)
cd $(TF_DIR) && terraform apply -var-file=$(TF_VARS) $(AUTO_APPROVE_FLAG) -lock=false
.PHONY: plan-terraform
plan-terraform: check-terraform check-tf-files ## Run terraform plan (preview only, no apply)
cd $(TF_DIR) && terraform init -backend-config=$(TF_BACKEND)
cd $(TF_DIR) && terraform plan -var-file=$(TF_VARS)
.PHONY: destroy-terraform
destroy-terraform: check-terraform check-tf-files ## Destroy Terraform-managed infrastructure
cd $(TF_DIR) && terraform init -backend-config=$(TF_BACKEND)
# Always use -auto-approve to prevent CI cleanup from hanging on interactive prompt
cd $(TF_DIR) && terraform destroy -var-file=$(TF_VARS) -auto-approve -lock=false
.PHONY: get-credentials
get-credentials: check-terraform ## Configure kubectl credentials from Terraform outputs
@echo "Fetching cluster credentials..."
@eval $$(cd $(TF_DIR) && terraform output -raw connect_command)
@echo "OK: kubectl configured"
# ==== Kind Targets ====
KIND_CONFIG ?= scripts/kind-config.yaml
# kind's default CNI (kindnet) has no NetworkPolicy enforcement, so it's
# disabled (see scripts/kind-config.yaml) and install-kind-cilium installs
# Cilium as the sole CNI, providing both pod networking and policy enforcement.
# GKE gets equivalent enforcement via Dataplane V2 (see terraform/modules/cluster/gke),
# though its managed Cilium build may differ in version/config from this pinned chart.
CILIUM_VERSION ?= 1.20.1
CILIUM_NAMESPACE ?= kube-system
.PHONY: create-kind-cluster
create-kind-cluster: check-kind ## Create a new kind cluster or export kubeconfig if exists
@test -n "$(KIND_CLUSTER_NAME)" || { echo "ERROR: KIND_CLUSTER_NAME is empty. HELMFILE_ENV=$(HELMFILE_ENV) does not include env.kind (only HELMFILE_ENV values without 'gcp' do) - run with HELMFILE_ENV=kind or e2e-kind."; exit 1; }
@if kind get clusters 2>/dev/null | grep -q "^$(KIND_CLUSTER_NAME)$$"; then \
echo "kind cluster '$(KIND_CLUSTER_NAME)' already exists ..."; \
_kindnet_check_kubeconfig=$$(mktemp); \
kind get kubeconfig --name $(KIND_CLUSTER_NAME) > $$_kindnet_check_kubeconfig; \
_has_kindnet=0; \
kubectl --kubeconfig $$_kindnet_check_kubeconfig get daemonset -n kube-system kindnet >/dev/null 2>&1 && _has_kindnet=1; \
rm -f $$_kindnet_check_kubeconfig; \
if [ "$$_has_kindnet" = "1" ]; then \
echo "ERROR: existing kind cluster '$(KIND_CLUSTER_NAME)' still runs the default CNI (kindnet)."; \
echo "It predates disableDefaultCNI, so Cilium NetworkPolicy enforcement will not be effective."; \
echo "Delete and recreate it: make delete-kind-cluster KIND_CLUSTER_NAME=$(KIND_CLUSTER_NAME) && make create-kind-cluster KIND_CLUSTER_NAME=$(KIND_CLUSTER_NAME)"; \
exit 1; \
fi; \
else \
echo "Creating new kind cluster '$(KIND_CLUSTER_NAME)'..."; \
kind create cluster --name $(KIND_CLUSTER_NAME) --config $(KIND_CONFIG); \
fi
@kind export kubeconfig --name $(KIND_CLUSTER_NAME) --kubeconfig $(KUBECONFIG)
@kubectl config use-context kind-$(KIND_CLUSTER_NAME) --kubeconfig $(KUBECONFIG)
@echo "OK: kubeconfig exported and context set for cluster $(KIND_CLUSTER_NAME)" \
.PHONY: delete-kind-cluster
delete-kind-cluster: ## Delete the kind cluster
kind delete cluster --name $(KIND_CLUSTER_NAME)
@echo "OK: deleted kind cluster $(KIND_CLUSTER_NAME)"
.PHONY: kind-build-images
kind-build-images: check-kind check-kubectl-context ## Build and load images to kind (skipped if BUILD_IMAGES=true in env.kind)
ifeq ($(BUILD_IMAGES), true)
@./scripts/kind-build-images.sh
else
@echo ""
@echo "[NOTE: Skipping building images for kind cluster]"
@echo "To enable kind image builds set BUILD_IMAGES=true in env.kind"
endif
# macOS + podman: the podman machine must run rootful (podman machine set
# --rootful <name>) or Cilium's bpf-mount init container crash-loops -
# rootless podman can't mount bpffs.
.PHONY: install-kind-cilium
install-kind-cilium: check-helm check-kubectl-context ## Install Cilium CNI on the kind cluster (GKE uses Dataplane V2 instead)
@echo "Installing Cilium $(CILIUM_VERSION)..."
@helm repo add cilium https://helm.cilium.io/ >/dev/null
@helm repo update cilium >/dev/null
helm upgrade --install $(DRY_RUN_FLAG) cilium cilium/cilium \
--version $(CILIUM_VERSION) \
--namespace $(CILIUM_NAMESPACE) \
--set ipam.mode=kubernetes \
--set operator.replicas=1 \
--wait --timeout 5m
@kubectl wait --for=condition=Ready pod -l k8s-app=cilium --namespace $(CILIUM_NAMESPACE) --timeout=180s
@echo "OK: Cilium is ready"
.PHONY: uninstall-kind-cilium
uninstall-kind-cilium: check-helm check-kubectl-context ## Uninstall Cilium CNI from the kind cluster
@helm uninstall cilium --namespace $(CILIUM_NAMESPACE) || true
@echo "OK: Cilium uninstalled (cluster networking is broken until Cilium is reinstalled or the cluster is deleted)"
# ==== Helmfile Targets ====
.PHONY: template-helmfile
template-helmfile: check-helmfile ## Template the helmfile for the current environment
helmfile -f helmfile/helmfile.yaml.gotmpl -e $(HELMFILE_ENV) template
.PHONY: build-helmfile
build-helmfile: check-helmfile ## Build the helmfile for the current environment
helmfile -f helmfile/helmfile.yaml.gotmpl -e $(HELMFILE_ENV) build
.PHONY: lint-helmfile
lint-helmfile: check-helmfile ## Lint the helmfile for the current environment
helmfile -f helmfile/helmfile.yaml.gotmpl -e $(HELMFILE_ENV) lint
# ==== Maestro Targets ====
# NOTE: This is a workaround to install the AppliedManifestWorks CRD manually if there are issues installing via Helm - https://github.com/openshift-online/maestro/blob/main/charts/maestro-agent/templates/crd.yaml is not working as expected
.PHONY: install-applied-manifest-crd
install-applied-manifest-crd: check-kubectl ## Install AppliedManifestWorks CRD (for Maestro)
@echo "Installing AppliedManifestWorks CRD..."
@kubectl apply -f https://raw.githubusercontent.com/open-cluster-management-io/api/main/work/v1/0000_01_work.open-cluster-management.io_appliedmanifestworks.crd.yaml
@echo "OK: AppliedManifestWorks CRD installed"
.PHONY: install-priority-classes
install-priority-classes: check-kubectl ## Install PriorityClasses for critical infrastructure pods
@kubectl apply -f "$(MANIFESTS_DIR)/priority-classes.yaml"
@echo "OK: PriorityClasses applied"
.PHONY: install-maestro
install-maestro: check-helm check-kubectl check-maestro-namespace install-applied-manifest-crd install-priority-classes ## Install Maestro (server + agent)
helm dependency update $(HELM_DIR)/maestro
@echo "Installing Maestro..."
if ! helm upgrade --install $(DRY_RUN_FLAG) $(MAESTRO_NAMESPACE)-maestro $(HELM_DIR)/maestro \
--namespace $(MAESTRO_NAMESPACE) \
--set agent.installWorkCRDs=false \
--set agent.messageBroker.mqtt.host=maestro-mqtt.$(MAESTRO_NAMESPACE) \
--wait --timeout 5m ; then \
echo "Warning: maestro install failed on cluster; continuing"; \
fi;
@echo "Waiting for Maestro pods to be ready..."
@kubectl wait --for=condition=ready pod -l app.kubernetes.io/instance=$(MAESTRO_NAMESPACE)-maestro --namespace $(MAESTRO_NAMESPACE) --timeout=180s || true
@echo "OK: Maestro pods are ready"
.PHONY: create-maestro-consumer
create-maestro-consumer: check-kubectl check-maestro-namespace check-jq ## Create a Maestro consumer (requires Maestro server running)
@echo "Validating MAESTRO_CONSUMER name..."
@if ! echo "$(MAESTRO_CONSUMER)" | grep -qE '^[a-zA-Z0-9_-]+$$'; then \
echo "ERROR: MAESTRO_CONSUMER='$(MAESTRO_CONSUMER)' contains invalid characters"; \
echo " Only alphanumerics, dashes, and underscores are allowed"; \
exit 1; \
fi
@echo "Creating Maestro consumer '$(MAESTRO_CONSUMER)'..."
@payload=$$(echo '{}' | jq -c --arg name "$(MAESTRO_CONSUMER)" '{name: $$name}'); \
for i in 1 2 3 4 5; do \
exists=$$(kubectl exec deploy/maestro --namespace $(MAESTRO_NAMESPACE) -- \
curl -sS --connect-timeout 5 --max-time 10 http://maestro.$(MAESTRO_NAMESPACE).svc.cluster.local:8000/api/maestro/v1/consumers \
2>/dev/null | jq --arg name "$(MAESTRO_CONSUMER)" '[.items[]? | select(.name == $$name)] | length') 2>/dev/null || exists=0; \
if [ "$$exists" -gt 0 ]; then \
echo "OK: consumer '$(MAESTRO_CONSUMER)' already exists"; exit 0; \
fi; \
status=$$(kubectl exec deploy/maestro --namespace $(MAESTRO_NAMESPACE) --kubeconfig $(KUBECONFIG) -- \
curl -sS --connect-timeout 5 --max-time 10 -o /dev/null -w '%{http_code}' -X POST \
-H "Content-Type: application/json" \
http://maestro.$(MAESTRO_NAMESPACE).svc.cluster.local:8000/api/maestro/v1/consumers \
-d "$$payload" 2>/dev/null) 2>/dev/null || status="error"; \
case "$$status" in \
201) echo "OK: consumer '$(MAESTRO_CONSUMER)' created"; exit 0;; \
409) echo "WARNING: consumer '$(MAESTRO_CONSUMER)' already exists (race condition)"; exit 0;; \
*) echo " Attempt $$i failed (status: $$status), retrying in 5s..."; sleep 5;; \
esac; \
done; \
echo "ERROR: failed to create Maestro consumer after 5 attempts"; exit 1
.PHONY: install-maestro-all
install-maestro-all: install-maestro create-maestro-consumer ## Install Maestro (server + agent + consumer)
@echo "OK: Maestro installed and consumer created"
.PHONY: uninstall-applied-manifest-crd
uninstall-applied-manifest-crd: check-kubectl ## Uninstall AppliedManifestWorks CRD (for Maestro)
@echo "Uninstalling AppliedManifestWorks CRD..."
@kubectl delete -f https://raw.githubusercontent.com/open-cluster-management-io/api/main/work/v1/0000_01_work.open-cluster-management.io_appliedmanifestworks.crd.yaml
@echo "OK: AppliedManifestWorks CRD uninstalled"
.PHONY: uninstall-maestro
uninstall-maestro: check-helm uninstall-applied-manifest-crd ## Uninstall Maestro
helm uninstall $(MAESTRO_NAMESPACE)-maestro --namespace $(MAESTRO_NAMESPACE) || true
# ==== Authorino Targets ====
.PHONY: install-authorino-operator
install-authorino-operator: check-kubectl ## Install the Authorino operator (pinned, cluster-wide) - prerequisite for gateway ext_authz
@echo "Installing Authorino operator $(AUTHORINO_OPERATOR_VERSION)..."
@tmp=$$(mktemp) && \
if ! curl -fsSL -o "$$tmp" "$(AUTHORINO_OPERATOR_MANIFEST)"; then \
echo "ERROR: failed to download Authorino operator manifest"; rm -f "$$tmp"; exit 1; \
fi; \
actual=$$( (command -v sha256sum >/dev/null 2>&1 && sha256sum "$$tmp" || shasum -a 256 "$$tmp") | cut -d' ' -f1 ); \
if [ "$$actual" != "$(AUTHORINO_OPERATOR_MANIFEST_SHA256)" ]; then \
echo "ERROR: Authorino operator manifest checksum mismatch (expected $(AUTHORINO_OPERATOR_MANIFEST_SHA256), got $$actual)"; rm -f "$$tmp"; exit 1; \
fi; \
kubectl apply -f "$$tmp"; \
rc=$$?; rm -f "$$tmp"; exit $$rc
@echo "Waiting for the Authorino operator to be Available..."
@kubectl wait --for=condition=Available deployment/authorino-operator --namespace $(AUTHORINO_OPERATOR_NAMESPACE) --timeout=180s
@echo "OK: Authorino operator installed"
.PHONY: uninstall-authorino-operator
uninstall-authorino-operator: check-kubectl ## Uninstall the Authorino operator
@echo "Uninstalling Authorino operator $(AUTHORINO_OPERATOR_VERSION)..."
@tmp=$$(mktemp) && \
if ! curl -fsSL -o "$$tmp" "$(AUTHORINO_OPERATOR_MANIFEST)"; then \
echo "ERROR: failed to download Authorino operator manifest"; rm -f "$$tmp"; exit 1; \
fi; \
actual=$$( (command -v sha256sum >/dev/null 2>&1 && sha256sum "$$tmp" || shasum -a 256 "$$tmp") | cut -d' ' -f1 ); \
if [ "$$actual" != "$(AUTHORINO_OPERATOR_MANIFEST_SHA256)" ]; then \
echo "ERROR: Authorino operator manifest checksum mismatch (expected $(AUTHORINO_OPERATOR_MANIFEST_SHA256), got $$actual)"; rm -f "$$tmp"; exit 1; \
fi; \
kubectl delete -f "$$tmp" --ignore-not-found; \
rc=$$?; rm -f "$$tmp"; exit $$rc
@echo "OK: Authorino operator uninstalled"
.PHONY: maybe-install-authorino-operator
maybe-install-authorino-operator: ## Install the Authorino operator only when EXT_AUTHZ_ENABLED=true
ifneq ($(strip $(EXT_AUTHZ_ENABLED)),true)
@echo "[NOTE: Skipping Authorino operator install (EXT_AUTHZ_ENABLED != true)]"
@echo "To enable the gateway auth boundary set EXT_AUTHZ_ENABLED=true"
else
$(MAKE) install-authorino-operator
endif
# ==== RabbitMQ Components ====
.PHONY: generate-rabbitmq-values
generate-rabbitmq-values: ## Generate Helm values for RabbitMQ deployments (HELMFILE_ENV=kind only)
ifeq ($(HELMFILE_ENV),kind)
./scripts/generate-rabbitmq-values.sh \
--rabbitmq-url $(RABBITMQ_URL) \
--namespace $(NAMESPACE)
else
@echo "OK: generate-rabbitmq-values is not supported for HELMFILE_ENV=$(HELMFILE_ENV)"
endif
# ==== Gateway Authentication Targets ====
.PHONY: mint-human-token
mint-human-token: check-kubectl-context check-jq check-ext-authz-config ## Mint a test-only human JWT (TENANT_MODEL, TOKEN_TENANT, and optional TOKEN_SUBTENANT)
@./scripts/mint-human-token.sh
.PHONY: check-human-token
check-human-token: check-kubectl-context check-jq check-ext-authz-config check-tenant-isolation-config ## Check human JWT tenant propagation, audience, and missing-claim denial
@./scripts/check-human-token.sh
# ==== Hyperfleet Targets ====
# add-helm-repo: add a helm repo for a component
# Usage: $(call add-helm-repo,<component-name>,<chart-ref>)
define add-helm-repo
helm repo add hyperfleet-$(1) "git+https://github.com/$(CHART_ORG)/hyperfleet-$(1)@charts?ref=$(2)&sparse=0"
helm repo update hyperfleet-$(1)
endef
.PHONY: install-repos
install-repos: check-helmfile-env ## Add all hyperfleet helm repos
$(call add-helm-repo,api,$(API_CHART_REF))
$(call add-helm-repo,sentinel,$(SENTINEL_CHART_REF))
$(call add-helm-repo,adapter,$(ADAPTER_CHART_REF))
.PHONY: install-hyperfleet
install-hyperfleet: check-helmfile-env check-hyperfleet-namespace check-jwt-config check-ext-authz-config check-tenant-isolation-config maybe-install-authorino-operator ## Install all HyperFleet components
helmfile -f helmfile/helmfile.yaml.gotmpl -e $(HELMFILE_ENV) apply
.PHONY: switch-tenant-model
switch-tenant-model: check-helmfile-env check-ext-authz-config check-tenant-isolation-config ## Switch the active tenant model (TENANT_MODEL=onprem|oracle); re-applies the gateway AuthConfig and API dimensions together
@if [ "$(EXT_AUTHZ_ENABLED)" != "true" ]; then \
echo "ERROR: switch-tenant-model requires EXT_AUTHZ_ENABLED=true; with ext_authz off no AuthConfig is deployed and nothing would be switched"; exit 1; \
fi
@case "$(TENANT_MODEL)" in \
onprem|oracle) ;; \
*) echo "ERROR: TENANT_MODEL='$(TENANT_MODEL)' must be 'onprem' or 'oracle'"; exit 1 ;; \
esac
@echo "Switching tenant model to '$(TENANT_MODEL)'..."
helmfile -f helmfile/helmfile.yaml.gotmpl -e $(HELMFILE_ENV) -l component=gateway apply
helmfile -f helmfile/helmfile.yaml.gotmpl -e $(HELMFILE_ENV) -l component=api apply
@echo "OK: tenant model switched to '$(TENANT_MODEL)' (same AuthConfig name replaces the policy; old-model tokens are rejected at the gateway)"
.PHONY: install-api
install-api: check-helmfile-env check-jwt-config check-tenant-isolation-config ## Install HyperFleet API
helmfile apply -f helmfile/helmfile.yaml.gotmpl -e $(HELMFILE_ENV) -l component=api
.PHONY: install-sentinels
install-sentinels: check-helmfile-env ## Install Hyperfleet Sentinels
helmfile apply -f helmfile/helmfile.yaml.gotmpl -e $(HELMFILE_ENV) -l component=sentinel
.PHONY: install-adapters
install-adapters: check-helmfile-env ## Install Hyperfleet Adapters
helmfile apply -f helmfile/helmfile.yaml.gotmpl -e $(HELMFILE_ENV) -l component=adapter
.PHONY: uninstall-hyperfleet
uninstall-hyperfleet: check-kubectl-context ## Uninstall all HyperFleet components
helmfile -f helmfile/helmfile.yaml.gotmpl -e $(HELMFILE_ENV) destroy
.PHONY: uninstall-api
uninstall-api: check-kubectl-context ## Uninstall Hyperfleet API
helmfile -f helmfile/helmfile.yaml.gotmpl -e $(HELMFILE_ENV) -l component=api destroy
.PHONY: uninstall-sentinels
uninstall-sentinels: check-kubectl-context ## Uninstall Hyperfleet Sentinels
helmfile -f helmfile/helmfile.yaml.gotmpl -e $(HELMFILE_ENV) -l component=sentinel destroy
.PHONY: uninstall-adapters
uninstall-adapters: check-kubectl-context ## Uninstall Hyperfleet Adapters
helmfile -f helmfile/helmfile.yaml.gotmpl -e $(HELMFILE_ENV) -l component=adapter destroy
# ==== Lifecycle Function Targets ====
.PHONY: test-lifecycle-function
test-lifecycle-function: ## Run unit tests for the lifecycle enforcer function
@command -v go >/dev/null 2>&1 || { echo "ERROR: go is not installed"; exit 1; }
cd "$(LIFECYCLE_DIR)" && go test ./... -v
.PHONY: build-lifecycle-function
build-lifecycle-function: ## Build the lifecycle enforcer function
@command -v go >/dev/null 2>&1 || { echo "ERROR: go is not installed"; exit 1; }
cd "$(LIFECYCLE_DIR)" && go build ./...
.PHONY: lint-lifecycle-function
lint-lifecycle-function: ## Lint the lifecycle enforcer function
@command -v go >/dev/null 2>&1 || { echo "ERROR: go is not installed"; exit 1; }
cd "$(LIFECYCLE_DIR)" && go vet ./...
# ==== OCI CI Sweep Function Targets ====
.PHONY: test-oci-sweep-function
test-oci-sweep-function: ## Run unit tests for the OCI CI compartment sweep function
@command -v go >/dev/null 2>&1 || { echo "ERROR: go is not installed"; exit 1; }
cd "$(OCI_SWEEP_DIR)" && go test ./... -v
.PHONY: build-oci-sweep-function
build-oci-sweep-function: ## Build the OCI CI compartment sweep function
@command -v go >/dev/null 2>&1 || { echo "ERROR: go is not installed"; exit 1; }
cd "$(OCI_SWEEP_DIR)" && go build ./...
.PHONY: lint-oci-sweep-function
lint-oci-sweep-function: ## Lint the OCI CI compartment sweep function
@command -v go >/dev/null 2>&1 || { echo "ERROR: go is not installed"; exit 1; }
cd "$(OCI_SWEEP_DIR)" && go vet ./...
.PHONY: add-ttl-labels
add-ttl-labels: ## Add TTL labels to existing GKE clusters (DRY_RUN=true by default)
./scripts/add-ttl-labels.sh
# ==== Namespace Cleaner Targets ====
.PHONY: install-cleaner
install-cleaner: check-helm check-kubectl ## Install namespace cleaner CronJob (CLEANER_SCHEDULE, CLEANER_LABEL_SELECTOR, CLEANER_AGE_MINUTES)
$(call check-namespace,CLEANER_NAMESPACE)
helm upgrade --install namespace-cleaner $(HELM_DIR)/namespace-cleaner \
--namespace $(CLEANER_NAMESPACE) \
--set-string "schedule=$(CLEANER_SCHEDULE)" \
--set-string "labelSelector=$(CLEANER_LABEL_SELECTOR)" \
--set "ageMinutes=$(CLEANER_AGE_MINUTES)" \
--set "maestroURL=$(CLEANER_MAESTRO_URL)" \
--wait
@echo "OK: namespace cleaner installed in namespace $(CLEANER_NAMESPACE)"
.PHONY: uninstall-cleaner
uninstall-cleaner: check-helm check-kubectl ## Uninstall namespace cleaner CronJob
helm uninstall namespace-cleaner --namespace $(CLEANER_NAMESPACE) || true
@echo "OK: namespace cleaner uninstalled"
# ==== Observability Targets ====
OBSERVABILITY_HELMFILE := helmfile/observability.yaml.gotmpl
GRAFANA_ADMIN_USER ?= admin
GRAFANA_ADMIN_PASSWORD ?=
.PHONY: maybe-install-grafana
maybe-install-grafana:
ifneq ($(strip $(OBSERVABILITY_ENABLED)),true)
@echo "[NOTE: Skipping observability stack]"
@echo "To enable set OBSERVABILITY_ENABLED=true in env.kind or env.gcp"
else
$(MAKE) install-grafana
endif
.PHONY: install-grafana
install-grafana: check-helmfile check-kubectl-context ## Install kube-prometheus-stack (Prometheus + Grafana + Operator + CRDs)
@test -n "$(GRAFANA_ADMIN_PASSWORD)" || { echo "ERROR: GRAFANA_ADMIN_PASSWORD is required"; exit 1; }
$(call check-namespace,MONITORING_NAMESPACE)
@kubectl create secret generic grafana-admin-credentials \
--namespace $(MONITORING_NAMESPACE) \
--from-literal=admin-user=$(GRAFANA_ADMIN_USER) \
--from-literal=admin-password=$(GRAFANA_ADMIN_PASSWORD) \
--dry-run=client -o yaml | kubectl apply -f -
helmfile -f $(OBSERVABILITY_HELMFILE) -e $(HELMFILE_ENV) -l component=kube-prometheus-stack apply
.PHONY: uninstall-grafana
uninstall-grafana: check-kubectl-context ## Uninstall kube-prometheus-stack
@if helm list --namespace $(MONITORING_NAMESPACE) --short | grep -q '^kube-prometheus-stack$$'; then \
helmfile -f $(OBSERVABILITY_HELMFILE) -e $(HELMFILE_ENV) -l component=kube-prometheus-stack destroy; \
kubectl delete secret grafana-admin-credentials --namespace $(MONITORING_NAMESPACE) --ignore-not-found; \
else \
echo "[NOTE: kube-prometheus-stack not installed, skipping uninstall]"; \
fi
.PHONY: maybe-install-tracing
maybe-install-tracing:
ifneq ($(strip $(TRACING_ENABLED)),true)
@echo "[NOTE: Skipping tracing backend]"
@echo "To enable set TRACING_ENABLED=true in env.kind or env.gcp"
else
@if [ "$(strip $(OBSERVABILITY_ENABLED))" != "true" ]; then \
echo "ERROR: TRACING_ENABLED=true requires OBSERVABILITY_ENABLED=true"; exit 1; \
fi
$(MAKE) install-tracing
endif
.PHONY: install-tracing
install-tracing: check-helmfile check-kubectl-context ## Install Tempo + OpenTelemetry Collector tracing backend
$(call check-dns-label,MONITORING_NAMESPACE)
$(call check-namespace,MONITORING_NAMESPACE)
helmfile -f "$(OBSERVABILITY_HELMFILE)" -e "$(HELMFILE_ENV)" -l component=tempo apply
helmfile -f "$(OBSERVABILITY_HELMFILE)" -e "$(HELMFILE_ENV)" -l component=otel-collector apply
.PHONY: uninstall-tracing
uninstall-tracing: check-kubectl-context ## Uninstall Tempo + OpenTelemetry Collector
$(call check-dns-label,MONITORING_NAMESPACE)
@helm_out=$$(helm list --namespace "$(MONITORING_NAMESPACE)" --short) || exit 1; \
if echo "$$helm_out" | grep -q '^otel-collector$$'; then \
helmfile -f "$(OBSERVABILITY_HELMFILE)" -e "$(HELMFILE_ENV)" -l component=otel-collector destroy; \
else \
echo "[NOTE: otel-collector not installed, skipping uninstall]"; \
fi
@helm_out=$$(helm list --namespace "$(MONITORING_NAMESPACE)" --short) || exit 1; \
if echo "$$helm_out" | grep -q '^tempo$$'; then \
helmfile -f "$(OBSERVABILITY_HELMFILE)" -e "$(HELMFILE_ENV)" -l component=tempo destroy; \
else \
echo "[NOTE: tempo not installed, skipping uninstall]"; \
fi
# ==== Prerequisite/Utility Targets ====
.PHONY: check-helm
check-helm: ## Verify helm and helm-git plugin are installed
@command -v helm >/dev/null 2>&1 || { echo "ERROR: helm is not installed"; exit 1; }
@helm plugin list | grep -q "helm-git" || { echo "ERROR: helm-git plugin is not installed. Install with: helm plugin install https://github.com/aslafy-z/helm-git"; exit 1; }
@echo "OK: helm and helm-git plugin found"
.PHONY: check-helmfile
check-helmfile: check-helm ## Verify helmfile is installed
@command -v helmfile >/dev/null 2>&1 || { echo "ERROR: helmfile is not installed"; exit 1; }
@echo "OK: helmfile found"
@helm diff version >/dev/null 2>&1 || { echo "ERROR: helm diff plugin is not installed. Install with: helm plugin install https://github.com/databus23/helm-diff --verify=false"; exit 1; }
@echo "OK: helm diff plugin found"
.PHONY: check-kind
check-kind: ## Verify kind is installed
@command -v kind >/dev/null 2>&1 || { echo "ERROR: kind is not installed"; exit 1; }
@echo "OK: kind found"
.PHONY: check-kubectl
check-kubectl: ## Verify kubectl is installed and context is set
@command -v kubectl >/dev/null 2>&1 || { echo "ERROR: kubectl is not installed"; exit 1; }
@kubectl config current-context >/dev/null 2>&1 || { echo "ERROR: no kubectl context set"; exit 1; }
@echo "OK: kubectl found, context: $$(kubectl config current-context)"
.PHONY: check-jq
check-jq: ## Verify jq is installed
@command -v jq >/dev/null 2>&1 || { echo "ERROR: jq is not installed. Install with: brew install jq"; exit 1; }
@echo "OK: jq found"
.PHONY: check-helmfile-env
check-helmfile-env: check-helmfile check-kubectl-context check-helmfile-env-generated ## Verify kubectl context and generated values directory exists
.PHONY: check-helmfile-env-generated
check-helmfile-env-generated: ## Check that the generated directory exists based on HELMFILE_ENV
@if [ "$(HELMFILE_ENV)" = "gcp" ]; then \
test -d $(GENERATED_DIR) || { echo "ERROR: generated-values-from-terraform directory does not exist"; exit 1; }; \
echo "OK: generated-values-from-terraform directory exists"; \
elif [ "$(HELMFILE_ENV)" = "kind" ]; then \
test -d $(GENERATED_RABBITMQ_DIR) || { echo "ERROR: generated-values-rabbitmq directory does not exist"; exit 1; }; \
echo "OK: generated-values-rabbitmq directory exists"; \
fi
@echo "OK: Did not need to validate generated values for environment: $(HELMFILE_ENV)"
.PHONY: check-kubectl-context
check-kubectl-context: check-kubectl ## Verify kubectl context matches HELMFILE_ENV for kind and e2e-kind
@if [ "$(HELMFILE_ENV)" = "kind" ] || [ "$(HELMFILE_ENV)" = "e2e-kind" ]; then \
if ! kubectl config current-context | grep -q "kind-"; then \
echo "ERROR: HELMFILE_ENV=$(HELMFILE_ENV) requires kind context"; \
exit 1; \
fi; \
echo "OK: kubectl context matches HELMFILE_ENV=$(HELMFILE_ENV)"; \
fi;
.PHONY: check-terraform
check-terraform: ## Verify terraform is installed
@command -v terraform >/dev/null 2>&1 || { echo "ERROR: terraform is not installed"; exit 1; }
@echo "OK: terraform found"
.PHONY: check-tf-files
check-tf-files: ## Verify terraform env files exist
@test -f $(TF_DIR)/$(TF_BACKEND) || { echo "ERROR: backend file not found: $(TF_DIR)/$(TF_BACKEND)"; echo "Create a copy from $(TF_DIR)/$(TF_BACKEND).example and customize it"; exit 1; }
@test -f $(TF_DIR)/$(TF_VARS) || { echo "ERROR: tfvars file not found: $(TF_DIR)/$(TF_VARS)"; echo "Create a copy from $(TF_DIR)/$(TF_VARS).example and customize it"; exit 1; }
@echo "OK: terraform env files found for $(TF_ENV)"
# check-dns-label: validate a Make variable as a Kubernetes DNS label.
# Pass the variable name (not its value) so the shell expands it safely.
# Usage: $(call check-dns-label,VAR_NAME)
define check-dns-label
@printf '%s' "$${$(1)}" | grep -qE '^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$$' \
|| { echo "ERROR: $(1) '$${$(1)}' is not a valid DNS label (lowercase alphanumeric and hyphens, 1-63 chars)"; exit 1; }
endef
# check-namespace: check if a namespace exists and create it if it doesn't.
# Pass the variable name (not its value) so the shell expands it safely.
# Usage: $(call check-namespace,VAR_NAME)
define check-namespace
@kubectl get namespace "$${$(1)}" >/dev/null 2>&1 || kubectl create namespace "$${$(1)}" \
|| { echo "ERROR: failed to create namespace $${$(1)}"; exit 1; }
@echo "OK: namespace $${$(1)} ready"
endef
.PHONY: check-jwt-config
check-jwt-config: ## Validate OIDC variables when JWT_AUTH_ENABLED=true with GCP OIDC; no-op otherwise
@if [ "$(JWT_AUTH_ENABLED)" = "true" ] && [ -n "$(OIDC_ISSUER_URL)" ]; then \
echo "$(OIDC_ISSUER_URL)" | grep -qE "^https://[a-zA-Z0-9]" \
|| { echo "ERROR: OIDC_ISSUER_URL must be a valid https:// URL (got: $(OIDC_ISSUER_URL))"; exit 1; }; \
if [ -n "$(OIDC_JWKS_URL)" ]; then \
echo "$(OIDC_JWKS_URL)" | grep -qE "^https://[a-zA-Z0-9]" \
|| { echo "ERROR: OIDC_JWKS_URL must be a valid https:// URL (got: $(OIDC_JWKS_URL))"; exit 1; }; \
fi; \
echo "OK: JWT auth config validated (OIDC_ISSUER_URL=$(OIDC_ISSUER_URL))"; \
elif [ "$(JWT_AUTH_ENABLED)" = "true" ] && [ -n "$(OIDC_JWKS_URL)" ]; then \
echo "ERROR: OIDC_JWKS_URL is set without OIDC_ISSUER_URL. Set both or neither."; exit 1; \
elif [ "$(JWT_AUTH_ENABLED)" = "true" ]; then \
echo "OK: JWT auth enabled with K8s in-cluster OIDC (no OIDC_ISSUER_URL needed)"; \
fi
.PHONY: check-ext-authz-config
check-ext-authz-config: ## Validate gateway auth config when EXT_AUTHZ_ENABLED=true; no-op otherwise
@if [ "$(EXT_AUTHZ_ENABLED)" = "true" ]; then \
case "$(OIDC_ISSUER_MODE)" in \
mock) \
echo "OK: ext_authz config validated (TENANT_MODEL=$(TENANT_MODEL), OIDC_ISSUER_MODE=mock)" >&2; \
;; \
external) \
test -n "$(OIDC_ISSUER_URL)" || { echo "ERROR: external OIDC_ISSUER_MODE requires OIDC_ISSUER_URL. The gateway is fail-closed; without an issuer the AuthConfig never becomes Ready and every request 403s."; exit 1; }; \
echo "$(OIDC_ISSUER_URL)" | grep -qE "^https://[a-zA-Z0-9]" \
|| { echo "ERROR: OIDC_ISSUER_URL must be a valid https:// URL in external mode (got: $(OIDC_ISSUER_URL))"; exit 1; }; \
echo "OK: ext_authz config validated (TENANT_MODEL=$(TENANT_MODEL), OIDC_ISSUER_MODE=external, issuer=$(OIDC_ISSUER_URL))" >&2; \
;; \
*) echo "ERROR: OIDC_ISSUER_MODE='$(OIDC_ISSUER_MODE)' must be 'mock' or 'external'"; exit 1 ;; \
esac; \
case "$(TENANT_MODEL)" in \
onprem|oracle) ;; \
*) echo "ERROR: TENANT_MODEL='$(TENANT_MODEL)' must be 'onprem' or 'oracle'"; exit 1 ;; \
esac; \
fi
.PHONY: check-tenant-isolation-config
check-tenant-isolation-config: ## Validate tenant isolation has a trusted gateway and supported model
@if [ "$(TENANT_ISOLATION_ENABLED)" = "true" ]; then \
[ "$(EXT_AUTHZ_ENABLED)" = "true" ] \
|| { echo "ERROR: TENANT_ISOLATION_ENABLED=true requires EXT_AUTHZ_ENABLED=true so tenant headers come from the trusted Authorino gateway"; exit 1; }; \
case "$(TENANT_MODEL)" in \
onprem|oracle) ;; \
*) echo "ERROR: TENANT_MODEL='$(TENANT_MODEL)' must be 'onprem' or 'oracle'"; exit 1 ;; \
esac; \
echo "OK: tenant isolation config validated (TENANT_MODEL=$(TENANT_MODEL))"; \
fi
.PHONY: check-hyperfleet-namespace
check-hyperfleet-namespace: ## Create Hyperfleet namespace if it doesn't exist and label it
$(call check-dns-label,NAMESPACE)
$(call check-namespace,NAMESPACE)
@kubectl label namespace "$${NAMESPACE}" "hyperfleet.io/test-run=$${NAMESPACE}" --overwrite >/dev/null
@echo "OK: namespace $${NAMESPACE} labeled with hyperfleet.io/test-run=$${NAMESPACE}"
.PHONY: check-maestro-namespace
check-maestro-namespace: ## Create Maestro namespace if it doesn't exist
$(call check-namespace,MAESTRO_NAMESPACE)
.PHONY: check-gke-context
check-gke-context: check-kubectl ## Verify kubectl context points to GKE cluster
@CONTEXT=$$(kubectl config current-context); \
if echo "$$CONTEXT" | grep -q "gke_"; then \
echo "OK: connected to GKE cluster (context: $$CONTEXT)"; \
else \
echo "WARNING: current context '$$CONTEXT' does not appear to be a GKE cluster"; \
echo " Expected context name containing 'gke_'"; \
echo " Continuing anyway, but verify your cluster is correct"; \
fi
.PHONY: clean-generated
clean-generated: ## Remove generated dir
rm -rf $(GENERATED_DIR)
rm -rf $(GENERATED_RABBITMQ_DIR)
@echo "OK: cleaned generated terraform values"
.PHONY: helm-deps
helm-deps: check-helm ## Run helm dependency update for all charts
@for chart in $(HELM_DIR)/*/; do \
echo "Updating dependencies for $$chart..."; \
helm dependency update "$$chart"; \
done
.PHONY: status
status: check-kubectl check-helmfile-env ## Show helm releases and pod status
@echo "=== Helm Releases ==="
@helm list --namespace $(NAMESPACE) 2>/dev/null || true
@helm list --namespace $(MAESTRO_NAMESPACE) 2>/dev/null || true
@helm list --namespace $(MONITORING_NAMESPACE) 2>/dev/null || true
@echo ""
@echo "=== Pods ==="
@kubectl get pods --namespace $(NAMESPACE) 2>/dev/null || true
@kubectl get pods --namespace $(MAESTRO_NAMESPACE) 2>/dev/null || true
@kubectl get pods --namespace $(MONITORING_NAMESPACE) 2>/dev/null || true
.PHONY: help
help: ## Show this help message
@echo "HyperFleet Infrastructure - Available Make Targets"
@echo ""
@echo "Usage: make [target] [VARIABLE=value ...]"
@echo ""
@echo "Environment: HELMFILE_ENV=$(HELMFILE_ENV) (gcp|kind|e2e-gcp|e2e-kind)"
@echo ""
@awk '/^# ====/ { \
section = $$0; \
sub(/^# ==== /, "", section); \
sub(/ ====$$/,"", section); \
next; \
} \
/^[a-zA-Z_-]+:.*?## / { \
if (section) { \
if (!(section in seen)) { \
if (count > 0) print ""; \
printf "\033[1m%s:\033[0m\n", section; \
seen[section] = 1; \
count++; \
} \
split($$0, parts, ":"); \
target = parts[1]; \
sub(/.*## /, "", $$0); \
printf " \033[36m%-35s\033[0m %s\n", target, $$0; \
} \
}' $(MAKEFILE_LIST)
# ==== CI Targets ====
# ci-dry-run: validation on terraform and helm plugins and maestro helm chart
# ci-test: Run terraform install + maestro install + health check on maestro
# ci-cleanup: Uninstall maestro and destroy terraform resources
# TODO: HYPERFLEET-1067 - Will add more complete helmfile linting and validation
# Currently only linting, validating and installing via terrafrom and installing the maestro chart
# CI-DRY-RUN
.PHONY: validate-terraform
validate-terraform: check-terraform ## Validate Terraform syntax and formatting (all stacks)
cd $(TF_DIR) && terraform fmt -check -recursive -diff
@for dir in $(TF_DIR) $(OCI_TF_DIR); do \
echo "Validating Terraform in $$dir..."; \
( cd "$$dir" && terraform init -backend=false && terraform validate ) || exit 1; \
done
.PHONY: lint-helm
lint-helm: check-helm helm-deps ## Lint all Helm charts
@for chart in $(HELM_DIR)/*/; do \
echo "Linting $$chart..."; \
helm lint "$$chart" || exit 1; \
done
.PHONY: lint-shellcheck
lint-shellcheck: ## Validate shell scripts with shellcheck
@if command -v shellcheck >/dev/null 2>&1; then \
find . -name '*.sh' -not -path './.terraform/*' -not -path './.git/*' -exec shellcheck {} +; \
elif [ -n "$$CI" ]; then \
echo "ERROR: shellcheck is required in CI but not installed"; exit 1; \
else \
echo "WARN: shellcheck not installed, skipping"; \
fi
.PHONY: validate-maestro
validate-maestro: check-helm ## Validate Maestro Helm chart rendering
helm dependency update $(HELM_DIR)/maestro
@echo "Validating maestro chart..."
helm template $(MAESTRO_NAMESPACE)-maestro $(HELM_DIR)/maestro \
--set agent.messageBroker.mqtt.host=maestro-mqtt.$(MAESTRO_NAMESPACE) > /dev/null
@echo "OK: all Helm charts rendered successfully"
.PHONY: validate-mock-oidc
validate-mock-oidc: check-helm ## Validate the test-only mock OIDC chart and issuer-mode guards
@echo "Validating mock OIDC chart..."
@helm lint $(HELM_DIR)/mock-oidc
@out=$$(helm template hyperfleet-mock-oidc $(HELM_DIR)/mock-oidc --namespace test) \
|| { echo "ERROR: mock OIDC chart failed to render"; exit 1; }; \
echo "$$out" | grep -q 'kind: NetworkPolicy' \
|| { echo "ERROR: mock OIDC ingress NetworkPolicy is missing"; exit 1; }; \
echo "$$out" | grep -q 'app.kubernetes.io/component: token-helper' \
|| { echo "ERROR: mock OIDC ingress does not allow the token helper pod"; exit 1; }; \
echo "$$out" | grep -q 'authorino-resource: authorino' \
|| { echo "ERROR: mock OIDC ingress does not allow the Authorino pod"; exit 1; }; \
if echo "$$out" | grep -q 'kubernetes.io/metadata.name'; then \
echo "ERROR: mock OIDC ingress must not allow the entire namespace"; exit 1; \
fi; \
grep -Fq -- '--labels=app.kubernetes.io/component=token-helper' scripts/mint-human-token.sh \
|| { echo "ERROR: mock token helper pod label is missing"; exit 1; }
@set -eu; \
created_files=""; \
cleanup_generated() { \
for file in $$created_files; do rm -f "$$file"; done; \
rmdir generated-values-rabbitmq generated-values-from-terraform 2>/dev/null || true; \
}; \
trap cleanup_generated EXIT; \
for file in generated-values-rabbitmq/adapter1.yaml generated-values-rabbitmq/adapter2.yaml generated-values-rabbitmq/adapter3.yaml generated-values-rabbitmq/sentinel-clusters.yaml generated-values-rabbitmq/sentinel-nodepools.yaml generated-values-from-terraform/adapter1.yaml generated-values-from-terraform/adapter2.yaml generated-values-from-terraform/adapter3.yaml generated-values-from-terraform/sentinel-clusters.yaml generated-values-from-terraform/sentinel-nodepools.yaml; do \
if [ ! -f "$$file" ]; then \
mkdir -p "$$(dirname "$$file")"; \
printf '%s\n' 'broker: {}' > "$$file"; \
created_files="$$created_files $$file"; \
fi; \
done; \
for env in kind e2e-gcp; do \
build=$$(HELMFILE_ENV=$$env NAMESPACE=hf-validate-$$env EXT_AUTHZ_ENABLED=true TENANT_ISOLATION_ENABLED=true OIDC_ISSUER_MODE=mock OIDC_ISSUER_URL= \
helmfile -f helmfile/helmfile.yaml.gotmpl -e $$env build) \
|| { echo "ERROR: Helmfile mock-mode build failed for $$env"; exit 1; }; \
echo "$$build" | grep -q 'name: hyperfleet-mock-oidc' \
|| { echo "ERROR: mock issuer release missing for $$env"; exit 1; }; \
echo "$$build" | grep -Fq "oidcIssuerUrl: http://hyperfleet-mock-oidc.hf-validate-$$env.svc.cluster.local:8080/default" \
|| { echo "ERROR: Helmfile did not pass the mock issuer URL to the gateway for $$env"; exit 1; }; \
done; \
if HELMFILE_ENV=kind NAMESPACE=hf-validate-kind EXT_AUTHZ_ENABLED=true OIDC_ISSUER_MODE=mock OIDC_ISSUER_URL= JWT_AUTH_ENABLED=true \
helmfile -f helmfile/helmfile.yaml.gotmpl -e kind build >/dev/null 2>&1; then \
echo "ERROR: Helmfile accepted JWT_AUTH_ENABLED=true with ext_authz mock mode"; exit 1; \
fi; \
if HELMFILE_ENV=kind NAMESPACE=hf-validate-kind EXT_AUTHZ_ENABLED=true OIDC_ISSUER_MODE=mock OIDC_ISSUER_URL=https://issuer.invalid \
helmfile -f helmfile/helmfile.yaml.gotmpl -e kind build >/dev/null 2>&1; then \
echo "ERROR: Helmfile accepted OIDC_ISSUER_URL with mock mode"; exit 1; \
fi; \
HELMFILE_ENV=kind NAMESPACE=hf-validate-kind EXT_AUTHZ_ENABLED=false OIDC_ISSUER_MODE=mock OIDC_ISSUER_URL=https://issuer.invalid JWT_AUTH_ENABLED=true \
helmfile -f helmfile/helmfile.yaml.gotmpl -e kind build >/dev/null \
|| { echo "ERROR: Helmfile rejected OIDC_ISSUER_URL with mock mode and EXT_AUTHZ_ENABLED=false"; exit 1; }; \
HELMFILE_ENV=e2e-gcp NAMESPACE=hf-validate-e2e-gcp EXT_AUTHZ_ENABLED=false OIDC_ISSUER_MODE=mock OIDC_ISSUER_URL=https://issuer.invalid JWT_AUTH_ENABLED=true \
helmfile -f helmfile/helmfile.yaml.gotmpl -e e2e-gcp build >/dev/null \
|| { echo "ERROR: Helmfile rejected OIDC_ISSUER_URL with mock mode and EXT_AUTHZ_ENABLED=false for e2e-gcp"; exit 1; }; \
if HELMFILE_ENV=gcp NAMESPACE=hf-validate-gcp EXT_AUTHZ_ENABLED=true OIDC_ISSUER_MODE=mock OIDC_ISSUER_URL= \
helmfile -f helmfile/helmfile.yaml.gotmpl -e gcp build >/dev/null 2>&1; then \
echo "ERROR: Helmfile accepted the test-only mock issuer in regular gcp"; exit 1; \
fi; \
build=$$(HELMFILE_ENV=gcp NAMESPACE=hf-validate-gcp EXT_AUTHZ_ENABLED=true TENANT_ISOLATION_ENABLED=true OIDC_ISSUER_MODE=external OIDC_ISSUER_URL=https://issuer.invalid \
helmfile -f helmfile/helmfile.yaml.gotmpl -e gcp build) \
|| { echo "ERROR: Helmfile external-mode build failed for gcp"; exit 1; }; \
echo "$$build" | grep -Fq 'oidcIssuerUrl: https://issuer.invalid' \
|| { echo "ERROR: Helmfile did not pass the external issuer URL to the gateway"; exit 1; }; \
echo "$$build" | grep -q 'name: hyperfleet-mock-oidc' \
|| { echo "ERROR: mock issuer release is missing from state for regular gcp external mode cleanup"; exit 1; }; \
if HELMFILE_ENV=gcp NAMESPACE=hf-validate-gcp EXT_AUTHZ_ENABLED=true OIDC_ISSUER_MODE=external OIDC_ISSUER_URL=http://issuer.invalid \
helmfile -f helmfile/helmfile.yaml.gotmpl -e gcp build >/dev/null 2>&1; then \
echo "ERROR: Helmfile accepted an HTTP issuer in external mode"; exit 1; \
fi
@if ! helm template gw $(HELM_DIR)/hyperfleet-gateway --namespace default \
--set auth.extAuthz.enabled=true --set tenant.model=onprem \
--set-string auth.oidc.issuerUrl=http://issuer.invalid/default >/dev/null; then \
echo "ERROR: gateway chart rejected a directly configured HTTP issuer"; exit 1; \
fi
@if helm template gw $(HELM_DIR)/hyperfleet-gateway --namespace default \
--set auth.extAuthz.enabled=true >/dev/null 2>&1; then \
echo "ERROR: empty issuer URL was accepted"; exit 1; \
fi
@if helm template gw $(HELM_DIR)/hyperfleet-gateway --namespace default \
--set auth.extAuthz.enabled=true \
--set-string auth.oidc.issuerUrl=issuer.invalid/default >/dev/null 2>&1; then \
echo "ERROR: malformed issuer URL was accepted"; exit 1; \
fi
@echo "OK: mock OIDC chart and Helmfile issuer-mode guards are valid"
.PHONY: validate-authorino
validate-authorino: check-helm ## Validate gateway auth templates
@echo "Validating Authorino gateway templates..."
@for model in onprem oracle; do \
out=$$(helm template gw $(HELM_DIR)/hyperfleet-gateway --namespace default \
--set auth.extAuthz.enabled=true --set tenant.model=$$model \
--set auth.oidc.issuerUrl=https://issuer.invalid/oidc 2>&1) \
|| { echo "ERROR: render failed for tenantModel=$$model"; echo "$$out"; exit 1; }; \
echo "$$out" | grep -q "failure_mode_allow: false" \
|| { echo "ERROR ($$model): ext_authz is not fail-closed"; exit 1; }; \
echo "$$out" | grep -q "operator.authorino.kuadrant.io/v1beta1" \
|| { echo "ERROR ($$model): Authorino instance not rendered"; exit 1; }; \
echo "$$out" | awk '/name: envoy.filters.http.ext_authz/{e=NR} /name: envoy.filters.http.router/{r=NR} END{exit !(e>0 && r>0 && e<r)}' \
|| { echo "ERROR ($$model): ext_authz must be ordered before router"; exit 1; }; \
echo "$$out" | grep -q "kubernetesTokenReview" \
|| { echo "ERROR ($$model): hyperfleet-components machine identity (kubernetesTokenReview) not rendered"; exit 1; }; \
echo "$$out" | grep -A3 "kubernetesTokenReview:" | grep -q '"hyperfleet-api"' \
|| { echo "ERROR ($$model): kubernetesTokenReview audiences does not include hyperfleet-api"; exit 1; }; \
echo "$$out" | grep -A12 '"require-human-audience":' | grep -q 'type(auth.identity.aud) == string' \
|| { echo "ERROR ($$model): human JWT audience rule is missing"; exit 1; }; \
echo "$$out" | grep -A12 '"require-human-audience":' | grep -q 'auth.identity.aud.exists' \
|| { echo "ERROR ($$model): human JWT audience rule does not support array claims"; exit 1; }; \
echo "$$out" | grep -A12 '"require-human-audience":' | grep -q 'hyperfleet-api' \
|| { echo "ERROR ($$model): human JWT audience is not hyperfleet-api"; exit 1; }; \
subj_pattern=$$(echo "$$out" | sed -n 's/.*value: "\(\^system:serviceaccount:[^"]*\)".*/\1/p') ; \
[ -n "$$subj_pattern" ] \
|| { echo "ERROR ($$model): restrict-system-subjects pattern not rendered"; exit 1; }; \
for sa in nodepools-hyperfleet-sentinel clusters-hyperfleet-sentinel adapter1-hyperfleet-adapter; do \
echo "system:serviceaccount:default:$$sa" | grep -Eq "$$subj_pattern" \
|| { echo "ERROR ($$model): restrict-system-subjects pattern rejects known-good SA $$sa"; exit 1; }; \
done; \
for sa in default some-other-sa hyperfleet-api hyperfleet-adapter-lookalike; do \
echo "system:serviceaccount:default:$$sa" | grep -Eq "$$subj_pattern" \
&& { echo "ERROR ($$model): restrict-system-subjects pattern wrongly accepts unlisted SA $$sa (audience alone must not be the credential)"; exit 1; }; \
true; \
done; \
done
@helm template gw $(HELM_DIR)/hyperfleet-gateway --set auth.extAuthz.enabled=true --set tenant.model=onprem --set auth.oidc.issuerUrl=https://issuer.invalid/oidc \
| awk '/"x-tenant-project":/{f=1} f&&/when:/{g=1} f&&g&&/selector: auth.identity.project_id/{ok=1} END{exit !ok}' \
|| { echo "ERROR: onprem optional header x-tenant-project is not when-gated"; exit 1; }
@if helm template gw $(HELM_DIR)/hyperfleet-gateway --set auth.extAuthz.enabled=true --set tenant.model=bogus >/dev/null 2>&1; then \
echo "ERROR: invalid tenantModel was accepted (expected fail-fast)"; exit 1; \
fi
@hosts_out=$$(helm template gw $(HELM_DIR)/hyperfleet-gateway --set auth.extAuthz.enabled=true --set tenant.model=onprem \
--set auth.oidc.issuerUrl=https://issuer.invalid/oidc --set auth.authorino.hosts='{gateway.example.com}') \
|| { echo "ERROR: render failed with authorino.hosts set"; exit 1; }; \
echo "$$hosts_out" | grep -q '"gw-hyperfleet-gateway"' \
|| { echo "ERROR: default gateway Service DNS host dropped when authorino.hosts is set"; exit 1; }; \
echo "$$hosts_out" | grep -q '"localhost"' \
|| { echo "ERROR: default localhost host dropped when authorino.hosts is set"; exit 1; }; \
echo "$$hosts_out" | grep -q '"gateway.example.com"' \
|| { echo "ERROR: configured authorino.hosts entry not rendered"; exit 1; }
@echo "OK: Authorino gateway templates valid (audience enforcement, ext_authz before router, fail-closed, tenant guards, additive AUTHORINO_HOSTS, machine subject allowlist)"
.PHONY: validate-network-policies
validate-network-policies: check-helm ## Validate network-policies Helm chart rendering
@echo "Validating network-policies chart..."
@out=$$(helm template netpol $(HELM_DIR)/network-policies --set namespace=hyperfleet-local) \
|| { echo "ERROR: network-policies chart failed to render"; exit 1; }; \
echo "$$out" | grep -q "name: hyperfleet-api-ingress" \
|| { echo "ERROR: hyperfleet-api-ingress NetworkPolicy not rendered"; exit 1; }; \
echo "$$out" | grep -q "name: hyperfleet-api-postgres-ingress" \
|| { echo "ERROR: hyperfleet-api-postgres-ingress NetworkPolicy not rendered"; exit 1; }
@echo "OK: network-policies chart rendered successfully"
.PHONY: ci-validate
ci-validate: validate-terraform lint-helm lint-shellcheck ## Ci validate: validate terraform (all stacks) + lint helm + lint shellcheck
.PHONY: ci-dry-run
ci-dry-run: ci-validate ## Ci dry-run: ci-validate + validate maestro + validate authorino + validate network policies + validate mock OIDC
$(MAKE) validate-maestro
$(MAKE) validate-mock-oidc
$(MAKE) validate-authorino
$(MAKE) validate-network-policies
.PHONY: health-check-maestro
health-check-maestro: check-kubectl ## Verify Maestro Components
@echo "Checking Maestro components..."
@deploys=$$(kubectl get deployments --namespace $(MAESTRO_NAMESPACE) --kubeconfig $(KUBECONFIG) -o name) && \
[ -n "$$deploys" ] || { echo "ERROR: no deployments found in namespace $(MAESTRO_NAMESPACE)"; exit 1; }; \
for deploy in $$deploys; do \
echo " Waiting for $$deploy..."; \
kubectl rollout status $$deploy --namespace $(MAESTRO_NAMESPACE) --kubeconfig $(KUBECONFIG) --timeout=300s || exit 1; \
done
@echo "OK: all components healthy"
.PHONY: ci-test
ci-test: install-terraform get-credentials install-priority-classes install-maestro create-maestro-consumer health-check-maestro ## Ci test: install terraform + get credentials + install maestro + create maestro consumer + health check maestro
# CI-CLEANUP
.PHONY: ci-cleanup
ci-cleanup: uninstall-maestro destroy-terraform ## Ci cleanup: uninstall maestro + destroy terraform
# ==== Full Deployment Targets ====
# Kind targets
.PHONY: local-up-kind
local-up-kind: create-kind-cluster install-kind-cilium kind-build-images install-priority-classes install-maestro-all generate-rabbitmq-values maybe-install-grafana maybe-install-tracing install-hyperfleet ## Full local kind setup
.PHONY: local-down-kind
local-down-kind: uninstall-hyperfleet uninstall-tracing uninstall-grafana uninstall-maestro delete-kind-cluster ## Tear down kind stack and delete cluster
# GKE targets
.PHONY: local-up-gcp
local-up-gcp: install-terraform get-credentials install-priority-classes install-maestro-all maybe-install-grafana maybe-install-tracing install-hyperfleet ## Full gke setup
.PHONY: local-down-gcp
local-down-gcp: get-credentials uninstall-hyperfleet uninstall-tracing uninstall-grafana uninstall-maestro destroy-terraform ## Tear down gke stack and destroy terraform