-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMakefile
More file actions
276 lines (242 loc) · 12.8 KB
/
Copy pathMakefile
File metadata and controls
276 lines (242 loc) · 12.8 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
SHELL := /bin/bash
.DEFAULT_GOAL := help
GO := go
BINARY := bin/orchestrator
PKG := github.com/leannodes/leannodes
VERSION ?= $(shell git describe --tags --dirty --always 2>/dev/null || echo dev)
# Hardened ldflags:
# -s strip symbol table
# -w strip DWARF debug info
# -buildid= remove Go build-ID fingerprint (prevents correlation across builds)
# -X inject the version string at link time
LDFLAGS := -s -w -buildid= -X $(PKG)/internal/config.buildVersion=$(VERSION)
# -trimpath strips absolute filesystem paths ($(HOME)/workspace/...) from the binary.
GOFLAGS := -trimpath
# CGO_ENABLED=0 produces a single static binary with no dynamic linker
# symbols — `strings` + libc leak surface disappears, and the binary
# runs unchanged on scratch/distroless images.
BUILD_ENV := CGO_ENABLED=0
# Garble (mvdan.cc/garble) — Go-specific obfuscator. Install with:
# go install mvdan.cc/garble@latest
# Flags:
# -literals encrypt string + numeric constants at build time
# -tiny strip line numbers, panic messages, func names (stack
# traces become unreadable — use symbolicated dev builds
# for debugging; ship the tiny one)
# -seed=random rotate per release so a reverser's prior notes don't
# carry over to the next build
GARBLE := garble
GARBLE_FLAGS := -literals -tiny -seed=random
COVER := coverage.txt
.PHONY: help
help: ## Show this help
@awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z0-9_.-]+:.*##/ { printf "\033[36m%-20s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST)
.PHONY: deps
deps: ## Download and tidy Go modules
$(GO) mod download
$(GO) mod tidy
.PHONY: ui
ui: ## Build the React+Vite UI and sync into internal/ui/dist for go:embed
cd web && npm ci --no-audit --no-fund && npm run build
rm -rf internal/ui/dist
mkdir -p internal/ui/dist
cp -R web/dist/. internal/ui/dist/
# Keep .gitkeep present so a fresh clone that runs `make build-go`
# without first running `make ui` still has a non-empty dist/ for go:embed.
touch internal/ui/dist/.gitkeep
.PHONY: build
build: ui ## Build the orchestrator binary — hardened (trimpath, stripped, static)
$(BUILD_ENV) $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BINARY) ./cmd/orchestrator
.PHONY: build-go
build-go: ## Build the Go binary only — hardened (skip UI; use when iterating on backend)
$(BUILD_ENV) $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BINARY) ./cmd/orchestrator
.PHONY: build-release
build-release: ui ## Build an obfuscated release binary via garble (hardened + literal/symbol obfuscation)
@command -v $(GARBLE) >/dev/null 2>&1 || { \
echo " garble not found. Install it with:"; \
echo " go install mvdan.cc/garble@latest"; \
exit 1; \
}
$(BUILD_ENV) $(GARBLE) $(GARBLE_FLAGS) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BINARY) ./cmd/orchestrator
@echo " built obfuscated binary: $(BINARY)"
@echo " NOTE: stack traces are unreadable. Keep a symbolicated 'make build' binary internally."
.PHONY: test
test: ## Run unit tests with race detector
$(GO) test -race -count=1 ./...
.PHONY: ddb-local-up
ddb-local-up: ## Start amazon/dynamodb-local on :8000 with a persistent volume
@# Persistent: a named docker volume + -sharedDb + -dbPath keep flows
@# alive across `docker rm -f`. Without this, the container defaulted
@# to in-memory mode and tearing it down wiped every flow — burned us
@# once, belt-and-braces now.
@docker volume inspect leannodes-ddb-data >/dev/null 2>&1 || docker volume create leannodes-ddb-data >/dev/null
@if docker ps --format '{{.Names}}' | grep -q '^leannodes-ddb-local$$'; then \
echo " dynamodb-local already running"; \
elif docker ps -a --format '{{.Names}}' | grep -q '^leannodes-ddb-local$$'; then \
docker start leannodes-ddb-local >/dev/null; \
echo " restarted existing leannodes-ddb-local"; \
else \
docker run -d --name leannodes-ddb-local \
-p 8000:8000 \
-v leannodes-ddb-data:/ddb-data \
--user root \
-w /ddb-data \
amazon/dynamodb-local \
-jar /home/dynamodblocal/DynamoDBLocal.jar -sharedDb -dbPath /ddb-data >/dev/null; \
echo " created leannodes-ddb-local (persistent volume: leannodes-ddb-data)"; \
fi
@for i in 1 2 3 4 5 6 7 8 9 10; do \
curl -s -o /dev/null -w '%{http_code}' http://localhost:8000/ | grep -q '^[0-9]' && exit 0; \
sleep 0.2; \
done; echo " dynamodb-local did not respond"; exit 1
.PHONY: ddb-local-down
ddb-local-down: ## Stop amazon/dynamodb-local (data survives in the named volume)
@# Keeps the named volume 'leannodes-ddb-data' intact so the next
@# `make ddb-local-up` restores every flow. Use `ddb-local-wipe`
@# if you really want to start fresh.
@docker stop leannodes-ddb-local >/dev/null 2>&1 || true
@docker rm leannodes-ddb-local >/dev/null 2>&1 || true
.PHONY: ddb-local-wipe
ddb-local-wipe: ## DESTRUCTIVE — delete the persistent DDB volume (all local flows gone)
@# Named + verbose so nobody runs this by muscle memory. Mirrors
@# the "drop table if exists" pattern from SQL tooling: explicit
@# intent, not a side effect of a cleanup command.
@echo "This will permanently delete every flow + user + audit row stored in the local DDB volume 'leannodes-ddb-data'."
@read -p "Type 'wipe' to confirm: " ans && [ "$$ans" = "wipe" ] || (echo "aborted"; exit 1)
@docker stop leannodes-ddb-local >/dev/null 2>&1 || true
@docker rm leannodes-ddb-local >/dev/null 2>&1 || true
@docker volume rm leannodes-ddb-data >/dev/null 2>&1 || true
@echo " leannodes-ddb-data volume removed. 'make ddb-local-up' will start fresh."
.PHONY: ddb-local-backup
ddb-local-backup: ## Snapshot the persistent DDB volume to /tmp/leannodes-ddb-backup-<ts>.tar.gz
@# Tarballs the volume's contents. Point-in-time snapshot you can
@# stash before a risky experiment and `ddb-local-restore` later.
@ts=$$(date -u +%Y%m%dT%H%M%SZ); \
out="/tmp/leannodes-ddb-backup-$$ts.tar.gz"; \
docker run --rm -v leannodes-ddb-data:/data -v /tmp:/backup alpine \
sh -c "cd /data && tar czf /backup/leannodes-ddb-backup-$$ts.tar.gz ." >/dev/null; \
echo " snapshot written: $$out"
.PHONY: ddb-local-restore
ddb-local-restore: ## Restore a ddb-local backup: FROM=/tmp/leannodes-ddb-backup-<ts>.tar.gz
@if [ -z "$(FROM)" ]; then echo " usage: make ddb-local-restore FROM=/tmp/leannodes-ddb-backup-<ts>.tar.gz"; exit 1; fi
@if [ ! -f "$(FROM)" ]; then echo " backup file not found: $(FROM)"; exit 1; fi
@echo "This will OVERWRITE the current leannodes-ddb-data volume contents with the backup."
@read -p "Type 'restore' to confirm: " ans && [ "$$ans" = "restore" ] || (echo "aborted"; exit 1)
@docker stop leannodes-ddb-local >/dev/null 2>&1 || true
@docker volume inspect leannodes-ddb-data >/dev/null 2>&1 || docker volume create leannodes-ddb-data >/dev/null
@docker run --rm -v leannodes-ddb-data:/data -v $(shell dirname $(FROM)):/backup alpine \
sh -c "cd /data && rm -rf ./* ./.[!.]* 2>/dev/null; tar xzf /backup/$(shell basename $(FROM))"
@echo " restored from $(FROM). Start with 'make ddb-local-up'."
.PHONY: test-integration
test-integration: ddb-local-up ## Run integration tests against dynamodb-local
$(GO) test -tags=integration -count=1 -race ./internal/store/...
.PHONY: test-k8s
test-k8s: ## Run k8s integration tests against the live kind cluster
$(GO) test -tags=integration_k8s -count=1 -race ./internal/k8s/...
.PHONY: test-ui
test-ui: build ## Run Playwright end-to-end UI tests (requires kind + istio + ddb-local)
cd web && npm run test:ui
.PHONY: kind-up
kind-up: ## Create local kind cluster `leannodes-dev` + deploy 6-workload demo fixture
@if kind get clusters 2>/dev/null | /usr/bin/grep -q '^leannodes-dev$$'; then \
echo " kind cluster leannodes-dev already exists"; \
else \
kind create cluster --name leannodes-dev --wait 120s; \
fi
kubectl --context kind-leannodes-dev apply -f scripts/fixtures/demo-workloads.yaml
kubectl --context kind-leannodes-dev apply -f scripts/fixtures/demo-altns-workloads.yaml
@echo " kind cluster ready; context=kind-leannodes-dev"
.PHONY: istio-up
istio-up: kind-up ## Install Istio into the kind cluster with the leannodes-authz provider
istioctl install --context kind-leannodes-dev -f scripts/fixtures/istio-operator.yaml -y
@echo " istio installed; ext_authz provider leannodes-authz registered"
.PHONY: keda-up
keda-up: kind-up ## Install KEDA into the kind cluster (slice C2.1 coexistence tests)
@if kubectl --context kind-leannodes-dev get crd scaledobjects.keda.sh >/dev/null 2>&1; then \
echo " keda already installed (scaledobjects.keda.sh CRD present)"; \
else \
kubectl --context kind-leannodes-dev apply --server-side -f https://github.com/kedacore/keda/releases/download/v2.15.1/keda-2.15.1.yaml; \
kubectl --context kind-leannodes-dev -n keda wait --for=condition=available --timeout=120s deploy/keda-operator; \
echo " keda installed"; \
fi
kubectl --context kind-leannodes-dev apply -f scripts/fixtures/demo-keda.yaml
.PHONY: kind-down
kind-down: ## Delete the local kind cluster
kind delete cluster --name leannodes-dev
.PHONY: test-functional
test-functional: build-go ddb-local-up ## Run functional tests (all 27 suites)
scripts/functional-test-multi-instance.sh
scripts/functional-test-job-node.sh
scripts/functional-test-keda-coexistence.sh
scripts/functional-test-karpenter-pending.sh
scripts/functional-test-auth-static.sh
scripts/functional-test-k8s-browser.sh
scripts/functional-test-istio-authz.sh
scripts/functional-test-extauthz-grpc.sh
scripts/functional-test-coldstart-e2e.sh
scripts/functional-test-idle-reconciler.sh
scripts/functional-test-coldstart-page.sh
scripts/functional-test-execution-stream.sh
scripts/functional-test-external-probes.sh
scripts/functional-test-pin-routing.sh
scripts/functional-test-execution-history.sh
scripts/functional-test-shared-workload-drain.sh
scripts/functional-test-manual-warm-drain.sh
scripts/functional-test-live-status.sh
scripts/functional-test-health-recover.sh
scripts/functional-test-endpoint-readiness.sh
scripts/functional-test-stats-savings.sh
scripts/functional-test-metrics.sh
scripts/functional-test-leader-election.sh
scripts/functional-test-audit-log.sh
scripts/functional-test-schedules.sh
scripts/functional-test-schedule-proactive-warm.sh
scripts/functional-test-multi-namespace.sh
scripts/functional-test-disable-flow.sh
scripts/functional-test-secret-redaction.sh
scripts/functional-test-audit-hash-chain.sh
scripts/functional-test-cli.sh
.PHONY: test-short
test-short: ## Run unit tests (no race, faster feedback)
$(GO) test -count=1 -short ./...
.PHONY: cover
cover: ## Run tests and generate HTML coverage report
$(GO) test -race -count=1 -covermode=atomic -coverprofile=$(COVER) ./...
$(GO) tool cover -html=$(COVER) -o coverage.html
@echo "coverage.html generated"
.PHONY: vet
vet: ## go vet across all packages
$(GO) vet ./...
.PHONY: fmt
fmt: ## gofmt all Go files
gofmt -w -s .
.PHONY: lint
lint: vet ## Alias for vet (will expand when golangci-lint is added)
.PHONY: run
run: build ## Run the orchestrator locally
LEANNODES_ENV=development LEANNODES_LOG_LEVEL=debug LEANNODES_IN_CLUSTER=false ./$(BINARY)
.PHONY: clean
clean: ## Remove build artifacts
rm -rf bin/ $(COVER) coverage.html
.PHONY: docker
docker: ## Build the Docker image (tag: leannodes:$(VERSION))
docker build -t leannodes:$(VERSION) -f Dockerfile .
.PHONY: cli
cli: ## Build the leannodes CLI (output: bin/leannodes)
$(BUILD_ENV) $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS) -X main.cliVersion=$(VERSION)" -o bin/leannodes ./cmd/leannodes-cli
.PHONY: sbom
sbom: ## Generate SBOMs (SPDX + CycloneDX) for the current Docker image — requires `syft`
@command -v syft >/dev/null 2>&1 || { echo " syft not installed; install via: brew install syft (or see https://github.com/anchore/syft)"; exit 1; }
@docker image inspect leannodes:$(VERSION) >/dev/null 2>&1 || { echo " image leannodes:$(VERSION) not found locally; run 'make docker' first"; exit 1; }
@mkdir -p bin/sbom
syft packages leannodes:$(VERSION) -o spdx-json=bin/sbom/leannodes-$(VERSION).spdx.json
syft packages leannodes:$(VERSION) -o cyclonedx-json=bin/sbom/leannodes-$(VERSION).cdx.json
@echo " wrote bin/sbom/leannodes-$(VERSION).spdx.json + .cdx.json"
.PHONY: verify-image
verify-image: ## Verify the signature + SBOM of a released image. Usage: make verify-image IMAGE=ghcr.io/.../leannodes:vX.Y.Z OWNER=...
@[ -n "$(IMAGE)" ] || { echo " usage: make verify-image IMAGE=ghcr.io/<owner>/leannodes:<tag> OWNER=<owner>"; exit 1; }
@[ -n "$(OWNER)" ] || { echo " OWNER is required (GitHub org/user that owns the release)"; exit 1; }
@command -v cosign >/dev/null 2>&1 || { echo " cosign not installed; install via: brew install cosign (or see https://docs.sigstore.dev/cosign/installation)"; exit 1; }
cosign verify $(IMAGE) \
--certificate-identity-regexp='https://github.com/$(OWNER)/leannodes/\.github/workflows/release\.yml@refs/tags/.*' \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com