Skip to content

Commit 786e2da

Browse files
mjudeikisclaude
andauthored
feat(infrastructure): CRD-driven operator (kro helm lifecycle + serve) (#331)
* feat(infrastructure): CRD-driven operator (kro helm lifecycle + serve) Add an InfrastructureProvider operator: one CR declares the kro + provider image versions and two kubeconfig Secret refs; the operator continuously bootstraps the provider kcp workspace, lifecycles the kro Helm release via the helm CLI (multicluster fork chart + image, kcp-apiexport mode, kind hostAliases + self-cluster patches behind env flags), seeds kro's kcp-kubeconfig, and owns the provider serve Deployment. - apis/v1alpha1: InfrastructureProvider CRD (+ generated deepcopy/CRD YAML) - operator/: controller-runtime reconciler, helm exec, serve Deployment, bootstrap (shared with the env-driven operator subcommand), kind dev patches - main.go: `operator` (env-driven) + `controller` (CRD) subcommands; serveWithConfig extracted; loadControllerConfig honors INFRASTRUCTURE_WORKSPACE_PATH - install/operatorseed.go: seed kro from a provided kubeconfig (no SA mint) - deploy/chart: operator Deployment + RBAC + CRD + Secrets + CR (operator.enabled), legacy serve/Service gated off in operator mode - Dockerfile: bundle the helm CLI for the operator pod - Makefile + Tiltfile.cluster: operator dev flow (controller host binary; replaces kro-mgmt-up + infrastructure-init), --validate=false on register - README: operator deploy section Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(kuery): bump provider to 788efce Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f2063d2 commit 786e2da

29 files changed

Lines changed: 2648 additions & 121 deletions

Makefile

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -828,10 +828,62 @@ run-provider-infrastructure: build-infrastructure-provider ## Run the infrastruc
828828
KEDGE_HUB_INSECURE=true \
829829
KEDGE_PROVIDER_NAME=infrastructure \
830830
KEDGE_DEV_ALLOW_TENANT_QUERY=true \
831+
INFRASTRUCTURE_WORKSPACE_PATH=$${INFRASTRUCTURE_WORKSPACE_PATH:-$(INFRASTRUCTURE_WORKSPACE_PATH)} \
831832
KRO_KUBECONFIG=$${KRO_KUBECONFIG:-$$( [ -f "$(KRO_KIND_KUBECONFIG)" ] && echo "$(KRO_KIND_KUBECONFIG)" )} \
832833
INFRASTRUCTURE_KUBECONFIG=$${INFRASTRUCTURE_KUBECONFIG:-$$( [ -f "$(INFRASTRUCTURE_RUNTIME_KUBECONFIG)" ] && echo "$(INFRASTRUCTURE_RUNTIME_KUBECONFIG)" )} \
833834
$(BINDIR)/infrastructure-provider
834835

836+
run-provider-infrastructure-operator: build-infrastructure-provider ## Run the infrastructure provider in OPERATOR mode (bootstrap reconcile + serve from a provider + runtime kubeconfig)
837+
@echo "Starting infrastructure provider (operator) on :$(KROMC_PORT)"
838+
@echo " hub: $(KROMC_HUB_URL)"
839+
@echo " provider: $${INFRASTRUCTURE_PROVIDER_KUBECONFIG:-$(KROMC_KCP_KUBECONFIG)} (kcp)"
840+
@echo " runtime: $${INFRASTRUCTURE_RUNTIME_KUBECONFIG:-$(KRO_KIND_KUBECONFIG)} (kro cluster)"
841+
@echo " ws: $(INFRASTRUCTURE_WORKSPACE_PATH)"
842+
@# The operator needs the provider workspace to already exist — run
843+
@# `make install-provider-infrastructure` (admin-portal onboarding in prod)
844+
@# first. It then reconciles the in-workspace bootstrap and seeds kro itself.
845+
PORT=$(KROMC_PORT) \
846+
KEDGE_HUB_URL=$(KROMC_HUB_URL) \
847+
KEDGE_HUB_TOKEN=$(KROMC_TOKEN) \
848+
KEDGE_HUB_INSECURE=true \
849+
KEDGE_PROVIDER_NAME=infrastructure \
850+
KEDGE_DEV_ALLOW_TENANT_QUERY=true \
851+
INFRASTRUCTURE_WORKSPACE_PATH=$(INFRASTRUCTURE_WORKSPACE_PATH) \
852+
INFRASTRUCTURE_PROVIDER_KUBECONFIG=$${INFRASTRUCTURE_PROVIDER_KUBECONFIG:-$(KROMC_KCP_KUBECONFIG)} \
853+
INFRASTRUCTURE_RUNTIME_KUBECONFIG=$${INFRASTRUCTURE_RUNTIME_KUBECONFIG:-$$( [ -f "$(KRO_KIND_KUBECONFIG)" ] && echo "$(KRO_KIND_KUBECONFIG)" )} \
854+
$(BINDIR)/infrastructure-provider operator
855+
856+
# ── CRD-driven operator (controller) dev flow ───────────────────────────────
857+
# Replaces kro-mgmt-up + infrastructure-init: one host-binary controller that
858+
# bootstraps the workspace, helm-installs kro (with the kind hostAliases +
859+
# self-cluster patches), and (skip-serve in dev) leaves serve to the host binary.
860+
INFRA_OPERATOR_NS ?= kedge-infrastructure-operator
861+
INFRA_OPERATOR_PROVIDER_KC ?= $(KROMC_KCP_KUBECONFIG)
862+
INFRA_OPERATOR_RUNTIME_KC ?= $(KRO_KIND_KUBECONFIG)
863+
INFRA_OPERATOR_KIND_NAME ?= $(KRO_KIND_NAME)
864+
INFRA_OPERATOR_SELF_KC ?= $(KCP_DATA_DIR)/kro-self.kubeconfig
865+
INFRA_OPERATOR_HOSTALIASES_IP ?= 10.96.2.2
866+
INFRA_OPERATOR_HOSTALIASES_NAMES ?= kcp.localhost,root.kcp.localhost,theseus.kcp.localhost
867+
INFRA_OPERATOR_CRD ?= providers/infrastructure/config/crds/infrastructure.kedge.faros.sh_infrastructureproviders.yaml
868+
869+
run-provider-infrastructure-controller: build-infrastructure-provider ## Apply the operator CRD/Secrets/CR into the runtime cluster and run the controller (dev)
870+
@echo "Applying operator CRD + Secrets + CR into runtime cluster ($(INFRA_OPERATOR_RUNTIME_KC))"
871+
KUBECONFIG=$(INFRA_OPERATOR_RUNTIME_KC) kubectl apply -f $(INFRA_OPERATOR_CRD)
872+
KUBECONFIG=$(INFRA_OPERATOR_RUNTIME_KC) kubectl create namespace $(INFRA_OPERATOR_NS) --dry-run=client -o yaml | KUBECONFIG=$(INFRA_OPERATOR_RUNTIME_KC) kubectl apply -f -
873+
KUBECONFIG=$(INFRA_OPERATOR_RUNTIME_KC) kubectl -n $(INFRA_OPERATOR_NS) create secret generic provider-kubeconfig --from-file=kubeconfig=$(INFRA_OPERATOR_PROVIDER_KC) --dry-run=client -o yaml | KUBECONFIG=$(INFRA_OPERATOR_RUNTIME_KC) kubectl apply -f -
874+
KUBECONFIG=$(INFRA_OPERATOR_RUNTIME_KC) kubectl -n $(INFRA_OPERATOR_NS) create secret generic runtime-kubeconfig --from-file=kubeconfig=$(INFRA_OPERATOR_RUNTIME_KC) --dry-run=client -o yaml | KUBECONFIG=$(INFRA_OPERATOR_RUNTIME_KC) kubectl apply -f -
875+
@# kind-internal kubeconfig for the kro local-runtime self-member.
876+
kind get kubeconfig --internal --name $(INFRA_OPERATOR_KIND_NAME) > $(INFRA_OPERATOR_SELF_KC)
877+
@printf 'apiVersion: infrastructure.kedge.faros.sh/v1alpha1\nkind: InfrastructureProvider\nmetadata:\n name: infrastructure\n namespace: %s\nspec:\n providerWorkspace: %s\n providerKubeconfigSecret:\n name: provider-kubeconfig\n runtimeKubeconfigSecret:\n name: runtime-kubeconfig\n kro:\n chart: %s\n version: %s\n image:\n repository: %s\n tag: %s\n provider:\n image:\n repository: ghcr.io/faroshq/kedge-infrastructure-provider\n tag: dev\n' "$(INFRA_OPERATOR_NS)" "$(INFRASTRUCTURE_WORKSPACE_PATH)" "$(KRO_CHART)" "$(KRO_CHART_VERSION)" "$(KRO_IMAGE_REPO)" "$(KRO_IMAGE_TAG)" | KUBECONFIG=$(INFRA_OPERATOR_RUNTIME_KC) kubectl apply -f -
878+
@echo "Running infrastructure operator controller (KUBECONFIG=$(INFRA_OPERATOR_RUNTIME_KC), skip-serve)"
879+
KUBECONFIG=$(INFRA_OPERATOR_RUNTIME_KC) \
880+
INFRASTRUCTURE_WORKSPACE_PATH=$(INFRASTRUCTURE_WORKSPACE_PATH) \
881+
INFRASTRUCTURE_OPERATOR_SKIP_SERVE=true \
882+
INFRASTRUCTURE_KRO_HOSTALIASES_IP=$(INFRA_OPERATOR_HOSTALIASES_IP) \
883+
INFRASTRUCTURE_KRO_HOSTALIASES_NAMES=$(INFRA_OPERATOR_HOSTALIASES_NAMES) \
884+
INFRASTRUCTURE_KRO_SELF_CLUSTER_KUBECONFIG=$(INFRA_OPERATOR_SELF_KC) \
885+
$(BINDIR)/infrastructure-provider controller
886+
835887
## Run the App Studio provider binary locally. Mirrors the other external
836888
## providers so the heartbeat path is consistent and the UI runs on :8085.
837889
app-studio-db-up: ## Start/reuse local Postgres for App Studio message history (skips when APP_STUDIO_DATABASE_URL or in-memory mode is set)
@@ -985,7 +1037,7 @@ install-provider-infrastructure: ## Apply infrastructure Provider + CatalogEntry
9851037
kubectl --kubeconfig=$(KROMC_KCP_KUBECONFIG) \
9861038
--server=$(KROMC_KCP_SERVER)/clusters/root:kedge:system:providers \
9871039
--insecure-skip-tls-verify \
988-
apply -f $(KROMC_PROVIDER_MANIFEST) -f $(KROMC_MANIFEST)
1040+
apply --validate=false -f $(KROMC_PROVIDER_MANIFEST) -f $(KROMC_MANIFEST)
9891041

9901042
## Delete the infrastructure CatalogEntry + Provider (Provider delete triggers
9911043
## full teardown of the sub-workspace via the controller's finalizer).

Tiltfile.cluster

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,8 @@ local_resource(
639639
# kro reaches kcp via in-cluster Service DNS — no cross-kind networking.
640640
# The infrastructure provider on the host still reaches kro via the kind
641641
# cluster's host-published apiserver (KIND_HOST_KUBECONFIG).
642+
# kro-mgmt-up is now MANUAL: the infrastructure-operator helm-installs +
643+
# patches kro itself. Kept as a fallback / for poking kro without the operator.
642644
local_resource(
643645
'kro-mgmt-up',
644646
cmd=('KRO_TARGET_KUBECONFIG={kc} KRO_TARGET_KIND_NAME={name} ' +
@@ -647,6 +649,8 @@ local_resource(
647649
deps=[
648650
'providers/infrastructure/examples/rgds',
649651
],
652+
trigger_mode=TRIGGER_MODE_MANUAL,
653+
auto_init=False,
650654
labels=['providers-kro'],
651655
)
652656

@@ -662,12 +666,14 @@ local_resource(
662666
local_resource(
663667
'infrastructure',
664668
cmd='make build-infrastructure-provider',
665-
# Point KRO_KUBECONFIG at the same kind cluster's host kubeconfig (where
666-
# kro is now installed). The provider's make target already falls back to
667-
# this when KRO_KUBECONFIG is set in the env, so the default kedge-kro
668-
# path remains untouched for the embedded-kcp Tiltfile.
669-
serve_cmd='KRO_KUBECONFIG={kc} make run-provider-infrastructure'.format(
670-
kc=KIND_HOST_KUBECONFIG),
669+
# Serve (host binary). The operator does bootstrap + kro; serve just runs
670+
# with the admin kubeconfig retargeted to the provider workspace
671+
# (INFRASTRUCTURE_WORKSPACE_PATH), so no init-minted kubeconfig is needed.
672+
# KRO_KUBECONFIG points at the kind cluster's host kubeconfig (where kro is).
673+
serve_cmd=('KRO_KUBECONFIG={kro} INFRASTRUCTURE_KUBECONFIG={kc} ' +
674+
'INFRASTRUCTURE_WORKSPACE_PATH=root:kedge:providers:infrastructure ' +
675+
'make run-provider-infrastructure').format(
676+
kro=KIND_HOST_KUBECONFIG, kc=KCP_ADMIN_KUBECONFIG),
671677
deps=[
672678
'providers/infrastructure/main.go',
673679
'providers/infrastructure/heartbeat.go',
@@ -685,19 +691,10 @@ local_resource(
685691
'providers/infrastructure/portal/package.json',
686692
'providers/infrastructure/go.mod',
687693
'providers/infrastructure/go.sum',
688-
# Restart whenever init writes/updates the runtime kubeconfig:
689-
# the controller manager only starts when INFRASTRUCTURE_KUBECONFIG
690-
# resolves to a real file (see the Makefile target). Without this
691-
# watch, the provider that booted before `infrastructure-init`
692-
# keeps running with controllers disabled, and per-template
693-
# APIResourceSchemas never get added to the APIExport.
694-
'.kcp/infrastructure-runtime.kubeconfig',
695694
],
696-
# kedge-hub for CatalogEntry registration target, kro-mgmt-up for the
697-
# backend cluster the catalog reads from. Both must be green before
698-
# the provider starts; otherwise it boots in stub mode which is fine
699-
# but confusing for the dev who just ran `tilt up`.
700-
resource_deps=['kedge-hub', 'kro-mgmt-up'],
695+
# kedge-hub for the CatalogEntry registration target; infrastructure-operator
696+
# for the workspace bootstrap + kro install the serve process depends on.
697+
resource_deps=['kedge-hub', 'infrastructure-operator'],
701698
readiness_probe=probe(
702699
period_secs=5,
703700
http_get=http_get_action(port=8082, path='/healthz'),
@@ -752,6 +749,35 @@ local_resource(
752749
labels=['providers-kro'],
753750
)
754751

752+
# --- infrastructure-operator (CRD-driven) ---
753+
# One controller that REPLACES kro-mgmt-up + infrastructure-init: it applies the
754+
# InfrastructureProvider CRD/Secrets/CR into the kind cluster, then reconciles —
755+
# bootstrapping the provider workspace, helm-installing kro (with the kind
756+
# hostAliases + self-cluster patches), and seeding kro's kcp-kubeconfig. Serve
757+
# still runs as the host-binary `infrastructure` resource (skip-serve) for fast
758+
# iteration. Order: infrastructure-register (creates the workspace) →
759+
# infrastructure-operator → infrastructure (serve).
760+
local_resource(
761+
'infrastructure-operator',
762+
cmd='make build-infrastructure-provider',
763+
serve_cmd=('INFRA_OPERATOR_PROVIDER_KC={kc} INFRA_OPERATOR_RUNTIME_KC={kro} ' +
764+
'INFRA_OPERATOR_KIND_NAME={name} ' +
765+
'make run-provider-infrastructure-controller').format(
766+
kc=KCP_ADMIN_KUBECONFIG, kro=KIND_HOST_KUBECONFIG, name=cluster_name),
767+
deps=[
768+
'providers/infrastructure/main.go',
769+
'providers/infrastructure/operator.go',
770+
'providers/infrastructure/operator',
771+
'providers/infrastructure/apis',
772+
'providers/infrastructure/init_cmd.go',
773+
'providers/infrastructure/install',
774+
'providers/infrastructure/go.mod',
775+
'providers/infrastructure/go.sum',
776+
],
777+
resource_deps=['kedge-hub', 'infrastructure-register'],
778+
labels=['providers-kro'],
779+
)
780+
755781
# --- providers-code (git repository management) ---
756782
# Host binary on :8083, same shape as quickstart/infrastructure. register/init/
757783
# unregister override KROMC_KCP_KUBECONFIG / KROMC_KCP_SERVER to the in-cluster

providers/infrastructure/Dockerfile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,26 @@ COPY . ./
1919
COPY --from=portal /portal/dist ./portal/dist
2020
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/infrastructure-provider .
2121

22+
# 2b. Fetch the helm CLI. The operator (`controller` subcommand) shells out to
23+
# helm to install/upgrade the kro release, so the runtime image needs it.
24+
FROM alpine:3.20 AS helm
25+
ARG TARGETARCH
26+
ARG HELM_VERSION=v3.16.4
27+
RUN apk add --no-cache curl tar && \
28+
curl -fsSL "https://get.helm.sh/helm-${HELM_VERSION}-linux-${TARGETARCH}.tar.gz" | tar -xz && \
29+
install -m 0755 "linux-${TARGETARCH}/helm" /helm
30+
2231
# 3. Minimal runtime image.
2332
FROM gcr.io/distroless/static:nonroot
2433
COPY --from=build /out/infrastructure-provider /infrastructure-provider
34+
COPY --from=helm /helm /usr/local/bin/helm
2535
EXPOSE 8081
2636
ENV PORT=8081
37+
# helm needs writable cache/config/data dirs; point them at the world-writable
38+
# /tmp so the operator can run helm as nonroot. The operator also writes the
39+
# runtime kubeconfig to a /tmp temp file (os.CreateTemp).
40+
ENV HELM_CACHE_HOME=/tmp/helm/cache \
41+
HELM_CONFIG_HOME=/tmp/helm/config \
42+
HELM_DATA_HOME=/tmp/helm/data
2743
USER nonroot:nonroot
2844
ENTRYPOINT ["/infrastructure-provider"]

0 commit comments

Comments
 (0)