Try the Hub
This quickstart installs and runs the Hub API locally and connects two Crossplane control planes to it. By the end you have a three control plane fleet and a Console that queries across the fleet. You can check which resources exist, types and where they exist, and which control plane runs an older package version.
Budget about 30 minutes. Everything runs in local kind clusters and needs no
cloud credentials.
This quickstart uses the chart's demo mode. Demo mode bundles PostgreSQL, Keycloak, and the Envoy Gateway controller with fixed credentials, ephemeral storage, and a self-signed certificate. Don't run it in production. For a real install, see Installing Hub.
What you build
| Cluster | Role |
|---|---|
hub | Runs Hub, the bundled Postgres and Keycloak, and the Console. Registers itself as the default control plane. |
ctp-payments | Crossplane control plane running provider-nop v0.4.0. |
ctp-analytics | Crossplane control plane running provider-nop v0.5.0. |
The two control plane clusters run the same composite type on different provider versions. That difference is what the fleet views surface later.
Prerequisites
- Docker with at least 8 CPU cores and 8 GB of memory available
- kind v0.20 or later
- kubectl
- Helm 3
- The up CLI and an Upbound account
Log in before you start. The Hub chart lives in Upbound's registry:
up login
Step 1: Create the Hub cluster
Demo mode exposes the Console through an Envoy Gateway on node port 30443. Map
that node port to a host port so you can open the Console in a browser.
-
Write the cluster config.
hub-cluster.yamlkind: ClusterapiVersion: kind.x-k8s.io/v1alpha4name: hubnodes:- role: control-planeextraPortMappings:- containerPort: 30443hostPort: 8443protocol: TCP -
Create the cluster.
kind create cluster --config hub-cluster.yamlkind attaches every cluster it creates to a shared Docker network named
kind, and names the node container<cluster-name>-control-plane. The control plane clusters you create in step 4 reach this one athub-control-plane.
Step 2: Install Hub
-
Install the umbrella chart with demo mode on.
The two
NodePortsettings expose thehub-coreAPI and its token exchange endpoint so connectors running in the other kind clusters can reach them. A production install routes this traffic through a gateway instead.helm upgrade --install hub oci://xpkg.upbound.io/upbound/hub \--namespace hub --create-namespace \--version 1.0.1 \--set global.demo.enabled=true \--set hub-core.api.service.api.type=NodePort \--set hub-core.api.tokenExchange.service.type=NodePort -
Wait for the stack to come up. The first install runs a database migration before
hub-corestarts, so this takes up to 5 minutes.kubectl --context kind-hub -n hub wait --for=condition=ready pod --all --timeout=10m -
Read back the node ports Kubernetes assigned. Step 6 points the connectors at them, so stay in this shell or note the values down.
HUB_API_PORT=$(kubectl --context kind-hub -n hub get svc hub-core \-o jsonpath='{.spec.ports[?(@.name=="http")].nodePort}')HUB_TOKEN_PORT=$(kubectl --context kind-hub -n hub get svc hub-core-token-exchange \-o jsonpath='{.spec.ports[?(@.name=="token-exchange")].nodePort}')echo "api=$HUB_API_PORT token-exchange=$HUB_TOKEN_PORT"noteChart version 1.0.1 rejects an explicit
hub-core.api.service.api.nodePort, so you can't pin these ports at install time. Kubernetes allocates them from the node port range instead.
Demo mode bootstraps four things for you:
- A Keycloak identity provider named
keycloak, with a set of demo users. - A
defaultcontrol plane in thedefaultrealm, representing the Hub cluster itself. - An
OrganizationRoleBindinggranting thekeycloak:admingroup organization admin. - A
RealmRoleBindinggranting the same group admin on thedefaultrealm.
A realm is the namespace a control plane lives in. Organization roles govern realms, identity, and role bindings. Realm roles govern the control planes and resources inside a realm. Both bindings exist because organization admin alone doesn't grant access to a realm's contents. See Access and authorization.
Step 3: Sign in to the Console
-
Open
https://hub.127.0.0.1.nip.io:8443.Demo mode serves the Console, the Hub API, and Keycloak from this one hostname, routed by path. The gateway presents a self-signed certificate, so your browser warns you on first visit. Choose Advanced, then proceed.
-
Sign in as
adminwith the passwordadmin. -
Open the control planes view. One control plane,
default, is already registered andReady. Its resources are the Hub cluster's own.
Demo mode also creates users with narrower access. Keep them for step 7:
| User | Password | Access |
|---|---|---|
admin | admin | Organization admin and realm admin on default |
editor-alice | password | Editor |
editor-bob | password | Editor |
viewer-charlie | password | Read-only |
Step 4: Create two Crossplane control planes
Each cluster gets UXP, provider-nop at a different version, a composite
resource definition, and composite resources. The differing provider
versions give the fleet views something to compare.
-
Create the two clusters.
kind create cluster --name ctp-paymentskind create cluster --name ctp-analytics -
Install UXP in each one.
for ctx in kind-ctp-payments kind-ctp-analytics; doup uxp install --kubecontext "$ctx"kubectl --context "$ctx" -n crossplane-system \wait --for=condition=ready pod --all --timeout=5mdone -
Install the packages.
ctp-paymentsgetsprovider-nopv0.4.0 andctp-analyticsgets v0.5.0.install_packages() {kubectl --context "$1" apply -f - <<EOFapiVersion: pkg.crossplane.io/v1kind: Providermetadata:name: provider-nopspec:package: xpkg.upbound.io/crossplane-contrib/provider-nop:$2---apiVersion: pkg.crossplane.io/v1beta1kind: Functionmetadata:name: function-patch-and-transformspec:package: xpkg.upbound.io/crossplane-contrib/function-patch-and-transform:v0.9.0EOFkubectl --context "$1" wait --for=condition=healthy provider/provider-nop --timeout=5mkubectl --context "$1" wait --for=condition=healthy function/function-patch-and-transform --timeout=5m}install_packages kind-ctp-payments v0.4.0install_packages kind-ctp-analytics v0.5.0 -
Define a composite type in both clusters.
for ctx in kind-ctp-payments kind-ctp-analytics; dokubectl --context "$ctx" apply -f - <<'EOF'apiVersion: apiextensions.crossplane.io/v1kind: CompositeResourceDefinitionmetadata:name: xapps.example.upbound.iospec:group: example.upbound.ionames:kind: XAppplural: xappsversions:- name: v1alpha1served: truereferenceable: trueschema:openAPIV3Schema:type: objectproperties:spec:type: object---apiVersion: apiextensions.crossplane.io/v1kind: Compositionmetadata:name: xapps.example.upbound.iospec:compositeTypeRef:apiVersion: example.upbound.io/v1alpha1kind: XAppmode: Pipelinepipeline:- step: create-nopfunctionRef:name: function-patch-and-transforminput:apiVersion: pt.fn.crossplane.io/v1beta1kind: Resourcesresources:- name: nopbase:apiVersion: nop.crossplane.io/v1alpha1kind: NopResourcespec:forProvider:conditionAfter:- conditionType: ReadyconditionStatus: "True"time: 5sEOFkubectl --context "$ctx" wait --for=condition=established \crd/xapps.example.upbound.io --timeout=2mdone -
Create composite resources. Give each cluster a different count so the aggregate views show something other than a tie.
for n in 1 2 3; dokubectl --context kind-ctp-payments create -f - <<EOFapiVersion: example.upbound.io/v1alpha1kind: XAppmetadata:name: payments-$nspec: {}EOFdonekubectl --context kind-ctp-analytics create -f - <<'EOF'apiVersion: example.upbound.io/v1alpha1kind: XAppmetadata:name: analytics-1spec: {}EOF
Step 5: Register each control plane
Registering a control plane in Hub returns a one-time registration token. The
connector presents that token the first time it contacts hub-core.
Do this once per cluster.
-
In the Console, open the control planes view for the
defaultrealm. -
Select Create control plane and name it
payments. -
Copy the registration token the Console displays and set it as a shell variable.
PAYMENTS_TOKEN=<paste-registration-token>warningThe Console shows the token once. It's valid for 24 hours and single-use. If you lose it, reissue the token, which invalidates the old one.
-
Repeat for
analytics.ANALYTICS_TOKEN=<paste-registration-token>
Step 6: Install the connector
The connector runs inside each control plane cluster, exchanges its registration
token for a Hub credential, and streams resource state to hub-core.
-
Install the connector in
ctp-payments.kubectl --context kind-ctp-payments create namespace upbound-systemkubectl --context kind-ctp-payments -n upbound-system \create secret generic hub-connector-credentials \--from-literal=registrationToken="$PAYMENTS_TOKEN"helm install hub-connector oci://xpkg.upbound.io/upbound/hub-connector \--kube-context kind-ctp-payments \--namespace upbound-system \--version 1.0.1 \--set connector.hub.url=http://hub-control-plane:$HUB_API_PORT \--set connector.hub.tokenExchangeUrl=http://hub-control-plane:$HUB_TOKEN_PORT \--set connector.hub.allowInsecure=true$HUB_API_PORTand$HUB_TOKEN_PORTcome from step 2.connector.hub.allowInsecurepermits plaintext HTTP to the Hub. It's needed here because the node ports you exposed in step 2 don't end TLS. A real install points the connector at an HTTPS gateway and leaves this at its default offalse. -
Install the connector in
ctp-analytics.kubectl --context kind-ctp-analytics create namespace upbound-systemkubectl --context kind-ctp-analytics -n upbound-system \create secret generic hub-connector-credentials \--from-literal=registrationToken="$ANALYTICS_TOKEN"helm install hub-connector oci://xpkg.upbound.io/upbound/hub-connector \--kube-context kind-ctp-analytics \--namespace upbound-system \--version 1.0.1 \--set connector.hub.url=http://hub-control-plane:$HUB_API_PORT \--set connector.hub.tokenExchangeUrl=http://hub-control-plane:$HUB_TOKEN_PORT \--set connector.hub.allowInsecure=true -
Confirm both connectors are running.
for ctx in kind-ctp-payments kind-ctp-analytics; dokubectl --context "$ctx" -n upbound-system wait --for=condition=ready pod \--selector app.kubernetes.io/name=hub-connector --timeout=3mdone -
Refresh the Console. The control planes view now lists
default,payments, andanalytics, allReady.A control plane stays
Pendinguntil its connector registers. If one doesn't turnReady, see Troubleshooting.
Step 7: Query the fleet
Everything below happens in one Console, against all three control planes, with
no kubectl context switching.
Find resources across control planes
Open the resources view. It lists every resource the connectors report, from all three control planes at once, with sort, filter, and search.
Try these:
- Search for
XApp. Four composites come back: three frompayments, one fromanalytics. - Filter by control plane
analytics. The list narrows to that cluster without changing the query. - Filter by health to isolate resources that aren't
Ready.
By default the connector syncs only resources authorized by the
crossplane-admin ClusterRole, so you see Crossplane resources rather than every
object in the cluster. Widen connector.sync.limitToClusterRoles, or set it to
[], to sync more.
Roll resources up into counts
Group the resource list by health, label, annotation, or creation time.
Each grouping answers a fleet-wide question in one screen, such as how many
composites are Ready right now across all three control planes.
Hub keeps these aggregations as time series, so the same counts also show up as trends rather than a single snapshot. The trend line is thin right now because your fleet is minutes old.
Compare types across the fleet
Open the types view. Hub indexes the CRDs and XRDs installed in every connected
control plane, so XApp appears once with both payments and analytics
listed underneath it.
This view is where schema drift shows up. Change the XRD in one cluster and the two control planes stop agreeing on the same type:
kubectl --context kind-ctp-analytics patch xrd xapps.example.upbound.io \
--type=json \
-p='[{"op":"add","path":"/spec/names/shortNames","value":["xa"]}]'
After the next rediscovery interval, about 20 seconds, the types view reflects
the change on analytics only.
Spot package version drift
Open the packages view. Hub lists every Provider, Configuration, and Function across the fleet alongside the version each control plane runs.
provider-nop appears with two versions: v0.4.0 on payments and v0.5.0 on
analytics. You set this drift up in step 4, and finding it takes a single
lookup instead of one kubectl get providers per cluster.
Upgrade payments to match and watch the entry collapse to one version:
kubectl --context kind-ctp-payments patch provider provider-nop \
--type=merge \
-p '{"spec":{"package":"xpkg.upbound.io/crossplane-contrib/provider-nop:v0.5.0"}}'
See how access scopes the view
Sign out and sign back in as viewer-charlie with the password password.
The fleet views only show control planes in realms the signed-in user can access,
and counts reflect that scope. An operator with partial access sees partial
totals, not an error. Sign back in as admin to restore the full view.
Catalog, which indexes the package images behind those providers and makes them searchable, is a preview feature and off by default. See Catalog to enable it.
Troubleshooting
A control plane stays Pending
The connector hasn't completed registration. Check its logs:
kubectl --context kind-ctp-payments -n upbound-system logs deployment/hub-connector
The most common cause is the connector failing to reach
hub-control-plane:$HUB_API_PORT, which happens when the cluster didn't join the shared
kind Docker network. Attach it and restart the connector:
docker network connect kind ctp-payments-control-plane
kubectl --context kind-ctp-payments -n upbound-system \
rollout restart deployment hub-connector
The connector logs an authentication error
The registration token expired, or something already used it. Reissue a token for the control plane in the Console, update the secret, and restart the connector:
kubectl --context kind-ctp-payments -n upbound-system \
delete secret hub-connector-credentials
kubectl --context kind-ctp-payments -n upbound-system \
create secret generic hub-connector-credentials \
--from-literal=registrationToken="<new-token>"
kubectl --context kind-ctp-payments -n upbound-system \
rollout restart deployment hub-connector
The Console won't load
Confirm the gateway data plane is serving on node port 30443:
kubectl --context kind-hub -n hub get svc \
-l gateway.envoyproxy.io/owning-gateway-name=hub-gateway
If you created the hub cluster without the extraPortMappings from step 1,
reach the Console with a port-forward instead:
kubectl --context kind-hub -n hub port-forward \
"svc/$(kubectl --context kind-hub -n hub get svc \
-l gateway.envoyproxy.io/owning-gateway-name=hub-gateway \
-o jsonpath='{.items[0].metadata.name}')" 8443:8443
Resources appear for a control plane but not the ones you expect
The connector syncs only what the crossplane-admin ClusterRole authorizes.
Widen the filter and upgrade the release:
helm upgrade hub-connector oci://xpkg.upbound.io/upbound/hub-connector \
--version 1.0.1 \
--kube-context kind-ctp-payments \
--namespace upbound-system \
--reuse-values \
--set 'connector.sync.limitToClusterRoles=[]'
Clean up
Delete all three clusters:
kind delete cluster --name ctp-analytics
kind delete cluster --name ctp-payments
kind delete cluster --name hub
Next steps
- Installing the Hub API to run against your own PostgreSQL, OIDC provider, and gateway.
- Connect a control plane for the connector install against a real Hub, including the kubectl path for minting registration tokens.
- Production overview for sizing, high availability, autoscaling, and upgrades.