Platform Mesh

Provider quick start

This tutorial walks you through building your first service provider in Platform Mesh with api-syncagent. You will publish the HttpBin custom resource from a Kubernetes service cluster into kcp, then verify that a consumer workspace can create an HttpBin resource and receive status back.

::: tip No code provider This provider is considered “no code”, as all components are built and deployed. It is using generic UI of Platform-Mesh and api-syncagent, without any custom provider-specific code. You will deploy the HttpBin operator as a black box and use api-syncagent to publish its CRD into kcp and synchronize resources. :::

By the end of this tutorial, you will have:

::: warning Development preview The local setup is under active development. Commands and component versions may change. :::

Prerequisites

Before you begin, make sure you have:

If you already ran the example-data setup, the HttpBin provider is deployed automatically. This tutorial is most useful from a clean setup where you build the provider flow yourself.

From the helm-charts/local-setup directory:

task local-setup

What you will build

The HttpBin provider is a simple managed service provider. Consumers create HttpBin resources in their kcp workspace. api-syncagent synchronizes those resources into the service cluster, where the HttpBin operator reconciles them into running pods and writes status back.

Provider to consumer API sharing diagram

The detailed synchronization flow looks like this:

flowchart LR
    subgraph kcp["kcp"]
        subgraph cw["Consumer workspace"]
            httpbin_res["HttpBin resource"]
        end
        subgraph pw["Provider workspace"]
            apiexport["APIExport"]
        end
    end

    subgraph sc["Service cluster"]
        agent["api-syncagent"]
        httpbin_local["HttpBin CR"]
        operator["httpbin operator"]
        pod["httpbin pod"]
    end

    httpbin_res -- "APIBinding" --> apiexport
    apiexport -. "virtual workspace" .-> agent
    agent -- "spec sync" --> httpbin_local
    httpbin_local --> operator
    operator --> pod
    pod -. "status" .-> httpbin_local
    httpbin_local -. "status sync" .-> agent
    agent -. "status sync" .-> httpbin_res

The consumer never interacts with the service cluster directly. From their perspective, HttpBin is a Kubernetes resource type available in their workspace.

Set up kubeconfigs

In the local setup, one Kind cluster named platform-mesh hosts Platform Mesh and also acts as the service cluster for this tutorial. kcp runs inside that cluster, but exposes its own API server on https://localhost:8443.

You will use two kubeconfigs:

Target Kubeconfig Purpose
kcp .secret/kcp/admin.kubeconfig Manage workspaces, APIExports, and APIBindings.
Kind cluster default Kind kubeconfig Manage operators, pods, CRDs, and api-syncagent.

Run this from helm-charts/local-setup:

export PM_KUBECONFIG=$(pwd)/.secret/kcp/admin.kubeconfig

kind export kubeconfig --name platform-mesh
export COMPUTE_KUBECONFIG=$HOME/.kube/config

Verify both connections:

KUBECONFIG=$PM_KUBECONFIG kubectl kcp workspace use :root
kubectl --kubeconfig $COMPUTE_KUBECONFIG get nodes

Create the provider workspace

Provider workspaces are organized under root:providers. Create the container workspace and the HttpBin provider workspace:

KUBECONFIG=$PM_KUBECONFIG kubectl ws use :
KUBECONFIG=$PM_KUBECONFIG kubectl create-workspace providers --type=root:providers --enter --ignore-existing
KUBECONFIG=$PM_KUBECONFIG kubectl create-workspace httpbin-provider --type=root:provider --enter --ignore-existing

Expected output:

Current workspace is "root:providers:httpbin-provider" (type root:provider).

Create the APIExport

The APIExport makes the service API visible to consumers. Create it in the provider workspace:

KUBECONFIG=$PM_KUBECONFIG kubectl apply -f - <<EOF
apiVersion: apis.kcp.io/v1alpha1
kind: APIExport
metadata:
  name: orchestrate.platform-mesh.io
  labels:
    ui.platform-mesh.io/content-for: orchestrate.platform-mesh.io
spec: {}
EOF

Grant bind permission so a consumer workspace can create an APIBinding to this export:

KUBECONFIG=$PM_KUBECONFIG kubectl apply -f - <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: apiexport-bind
rules:
  - apiGroups: ["apis.kcp.io"]
    resources: ["apiexports"]
    verbs: ["bind"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: anonymous-view
subjects:
  - kind: User
    name: system:anonymous
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: apiexport-bind
  apiGroup: rbac.authorization.k8s.io
EOF

Verify the APIExport:

KUBECONFIG=$PM_KUBECONFIG kubectl get apiexports

Expected output:

NAME                           AGE
orchestrate.platform-mesh.io   5s

Deploy the HttpBin operator

The HttpBin operator runs on the service cluster. api-syncagent handles the integration with kcp, so the operator itself does not need to know about Platform Mesh.

Run this from the helm-charts repository root:

cd ..

helm upgrade --install example-httpbin-operator \
  ./charts/example-httpbin-operator \
  --kubeconfig $COMPUTE_KUBECONFIG \
  -n example-httpbin-provider \
  --create-namespace

Verify the operator:

KUBECONFIG=$COMPUTE_KUBECONFIG kubectl get pods -n example-httpbin-provider

Verify the CRD:

KUBECONFIG=$COMPUTE_KUBECONFIG kubectl get crd httpbins.orchestrate.platform-mesh.io

The CRD defines an HttpBin resource with a spec.region field and a status subresource:

apiVersion: orchestrate.platform-mesh.io/v1alpha1
kind: HttpBin
metadata:
  name: my-httpbin
spec:
  region: eu-west-1
status:
  ready: false
  url: ""
  conditions: []

The status subresource matters because api-syncagent uses it to synchronize provider status back to kcp.

Deploy api-syncagent

api-syncagent runs on the service cluster and connects to kcp. It needs a kubeconfig Secret for kcp access.

Create the Secret:

KUBECONFIG=$COMPUTE_KUBECONFIG kubectl create secret generic httpbin-kubeconfig \
  -n example-httpbin-provider \
  --from-file=kubeconfig=$PM_KUBECONFIG

In the local setup, kcp is exposed through Traefik on localhost:8443 at the pinned ClusterIP 10.96.188.4. Pods need host aliases so in-cluster traffic can resolve the kcp hostnames to that address. These values match the hostAliases block in the canonical example-httpbin-provider HelmRelease in the local-setup.

Install api-syncagent:

helm repo add kcp https://kcp-dev.github.io/helm-charts
helm repo update

helm upgrade --install api-syncagent kcp/api-syncagent \
  --kubeconfig $COMPUTE_KUBECONFIG \
  -n example-httpbin-provider \
  --set apiExportEndpointSliceName=orchestrate.platform-mesh.io \
  --set agentName=kcp-api-syncagent \
  --set kcpKubeconfig=httpbin-kubeconfig \
  --set hostAliases.enabled=true \
  --set "hostAliases.values[0].ip=10.96.188.4" \
  --set "hostAliases.values[0].hostnames[0]=localhost" \
  --set "hostAliases.values[0].hostnames[1]=portal.localhost" \
  --set "hostAliases.values[0].hostnames[2]=kcp.localhost" \
  --set "hostAliases.values[0].hostnames[3]=root.kcp.localhost"

Grant RBAC for api-syncagent on the service cluster:

KUBECONFIG=$COMPUTE_KUBECONFIG kubectl apply -f - <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: api-syncagent-httpbin
rules:
  - apiGroups: ["orchestrate.platform-mesh.io"]
    resources: ["httpbins", "httpbins/status"]
    verbs: ["*"]
  - apiGroups: [""]
    resources: ["namespaces"]
    verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: api-syncagent-httpbin
subjects:
  - kind: ServiceAccount
    name: api-syncagent
    namespace: example-httpbin-provider
roleRef:
  kind: ClusterRole
  name: api-syncagent-httpbin
  apiGroup: rbac.authorization.k8s.io
EOF

Verify api-syncagent:

KUBECONFIG=$COMPUTE_KUBECONFIG kubectl get pods -n example-httpbin-provider -l app.kubernetes.io/name=kcp-api-syncagent

Create a PublishedResource

PublishedResource tells api-syncagent which CRD to publish into kcp. Create it on the service cluster:

KUBECONFIG=$COMPUTE_KUBECONFIG kubectl apply -f - <<EOF
apiVersion: syncagent.kcp.io/v1alpha1
kind: PublishedResource
metadata:
  name: httpbin-local-provider
spec:
  resource:
    kind: HttpBin
    apiGroup: orchestrate.platform-mesh.io
    version: v1alpha1
EOF

api-syncagent will create an APIResourceSchema in kcp and update the APIExport.

Verify the schema:

KUBECONFIG=$PM_KUBECONFIG kubectl kcp workspace use :root:providers:httpbin-provider
KUBECONFIG=$PM_KUBECONFIG kubectl get apiresourceschemas

Verify the APIExport references the schema:

KUBECONFIG=$PM_KUBECONFIG kubectl get apiexport orchestrate.platform-mesh.io -o yaml

Look for the published schemas in the spec block.

Check the agent logs:

KUBECONFIG=$COMPUTE_KUBECONFIG kubectl logs -n example-httpbin-provider -l app.kubernetes.io/name=kcp-api-syncagent --tail=20

Test the consumer flow

Login into https://portal.localhost:8443 and create organization test-consumer:

Platform Mesh Login

Create an account under the organization.

Platform Mesh Account

Wait until the account is ready, then download the kubeconfig and set the environment variable:

Platform Mesh KubeConfig

export CONSUMER_KUBECONFIG=/path/to/consumer-kubeconfig

Create an APIBinding:

KUBECONFIG=$CONSUMER_KUBECONFIG kubectl apply -f - <<EOF
apiVersion: apis.kcp.io/v1alpha1
kind: APIBinding
metadata:
  name: orchestrate.platform-mesh.io
spec:
  reference:
    export:
      name: orchestrate.platform-mesh.io
      path: "root:providers:httpbin-provider"
  permissionClaims:
  - resource: namespaces
    state: Accepted
    all: true
  - resource: events
    state: Accepted
    all: true
EOF

Verify the binding:

KUBECONFIG=$CONSUMER_KUBECONFIG kubectl get apibindings

Verify the API is available:

KUBECONFIG=$CONSUMER_KUBECONFIG kubectl api-resources | grep httpbin

Create an HttpBin instance:

KUBECONFIG=$CONSUMER_KUBECONFIG kubectl apply -f - <<EOF
apiVersion: orchestrate.platform-mesh.io/v1alpha1
kind: HttpBin
metadata:
  name: my-httpbin
  namespace: default
spec:
  region: eu-west-1
EOF

Check the service cluster for the synchronized resource:

KUBECONFIG=$COMPUTE_KUBECONFIG kubectl get httpbins --all-namespaces
KUBECONFIG=$COMPUTE_KUBECONFIG kubectl get pods --all-namespaces | grep httpbin

Check status back in kcp:

KUBECONFIG=$CONSUMER_KUBECONFIG kubectl get httpbin my-httpbin -n default -o yaml

Look for a populated status section with readiness and URL information. If status is still empty, wait a few seconds and check the operator and api-syncagent logs.

Review the data flow

sequenceDiagram
    participant Consumer as Consumer workspace
    participant Kcp as kcp APIExport
    participant Agent as api-syncagent
    participant Operator as HttpBin operator
    participant Cluster as Service cluster

    Note over Consumer,Cluster: Setup
    Kcp->>Kcp: APIExport created
    Agent->>Kcp: Publish APIResourceSchema
    Consumer->>Kcp: Create APIBinding

    Note over Consumer,Cluster: Runtime
    Consumer->>Kcp: Create HttpBin spec
    Kcp-->>Agent: Expose resource through virtual workspace
    Agent->>Cluster: Sync HttpBin spec
    Operator->>Cluster: Reconcile pod and service
    Cluster-->>Agent: Update local HttpBin status
    Agent->>Kcp: Sync status back
    Kcp->>Consumer: Expose ready status and URL

The HttpBin operator did not need Platform Mesh-specific code. api-syncagent handled the integration path by publishing the CRD to kcp and synchronizing resources between the consumer workspace and the service cluster.

Marketplace

So far the HttpBin API works end-to-end on the command line, but the Platform Mesh portal still has no idea it exists. To make the provider discoverable in the Marketplace and to add a navigation entry for the resource in the UI, you create two resources in the provider workspace:

Resource Purpose
ProviderMetadata Registers your provider with the Marketplace — display name, description, contacts, icons.
ContentConfiguration Adds Luigi navigation nodes and views to the portal for your APIExport.

Both resources live in the provider workspace alongside the APIExport:

KUBECONFIG=$PM_KUBECONFIG kubectl kcp workspace use :root:providers:httpbin-provider

Register the provider

Apply a ProviderMetadata whose name matches the APIExport name. The Marketplace virtual workspace joins these two on name to build a MarketplaceEntry.

KUBECONFIG=$PM_KUBECONFIG kubectl apply -f - <<EOF
apiVersion: ui.platform-mesh.io/v1alpha1
kind: ProviderMetadata
metadata:
  name: orchestrate.platform-mesh.io
spec:
  displayName: HttpBin Provider
  description: |
    Example provider for the Platform Mesh quickstart.
    Provisions HttpBin instances on a service cluster.
  contacts:
    - displayName: HttpBin Team
      email: httpbin@platform-mesh.io
      role: ["Maintainer"]
  preferredSupportChannels:
    - displayName: HttpBin Support
      url: https://github.com/platform-mesh/platform-mesh.github.io
  icon:
    light:
      data: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMDAgMjAwIj4KICA8Y2lyY2xlIGN4PSIxMDAiIGN5PSIxMDAiIHI9IjkwIiBmaWxsPSIjMmM3YmU1IiAvPgogIDx0ZXh0IHg9IjEwMCIgeT0iMTE1IiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iNDAiIGZvbnQtd2VpZ2h0PSJib2xkIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjZmZmIj5IVFRQPC90ZXh0Pgo8L3N2Zz4K"
    dark:
      data: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMDAgMjAwIj4KICA8Y2lyY2xlIGN4PSIxMDAiIGN5PSIxMDAiIHI9IjkwIiBmaWxsPSIjMmM3YmU1IiAvPgogIDx0ZXh0IHg9IjEwMCIgeT0iMTE1IiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iNDAiIGZvbnQtd2VpZ2h0PSJib2xkIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjZmZmIj5IVFRQPC90ZXh0Pgo8L3N2Zz4K"
EOF

::: tip Matching names The metadata.name of the ProviderMetadata must match the APIExport name (orchestrate.platform-mesh.io). The Marketplace builds entries by joining the two. :::

Add a UI navigation entry

ContentConfiguration carries a Luigi config fragment that the portal merges into its navigation tree. The ui.platform-mesh.io/content-for label links the configuration to your APIExport — without it, the portal will not load this fragment when the consumer binds the API.

KUBECONFIG=$PM_KUBECONFIG kubectl apply -f - <<EOF
apiVersion: ui.platform-mesh.io/v1alpha1
kind: ContentConfiguration
metadata:
  labels:
    ui.platform-mesh.io/entity: core_platform-mesh_io_account
    ui.platform-mesh.io/content-for: orchestrate.platform-mesh.io
  name: httpbin-ui
spec:
  inlineConfiguration:
    content: |-
      {
          "name": "httpbins",
          "creationTimestamp": "2022-05-17T11:37:17Z",
          "luigiConfigFragment": {
              "data": {
                  "nodes": [
                      {
                          "pathSegment": "orchestrate_platform-mesh_io_httpbins",
                          "navigationContext": "orchestrate_platform-mesh_io_httpbins",
                          "label": "Http Bins",
                          "icon": "paint-bucket",
                          "order": 800,
                          "entityType": "main.core_platform-mesh_io_account",
                          "loadingIndicator": {
                              "enabled": false
                          },
                          "keepSelectedForChildren": true,
                          "url": "/assets/platform-mesh-portal-ui-wc.js#generic-list-view",
                          "webcomponent": {
                              "selfRegistered": true
                          },
                          "context": {
                              "resourceDefinition": {
                                  "apiGroup": "orchestrate_platform_mesh_io",
                                  "entityCollection": "HttpBins",
                                  "version": "v1alpha1",
                                  "entity": "HttpBin",
                                  "scope": "Namespaced",
                                  "namespace": null,
                                  "readyCondition": {
                                    "jsonPathExpression": "status.ready",
                                    "property": ["status.ready"]
                                  },
                                  "ui": {
                                      "logoUrl": "https://www.kcp.io/icons/logo.svg",
                                      "listView": {
                                          "fields": [
                                              {
                                                  "label": "Name",
                                                  "property": "metadata.name"
                                              },
                                              {
                                                  "label": "Ready",
                                                  "property": "status.ready",
                                                  "uiSettings": {
                                                    "displayAs": "boolIcon"
                                                  }
                                              },
                                              {
                                                  "label": "Link",
                                                  "property": "status.url",
                                                  "uiSettings": {
                                                    "displayAs": "link"
                                                  }
                                              }
                                          ]
                                      },
                                      "detailView": {
                                        "fields": [
                                          {
                                            "label": "Name",
                                            "property": "metadata.name"
                                          }
                                        ]
                                      },
                                      "createView": {
                                          "fields": [
                                              {
                                                  "label": "Name",
                                                  "property": "metadata.name",
                                                  "required": true
                                              }
                                          ]
                                      }
                                  }
                              }
                          },
                          "children": [
                            {
                                "pathSegment": ":httpbinId",
                                "hideFromNav": true,
                                "keepSelectedForChildren": false,
                                "defineEntity": {
                                    "id": "orchestrate_platform-mesh_io_httpbin",
                                    "contextKey": "httpbinId",
                                    "graphqlEntity": {
                                        "group": "orchestrate_platform-mesh_io",
                                        "version": "v1alpha1",
                                        "kind": "HttpBin",
                                        "query": "{ metadata { name } }"
                                    }
                                },
                                "context": {
                                    "accountId": ":accountId",
                                    "httpbinId": ":httpbinId",
                                    "resourceId": ":httpbinId"
                                }
                            }
                          ]
                      },
                      {
                        "entityType": "main.core_platform-mesh_io_account.orchestrate_platform-mesh_io_httpbin",
                        "pathSegment": "dashboard",
                        "label": "Dashboard",
                        "url": "/assets/platform-mesh-portal-ui-wc.js#generic-detail-view",
                        "webcomponent": {
                          "selfRegistered": true
                        },
                        "defineEntity": {
                            "id": "dashboard"
                        },
                        "compound": {
                            "children": []
                        }
                      }
                  ]
              }
          }
      }
    contentType: json
EOF

See it in the portal

Open the portal at https://portal.localhost:8443 as the consumer account you created earlier. Under Marketplace, the HttpBin Provider tile should now appear. Clicking Install creates an APIBinding in the consumer workspace — equivalent to the manifest you applied by hand in the Test the consumer flow step. After installation, a HttpBins entry appears in the left navigation, listing the resources you created.

Next

Continue with Consume a service from a controller to build the consumer side of the loop you just opened.

Optional branches: