Skip to content

Commit fedd247

Browse files
mjudeikisclaude
andauthored
Rework provider bootstrap: self-register CatalogEntry via init container; per-provider release versioning (#320)
* Rework provider bootstrap * ci: take the Tilt E2E job off automatic CI runs The multi-shard Tilt stack over-subscribes the 2-vCPU GitHub-hosted runner (root-kcp won't schedule, shards/proxies crash-loop on probe timeouts), so the job reliably fails on insufficient resources rather than on the code under test. Comment out the pull_request/push triggers; it stays runnable on demand via workflow_dispatch. Re-enable once we have bigger workers. 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 37eb6a9 commit fedd247

22 files changed

Lines changed: 500 additions & 203 deletions

File tree

.github/workflows/tilt-e2e.yaml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,17 @@ name: Tilt E2E
1414
# internal retries) if the stack isn't fully settled.
1515

1616
on:
17-
pull_request:
18-
branches: [main]
19-
push:
20-
branches: [main]
17+
# DISABLED on CI: the multi-shard Tilt stack over-subscribes the 2-vCPU
18+
# GitHub-hosted runner — root-kcp won't schedule and the shards/proxies
19+
# crash-loop on probe timeouts, so this job fails on insufficient resources
20+
# rather than on the code under test. Re-enable the pull_request/push
21+
# triggers once we have bigger (self-hosted / larger-runner) workers with
22+
# enough CPU+memory to stand up the full operator topology. Until then it
23+
# stays runnable on demand via the Actions "Run workflow" button below.
24+
# pull_request:
25+
# branches: [main]
26+
# push:
27+
# branches: [main]
2128
workflow_dispatch:
2229

2330
permissions:

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ providers/kuery/portal/dist/
5757
# Data dirs
5858
data/
5959
*.kubeconfig
60+
# Downloaded kubeconfigs that don't end in .kubeconfig (e.g. code-kubeconfig (2).yaml)
61+
*kubeconfig*.yaml
6062

6163
# Dev tools and certs
6264
hack/tools/

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,9 +1191,9 @@ helm-undeploy-provider-infrastructure: ## (experimental) helm uninstall the infr
11911191
KRO_KIND_NAME ?= kedge-kro
11921192
KRO_KIND_KUBECONFIG ?= $(CURDIR)/.kedge-kro.kubeconfig
11931193
KRO_CHART ?= oci://ghcr.io/faroshq/kro-multicluster/charts/kro/kro
1194-
KRO_CHART_VERSION ?= v0.0.1-mc.6
1194+
KRO_CHART_VERSION ?= v0.0.1-mc.7
11951195
KRO_IMAGE_REPO ?= ghcr.io/faroshq/kro-multicluster/kro
1196-
KRO_IMAGE_TAG ?= v0.0.1-mc.6
1196+
KRO_IMAGE_TAG ?= v0.0.1-mc.7
11971197
KRO_NAMESPACE ?= kro-system
11981198
KRO_SEED_DIR ?= providers/infrastructure/examples/rgds
11991199

Tiltfile.cluster

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ cluster_name = 'kcp-tilt'
5353
# silently shadow any later edit to the default here. The putenv is one-way:
5454
# parent (this file) → child (the included Tiltfile.static, which reads the env).
5555
kcp_image_repo = cfg.get('kcp-image-repo', '')
56-
kcp_image_tag = cfg.get('kcp-image-tag', 'fb8629abb')
56+
kcp_image_tag = cfg.get('kcp-image-tag', 'v0.32.0')
5757
# Always export (even empty) so an explicit '' override clears any inherited env.
5858
os.putenv('KCP_IMAGE_REPO', kcp_image_repo)
5959
os.putenv('KCP_IMAGE_TAG', kcp_image_tag)
Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,55 @@
11
{{- if .Values.catalogEntry.enabled -}}
2-
apiVersion: providers.kedge.faros.sh/v1alpha1
3-
kind: CatalogEntry
2+
# CatalogEntry registers this provider with the kedge hub for routing + the
3+
# portal Enable flow. It is a kcp resource (providers.kedge.faros.sh/v1alpha1),
4+
# so it MUST NOT be applied to the hosting cluster where this chart installs.
5+
# Instead the chart renders it into a ConfigMap that the init container mounts
6+
# and applies into the provider workspace via the provider kubeconfig
7+
# (KEDGE_CATALOGENTRY_FILE -> sdkinstall.Bootstrap). The hub watches
8+
# CatalogEntries in every workspace bound to providers.kedge.faros.sh, including
9+
# the provider's own, so a self-registered entry is seen.
10+
#
11+
# It only REFERENCES the APIExport (apiExport.name); the APIExport, its
12+
# APIResourceSchemas, the APIExportEndpointSlice, and the bind grant are created
13+
# by the same init container. The hub no longer provisions anything from this
14+
# CatalogEntry.
15+
apiVersion: v1
16+
kind: ConfigMap
417
metadata:
5-
name: app-studio
6-
annotations:
7-
providers.kedge.faros.sh/builtin: "false"
18+
name: {{ include "appstudio.fullname" . }}-catalogentry
819
labels:
920
{{- include "appstudio.labels" . | nindent 4 }}
10-
spec:
11-
displayName: "App Studio"
12-
description: "Persistent AI project workspace for planning, memory, and chat."
13-
vendor: "kedge"
14-
version: {{ .Chart.AppVersion | quote }}
15-
category: "AI"
16-
iconURL: "/ui/providers/app-studio/icon.svg"
17-
dependencies:
18-
- name: code
19-
ui:
20-
url: {{ default (printf "http://%s.%s.svc.cluster.local:%v" (include "appstudio.fullname" .) .Release.Namespace .Values.service.port) .Values.catalogEntry.uiURL | quote }}
21-
indexPath: "/"
22-
backend:
23-
# The hub backend proxy forwards /services/providers/app-studio/* here,
24-
# injecting the verified X-Kedge-Tenant/X-Kedge-User headers and the
25-
# caller's bearer token. The provider serves /api/projects/* + /healthz.
26-
url: {{ default (printf "http://%s.%s.svc.cluster.local:%v" (include "appstudio.fullname" .) .Release.Namespace .Values.service.port) .Values.catalogEntry.backendURL | quote }}
27-
healthPath: "/healthz"
28-
# Reference-only: the APIExport, schemas, slice, and bind grant are created by
29-
# this chart's init container (provider `init`, via the kedge-provider-sdk).
30-
apiExport:
31-
name: "ai.kedge.faros.sh"
32-
permissionClaims:
33-
- resource: secrets
34-
verbs: ["get", "list", "watch", "create", "update", "delete"]
35-
tenantScoped: true
21+
data:
22+
catalogentry.yaml: |
23+
apiVersion: providers.kedge.faros.sh/v1alpha1
24+
kind: CatalogEntry
25+
metadata:
26+
name: app-studio
27+
annotations:
28+
providers.kedge.faros.sh/builtin: "false"
29+
spec:
30+
displayName: "App Studio"
31+
description: "Persistent AI project workspace for planning, memory, and chat."
32+
vendor: "kedge"
33+
version: {{ .Chart.AppVersion | quote }}
34+
category: "AI"
35+
iconURL: "/ui/providers/app-studio/icon.svg"
36+
dependencies:
37+
- name: code
38+
ui:
39+
url: {{ default (printf "http://%s.%s.svc.cluster.local:%v" (include "appstudio.fullname" .) .Release.Namespace .Values.service.port) .Values.catalogEntry.uiURL | quote }}
40+
indexPath: "/"
41+
backend:
42+
# The hub backend proxy forwards /services/providers/app-studio/* here,
43+
# injecting the verified X-Kedge-Tenant/X-Kedge-User headers and the
44+
# caller's bearer token. The provider serves /api/projects/* + /healthz.
45+
url: {{ default (printf "http://%s.%s.svc.cluster.local:%v" (include "appstudio.fullname" .) .Release.Namespace .Values.service.port) .Values.catalogEntry.backendURL | quote }}
46+
healthPath: "/healthz"
47+
# Reference-only: the APIExport, schemas, slice, and bind grant are created by
48+
# this chart's init container (provider `init`, via the kedge-provider-sdk).
49+
apiExport:
50+
name: "ai.kedge.faros.sh"
51+
permissionClaims:
52+
- resource: secrets
53+
verbs: ["get", "list", "watch", "create", "update", "delete"]
54+
tenantScoped: true
3655
{{- end }}

providers/app-studio/deploy/chart/templates/deployment.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,23 @@ spec:
3939
value: /var/run/secrets/kedge/kedge-provider-kubeconfig
4040
- name: KEDGE_SCHEMAS_DIR
4141
value: /etc/kedge/schemas
42+
{{- if .Values.catalogEntry.enabled }}
43+
# The CatalogEntry is a kcp resource, so the chart cannot apply it to
44+
# the hosting cluster. It is rendered into a ConfigMap (mounted below)
45+
# and init self-registers it into the provider workspace via the
46+
# provider kubeconfig.
47+
- name: KEDGE_CATALOGENTRY_FILE
48+
value: /etc/kedge/catalogentry/catalogentry.yaml
49+
{{- end }}
4250
volumeMounts:
4351
- name: kedge-provider-kubeconfig
4452
mountPath: /var/run/secrets/kedge
4553
readOnly: true
54+
{{- if .Values.catalogEntry.enabled }}
55+
- name: catalogentry
56+
mountPath: /etc/kedge/catalogentry
57+
readOnly: true
58+
{{- end }}
4659
containers:
4760
- name: provider
4861
image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}"
@@ -129,6 +142,11 @@ spec:
129142
items:
130143
- key: kubeconfig
131144
path: kedge-provider-kubeconfig
145+
{{- if .Values.catalogEntry.enabled }}
146+
- name: catalogentry
147+
configMap:
148+
name: {{ include "appstudio.fullname" . }}-catalogentry
149+
{{- end }}
132150
- name: project-workspaces
133151
{{- if .Values.workspace.existingClaim }}
134152
persistentVolumeClaim:

providers/app-studio/deploy/chart/values.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ service:
1616
type: ClusterIP
1717
port: 8081
1818

19+
# When true, the chart renders the CatalogEntry (which registers the provider
20+
# with the hub) into a ConfigMap that the init container applies into the
21+
# provider workspace via the provider kubeconfig. The CatalogEntry is a kcp
22+
# resource, so it is NOT applied to the hosting cluster this chart installs into.
23+
# Set to false to manage the CatalogEntry separately (e.g. GitOps with a
24+
# different rollout cadence).
1925
catalogEntry:
2026
enabled: true
2127
uiURL: ""

providers/code/README.md

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,11 @@ Connection — deleting the Connection garbage-collects the Secret.
106106

107107
## Register with the hub
108108

109-
The CatalogEntry is what makes the hub provision the provider's workspace,
110-
schemas, APIExport, and runtime kubeconfig. The Helm chart renders it
111-
(`catalogEntry.enabled=true`), or apply the raw manifest:
109+
The CatalogEntry registers the provider with the hub for routing + the portal
110+
Enable flow. It is a kcp resource, so it lives in the provider workspace — not
111+
the hosting cluster. With `catalogEntry.enabled=true` (default) the chart renders
112+
it into a ConfigMap and the init container self-registers it into the workspace
113+
via the provider kubeconfig; alternatively apply the raw manifest yourself:
112114

113115
```sh
114116
kubectl --kubeconfig kcp-admin.kubeconfig ws use root:kedge:providers
@@ -130,7 +132,8 @@ docker build -t ghcr.io/faroshq/kedge-code-provider:dev providers/code/
130132
## Deploy with Helm
131133

132134
The chart ships the provider Deployment, a ClusterIP Service, the ServiceAccount,
133-
and (optionally) the CatalogEntry. The runtime kubeconfig the controllers need
135+
and (optionally) the CatalogEntry ConfigMap the init container applies to kcp.
136+
The runtime kubeconfig the controllers need
134137
is **minted by the hub** when it reconciles the CatalogEntry and mounted from the
135138
`kedge-provider-kubeconfig` Secret — the volume is `optional`, so the pod serves
136139
portal/MCP/packages reads immediately and the controller manager engages once
@@ -148,8 +151,10 @@ helm install code providers/code/deploy/chart \
148151

149152
### With "Connect with GitHub" (OAuth)
150153

151-
Create a GitHub OAuth App with callback `https://<provider-host>/oauth/github/callback`,
152-
store its client secret in a Secret, then:
154+
Create a GitHub OAuth App, store its client secret in a Secret, then enable the
155+
`githubOAuth.*` block. The portal probes `/services/providers/code/oauth/github/config`
156+
through the hub; once OAuth is enabled and the provider backend is reachable, the
157+
**Connect with GitHub** button appears.
153158

154159
```sh
155160
kubectl -n code create secret generic kedge-code-github-oauth \
@@ -162,14 +167,88 @@ helm install code providers/code/deploy/chart \
162167
--set githubOAuth.enabled=true \
163168
--set githubOAuth.clientId=<oauth-app-client-id> \
164169
--set githubOAuth.clientSecretRef.name=kedge-code-github-oauth \
165-
--set githubOAuth.redirectURL=https://code.example.com/oauth/github/callback \
166-
--set githubOAuth.portalOrigin=https://kedge.example.com
170+
--set githubOAuth.redirectURL=https://<hub-host>/services/providers/code/oauth/github/callback \
171+
--set githubOAuth.portalOrigin=https://<hub-host>
167172
```
168173

169-
The OAuth callback is a top-level redirect from GitHub (no kedge auth), so
170-
`redirectURL` must point at the provider's **own externally-reachable URL**, not
171-
the hub `/services` proxy. `portalOrigin` should be the hub origin so the popup
172-
returns the token only to your portal.
174+
#### Choosing `redirectURL`
175+
176+
GitHub's callback is a **top-level browser redirect with no kedge auth**, so
177+
`redirectURL` must be publicly reachable and forward to the provider's HTTP
178+
backend (`:8083`). It must end in `/callback`; the matching `/start` URL is
179+
derived automatically by swapping `/callback``/start` under the **same host
180+
and path prefix**. Two options:
181+
182+
1. **Reuse the hub ingress (recommended — no extra ingress object):** point at
183+
the hub's existing `/services/providers/code/*` proxy:
184+
```
185+
https://<hub-host>/services/providers/code/oauth/github/callback
186+
```
187+
The proxy forwards these anonymous requests straight to the provider backend,
188+
so the whole flow rides the single hub hostname. Set `portalOrigin` to the
189+
same hub origin.
190+
191+
2. **The provider's own external host:** if you expose the provider directly
192+
(its own ingress/hostname), use:
193+
```
194+
https://code.example.com/oauth/github/callback
195+
```
196+
197+
Whichever you pick, register that **exact** callback URL on the GitHub OAuth App,
198+
and set `portalOrigin` to the hub origin so the popup returns the token only to
199+
your portal.
200+
201+
### Full production deployment (hub-routed OAuth)
202+
203+
Provider running in its own namespace, registered against an already-running hub,
204+
with OAuth routed through the hub ingress (no per-provider ingress). The runtime
205+
kubeconfig the controllers need is supplied as the `kedge-provider-kubeconfig`
206+
Secret (its key **must** be `kubeconfig`) — mint it via the admin onboarding flow
207+
(`/bonkers`).
208+
209+
```sh
210+
# 1. Namespace.
211+
kubectl create namespace kedge-prod-provider-code
212+
213+
# 2. Provider kubeconfig Secret (key MUST be "kubeconfig").
214+
kubectl -n kedge-prod-provider-code create secret generic kedge-provider-kubeconfig \
215+
--from-file=kubeconfig=kedge/provider-code.kubeconfig
216+
217+
# 3. GitHub OAuth App client secret.
218+
kubectl -n kedge-prod-provider-code create secret generic code-github-oauth \
219+
--from-literal=clientSecret=<oauth-app-client-secret>
220+
221+
# 4. Install the chart from the published OCI registry.
222+
helm upgrade --install code oci://ghcr.io/faroshq/charts/kedge-code-provider:0.0.82 \
223+
-n kedge-prod-provider-code \
224+
--set hub.url=https://kedge-kedge-hub.kedge-prod.svc.cluster.local:9443 \
225+
--set hub.insecure=true \
226+
--set hub.tokenSecretRef.name="" \
227+
--set image.tag=v0.0.82 \
228+
--set catalogEntry.enabled=false \
229+
--set githubOAuth.enabled=true \
230+
--set githubOAuth.clientId=<oauth-app-client-id> \
231+
--set githubOAuth.clientSecretRef.name=code-github-oauth \
232+
--set githubOAuth.clientSecretRef.key=clientSecret \
233+
--set githubOAuth.redirectURL=https://console.faros.sh/services/providers/code/oauth/github/callback \
234+
--set githubOAuth.portalOrigin=https://console.faros.sh
235+
```
236+
237+
Notes:
238+
- `hub.insecure=true` + `hub.tokenSecretRef.name=""` suit an in-cluster hub with
239+
a self-signed cert and no static heartbeat token. For a real heartbeat token,
240+
create a Secret and set `hub.tokenSecretRef.name`/`.key` instead.
241+
- `catalogEntry.enabled=false` means the chart does **not** manage the
242+
CatalogEntry — the hub uses whatever `backend.url` the existing CatalogEntry
243+
declares. **Make sure that `backend.url` points at this deployment's Service**
244+
(`http://code-kedge-code-provider.<namespace>.svc.cluster.local:8083`); a stale
245+
namespace there makes the hub→provider proxy return **502** (and the OAuth
246+
button stays hidden). Leaving `catalogEntry.enabled=true` lets the init
247+
container keep `backend.url` in sync with the release namespace automatically.
248+
- After install, verify the OAuth probe returns `{"enabled":true}`:
249+
```sh
250+
curl -s https://console.faros.sh/services/providers/code/oauth/github/config
251+
```
173252

174253
`values.yaml` documents the full surface — image, replicas, hub URL + token
175254
Secret, the runtime kubeconfig Secret name, the `githubOAuth.*` block, the
@@ -225,7 +304,7 @@ the connection token's `read:packages` scope.
225304
| `KEDGE_DEV_ALLOW_TENANT_QUERY` | (unset) | `true` lets `?tenant=`/`?token=` replace identity headers (dev only) |
226305
| `GITHUB_OAUTH_CLIENT_ID` | (unset → OAuth off) | GitHub OAuth App client ID |
227306
| `GITHUB_OAUTH_CLIENT_SECRET` | (unset) | GitHub OAuth App client secret |
228-
| `GITHUB_OAUTH_REDIRECT_URL` | (unset) | Absolute callback URL on the provider's own host |
307+
| `GITHUB_OAUTH_REDIRECT_URL` | (unset) | Absolute callback URL (must end in `/callback`); either the hub `/services/providers/code/oauth/github/callback` proxy route or the provider's own host. `/start` is derived from it |
229308
| `GITHUB_OAUTH_PORTAL_ORIGIN` | `*` | postMessage target origin (set to the hub origin in prod) |
230309
| `GITHUB_OAUTH_SCOPES` | `repo,read:org,admin:public_key,read:packages` | Requested OAuth scopes |
231310

0 commit comments

Comments
 (0)