Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ codegen-infrastructure-provider: $(CONTROLLER_GEN) ## Codegen for the infrastruc
$(CURDIR)/$(CONTROLLER_GEN) object paths="./apis/..." && \
$(CURDIR)/$(CONTROLLER_GEN) crd paths="./apis/..." \
output:crd:artifacts:config=$(CURDIR)/providers/infrastructure/config/crds
# The provider embeds the Template CRD from install/crds/ (//go:embed in
# install/crds.go) and applies it into the kcp provider workspace at init.
# Keep that embed copy in lockstep with the generated schema — otherwise the
# operator installs a stale Template CRD and kcp silently prunes new fields
# (e.g. spec.sampleValues). The InfrastructureProvider CRD is NOT embedded
# (it's applied to the host cluster by the chart), so it stays in config/ only.
cp providers/infrastructure/config/crds/infrastructure.kedge.faros.sh_templates.yaml \
providers/infrastructure/install/crds/infrastructure.kedge.faros.sh_templates.yaml
./hack/ensure-boilerplate.sh

## Generate deepcopy + CRD YAML + kcp APIResourceSchemas for the code
Expand Down Expand Up @@ -885,6 +893,8 @@ run-provider-infrastructure: build-infrastructure-provider ## Run the infrastruc
INFRASTRUCTURE_WORKSPACE_PATH=$${INFRASTRUCTURE_WORKSPACE_PATH:-$(INFRASTRUCTURE_WORKSPACE_PATH)} \
KRO_KUBECONFIG=$${KRO_KUBECONFIG:-$$( [ -f "$(KRO_KIND_KUBECONFIG)" ] && echo "$(KRO_KIND_KUBECONFIG)" )} \
INFRASTRUCTURE_KUBECONFIG=$${INFRASTRUCTURE_KUBECONFIG:-$$( [ -f "$(INFRASTRUCTURE_RUNTIME_KUBECONFIG)" ] && echo "$(INFRASTRUCTURE_RUNTIME_KUBECONFIG)" )} \
KEDGE_APP_BASE_DOMAIN=$${KEDGE_APP_BASE_DOMAIN:-apps.127.0.0.1.sslip.io} \
KEDGE_INGRESS_CLASS=$${KEDGE_INGRESS_CLASS:-nginx} \
$(BINDIR)/infrastructure-provider

run-provider-infrastructure-operator: build-infrastructure-provider ## Run the infrastructure provider in OPERATOR mode (bootstrap reconcile + serve from a provider + runtime kubeconfig)
Expand Down
11 changes: 11 additions & 0 deletions Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,17 @@ local_resource(
'providers/infrastructure/kro',
'providers/infrastructure/tenant',
'providers/infrastructure/mcpserver',
# The operator path: the controller/manager, the bootstrap install
# helpers, the embedded CRDs + seed Templates (install/), the API types,
# and the kro backend. Without these, edits to the CRD schema, the seed
# templates, or the controller don't trigger a rebuild and the running
# binary embeds a stale CRD (kcp then prunes new fields like sampleValues).
'providers/infrastructure/apis',
'providers/infrastructure/install',
'providers/infrastructure/controller',
'providers/infrastructure/operator',
'providers/infrastructure/backend',
'providers/infrastructure/apps',
'providers/infrastructure/portal/src',
'providers/infrastructure/portal/package.json',
'providers/infrastructure/go.mod',
Expand Down
34 changes: 34 additions & 0 deletions providers/infrastructure/apis/v1alpha1/types_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,40 @@ type TemplateSpec struct {
// +kubebuilder:pruning:PreserveUnknownFields
// +kubebuilder:validation:XPreserveUnknownFields
SampleValues *runtime.RawExtension `json:"sampleValues,omitempty"`

// Agent is operational guidance for AI agents that discover this template
// via MCP — what it provisions, when to choose it, prerequisites, and where
// its outputs (URL, DB connection Secret, …) land. It complements the
// human-facing displayName/description (which target the portal UI) and is
// not rendered in the form.
// +optional
Agent *TemplateAgent `json:"agent,omitempty"`
}

// TemplateAgent is machine-facing guidance for LLM agents operating this
// template through MCP. All fields are natural language aimed at an agent, not
// the portal UI.
type TemplateAgent struct {
// Usage is markdown guidance for an agent: what this template provisions,
// when to choose it, how the result is exposed (URLs/ingress/auth), and how
// to operate it after provisioning. The primary, free-form field; the
// structured fields below call out the most actionable specifics.
// +optional
// +kubebuilder:validation:MaxLength=8192
Usage string `json:"usage,omitempty"`

// Prerequisites the caller must satisfy BEFORE provisioning — e.g. a
// cloud-credentials Secret in the tenant's default namespace carrying
// specific keys. One human-readable requirement per entry.
// +optional
Prerequisites []string `json:"prerequisites,omitempty"`

// Outputs describe where the provisioned instance's results land so an agent
// can discover and wire them — e.g. "status.url: public app URL",
// "Secret <name>-db-credentials key 'uri': postgres:// connection string".
// One output per entry.
// +optional
Outputs []string `json:"outputs,omitempty"`
}

// TemplateInstanceCRD identifies the per-template CRD the platform
Expand Down
30 changes: 30 additions & 0 deletions providers/infrastructure/apis/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,40 @@ spec:
spec:
description: TemplateSpec is the desired state.
properties:
agent:
description: |-
Agent is operational guidance for AI agents that discover this template
via MCP — what it provisions, when to choose it, prerequisites, and where
its outputs (URL, DB connection Secret, …) land. It complements the
human-facing displayName/description (which target the portal UI) and is
not rendered in the form.
properties:
outputs:
description: |-
Outputs describe where the provisioned instance's results land so an agent
can discover and wire them — e.g. "status.url: public app URL",
"Secret <name>-db-credentials key 'uri': postgres:// connection string".
One output per entry.
items:
type: string
type: array
prerequisites:
description: |-
Prerequisites the caller must satisfy BEFORE provisioning — e.g. a
cloud-credentials Secret in the tenant's default namespace carrying
specific keys. One human-readable requirement per entry.
items:
type: string
type: array
usage:
description: |-
Usage is markdown guidance for an agent: what this template provisions,
when to choose it, how the result is exposed (URLs/ingress/auth), and how
to operate it after provisioning. The primary, free-form field; the
structured fields below call out the most actionable specifics.
maxLength: 8192
type: string
type: object
backend:
description: |-
Backend names the registered backend implementation that
Expand Down
12 changes: 11 additions & 1 deletion providers/infrastructure/controller/application/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
Expand All @@ -48,6 +49,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/manager"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"

apiskcpv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1"
apiskcpv1alpha2 "github.com/kcp-dev/sdk/apis/apis/v1alpha2"
"github.com/kcp-dev/multicluster-provider/apiexport"
mcbuilder "sigs.k8s.io/multicluster-runtime/pkg/builder"
mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager"
Expand Down Expand Up @@ -138,7 +141,14 @@ func New(cfg Config) (*Controller, error) {

c := &Controller{cfg: cfg}

scheme := runtime.NewScheme() // Application + Secret read unstructured; no typed registration needed.
// Application + Secret are read unstructured, but the apiexport multicluster
// provider builds a TYPED cache over APIExportEndpointSlice to discover the
// virtual-workspace URL — so the kcp apis scheme must be registered or the
// manager fails with "no kind is registered for the type
// v1alpha1.APIExportEndpointSlice". Mirrors the kuery engagement controller.
scheme := runtime.NewScheme()
utilruntime.Must(apiskcpv1alpha1.AddToScheme(scheme))
utilruntime.Must(apiskcpv1alpha2.AddToScheme(scheme))

provider, err := apiexport.New(cfg.ProviderConfig, cfg.APIExportName, apiexport.Options{Scheme: scheme})
if err != nil {
Expand Down
40 changes: 28 additions & 12 deletions providers/infrastructure/examples/application/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,25 @@ Example workloads for the infrastructure provider's **`application`** template (
3-tier app: frontend + backend + Postgres). They exist so the template can be
provisioned from the portal in essentially one click and visibly "just work".

Both tiers are served under **one URL**: `/` is the UI, `/api/*` is the backend.
The exposure layer does the path routing — the Ingress in `oidc.mode=none`, and
oauth2-proxy (multi-upstream) otherwise, so the backend stays behind the gate.

```
browser ──▶ frontend (Go, this dir/frontend) ──▶ backend (Go, this dir/backend) ──▶ Postgres
renders a guestbook page JSON API over DATABASE_URL (provisioned by the template)
one URL (https://<app>.<baseDomain>)
┌── / ───────────┴──────────── /api ──┐
▼ ▼
frontend (Go, this dir/frontend) backend (Go, this dir/backend) ──▶ Postgres
UI; calls /api from the browser JSON API over DATABASE_URL
```

- **[apps/frontend](apps/frontend)** — public-facing tier (the only one exposed).
Standard-library Go HTTP server: renders a guestbook page, calls the backend
over cluster DNS (`BACKEND_URL`). Never touches Postgres directly.
- **[apps/backend](apps/backend)** — internal API tier. Go + a pure-Go Postgres
driver; reads/writes the guestbook in Postgres via `DATABASE_URL` (the
- **[apps/frontend](apps/frontend)** — public UI tier. Standard-library Go server
that serves a small SPA; the page calls `/api/messages` from the **browser**
(same origin), so the frontend never proxies to the backend and needs no
`BACKEND_URL`.
- **[apps/backend](apps/backend)** — API tier, served at `/api/*`. Go + a pure-Go
Postgres driver; reads/writes the guestbook in Postgres via `DATABASE_URL` (the
connection string the template's credentials Job mints).

Each app is a self-contained Go module with its own `Dockerfile` (build context =
Expand All @@ -37,8 +46,9 @@ pulls a current build.

The `application` template is OIDC-gated by design. For a zero-setup demo the
sample uses **`oidc.mode: none`**, which drops the oauth2-proxy gate (the RGD
excludes those resources via `includeWhen`) and routes the Ingress straight to
the frontend.
excludes those resources via `includeWhen`) and routes via the Ingress directly
(`/` → frontend, `/api` → backend). In `byo` mode oauth2-proxy owns the URL and
path-routes the same `/` and `/api` behind authentication.

> ⚠️ `oidc.mode: none` means the app is **unauthenticated** — anyone with the URL
> can reach it. It is for demos/dev only. For real use, set `oidc.mode: byo`,
Expand All @@ -48,11 +58,17 @@ the frontend.

## Run locally

There's no path router locally, so hit each tier directly:

```sh
# backend needs a Postgres; point DATABASE_URL at one
DATABASE_URL=postgres://user:pass@localhost:5432/appdb?sslmode=disable \
go run ./apps/backend # listens on :8080
go run ./apps/backend # API on :8080 (GET/POST /api/messages)

BACKEND_URL=http://localhost:8080 PORT=8081 \
go run ./apps/frontend # open http://localhost:8081
PORT=8081 go run ./apps/frontend # UI on :8081 — but its /api calls are
# same-origin (:8081), where there's no
# backend. In-cluster the exposure layer
# routes /api to the backend; to mimic that
# locally, front both behind one proxy that
# sends /api → :8080 and / → :8081.
```
Loading
Loading