惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Security Archives - TechRepublic
Security Archives - TechRepublic
S
Secure Thoughts
V2EX - 技术
V2EX - 技术
Schneier on Security
Schneier on Security
Application and Cybersecurity Blog
Application and Cybersecurity Blog
L
LangChain Blog
博客园_首页
Jina AI
Jina AI
IT之家
IT之家
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tenable Blog
量子位
V
V2EX
酷 壳 – CoolShell
酷 壳 – CoolShell
S
Security Affairs
Last Week in AI
Last Week in AI
Scott Helme
Scott Helme
月光博客
月光博客
D
Darknet – Hacking Tools, Hacker News & Cyber Security
博客园 - 叶小钗
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
Tailwind CSS Blog
Simon Willison's Weblog
Simon Willison's Weblog
PCI Perspectives
PCI Perspectives
人人都是产品经理
人人都是产品经理
N
News and Events Feed by Topic
腾讯CDC
P
Proofpoint News Feed
T
The Exploit Database - CXSecurity.com
J
Java Code Geeks
博客园 - 司徒正美
博客园 - Franky
Latest news
Latest news
S
SegmentFault 最新的问题
小众软件
小众软件
博客园 - 三生石上(FineUI控件)
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
I
Intezer
Attack and Defense Labs
Attack and Defense Labs
H
Heimdal Security Blog
H
Hacker News: Front Page
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 【当耐特】
T
Troy Hunt's Blog
N
News | PayPal Newsroom
P
Palo Alto Networks Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Recent Commits to openclaw:main
Recent Commits to openclaw:main
雷峰网
雷峰网

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Getting Started with the Cluster Inventory API on OCM (Part 1: ClusterProfile)
Kahiro Okina · 2026-05-05 · via DEV Community

This is Part 1 of a two-part series.
Part 2 (coming soon): Connecting to spoke clusters from a controller using multicluster-runtime, driven by ClusterProfile.

What this article is about

The Cluster Inventory API (multicluster.x-k8s.io) is driven by SIG-Multicluster and centered on the ClusterProfile resource. It only delivers value when something produces those ClusterProfiles. That something is a cluster manager. Today, the production-ready open-source option is Open Cluster Management (OCM), whose registration controller can act as a ClusterProfile cluster manager behind a feature gate.

This article shows how to set that up end-to-end on a local kind environment:

  • An OCM hub-spoke setup with three kind clusters.
  • The ClusterProfile feature gate enabled on the hub.
  • cluster-proxy wired in so ClusterProfile.status.accessProviders carries real, usable connection info.
  • ClusterProperty from the spokes flowing into ClusterProfile.status.properties on the hub.

By the end, you'll have a working multicluster.x-k8s.io/v1alpha1 ClusterProfile inventory that any Cluster Inventory API consumer can read. That is exactly what Part 2 will plug into via multicluster-runtime.

Overview

The setup looks like this:

  • Three kind clusters: hub, cluster1, cluster2.
  • The OCM hub manages cluster1 and cluster2 as managed clusters.
  • The managed clusters belong to a ManagedClusterSet named sandbox-fleet.
  • The cluster-proxy and managed-serviceaccount addons are installed on the spokes.
  • ClusterProfile resources are created in the cluster-inventory namespace.
  • ClusterProperty values from the spokes flow into ClusterProfile.status.properties on the hub.

The synchronization path for ClusterProperty is shown below. The boxes name the actual OCM components for reference, but the only thing you need to take away is the direction: a property set on a spoke ends up in ClusterProfile.status.properties on the hub.


flowchart LR
  subgraph spoke["cluster1 / cluster2"]
    property["ClusterProperty<br/>about.k8s.io/v1alpha1"]
    agent["klusterlet-registration-agent"]
  end

  subgraph hub["hub"]
    managedCluster["ManagedCluster<br/>status.clusterClaims"]
    profileController["cluster-manager-registration-controller<br/>ClusterProfileStatusController"]
    clusterProfile["cluster-inventory/ClusterProfile<br/>status.properties"]
  end

  property --> agent
  agent --> managedCluster
  managedCluster --> profileController
  profileController --> clusterProfile

Enter fullscreen mode Exit fullscreen mode

In plain terms: spoke ClusterProperty flows to hub ManagedCluster.status.clusterClaims, then to hub ClusterProfile.status.properties. That last hop is what makes OCM a Cluster Inventory API cluster manager. Properties you set on a spoke become inventory data any consumer can read on the hub through a vendor-neutral API.

The commands in this article were verified with:

kind v0.31.0
clusteradm v1.2.1
helm v3.20.1
kubectl v1.35.3

Enter fullscreen mode Exit fullscreen mode

Prerequisites

The following commands must be available locally.

If clusteradm is not installed:

curl -L https://raw.githubusercontent.com/open-cluster-management-io/clusteradm/main/install.sh | bash

Enter fullscreen mode Exit fullscreen mode

Verify the installed tools:

kind version
helm version
kubectl version --client
clusteradm version

Enter fullscreen mode Exit fullscreen mode

clusteradm version also tries to connect to the current kubeconfig context to fetch the server version. At this point, just verify that the client version prints.

This article uses the following Kubernetes context names:

export HUB_CTX=kind-hub
export C1_CTX=kind-cluster1
export C2_CTX=kind-cluster2

Enter fullscreen mode Exit fullscreen mode

Clone the OCM repository

The OCM repository contains a script for creating a local development environment. This article uses solutions/setup-dev-environment/local-up.sh.

git clone https://github.com/open-cluster-management-io/ocm.git
cd ocm

Enter fullscreen mode Exit fullscreen mode

Create the hub / cluster1 / cluster2 kind clusters

local-up.sh runs the following setup steps:

  • Creates the hub, cluster1, and cluster2 kind clusters.
  • Initializes the hub with clusteradm init.
  • Registers the spoke clusters with clusteradm join.
  • Accepts the join requests with clusteradm accept.
./solutions/setup-dev-environment/local-up.sh

Enter fullscreen mode Exit fullscreen mode

After the script completes, check the managed clusters from the hub:

kubectl config use-context kind-hub
kubectl get managedclusters --context "$HUB_CTX"

Enter fullscreen mode Exit fullscreen mode

In this verified environment:

NAME       HUB ACCEPTED   MANAGED CLUSTER URLS                  JOINED   AVAILABLE   AGE
cluster1   true           https://cluster1-control-plane:6443   True     True        2m
cluster2   true           https://cluster2-control-plane:6443   True     True        2m

Enter fullscreen mode Exit fullscreen mode

JOINED and AVAILABLE are True for both spokes. This is the starting point for managing them from the hub.

Install the cluster-proxy addon

We install cluster-proxy for two reasons. The first is that the hub needs to reach spoke APIs. The second, which matters more for this article, is that ClusterProfile.status.accessProviders needs a real connection endpoint that downstream Cluster Inventory API consumers can actually use.

Add the OCM Helm repository:

helm repo add ocm https://open-cluster-management.io/helm-charts/
helm repo update

Enter fullscreen mode Exit fullscreen mode

We create a development fleet named sandbox-fleet as a ManagedClusterSet, then add cluster1 and cluster2 to it. The same fleet is used as the target for addon distribution and ClusterProfile creation.

OCM uses two resources for cluster-set handling:

  • ManagedClusterSet decides which ManagedCluster belongs to the set.
  • ManagedClusterSetBinding makes the set usable from a specific namespace.

sandbox-fleet uses ExclusiveClusterSetLabel. When a ManagedCluster has the label cluster.open-cluster-management.io/clusterset=sandbox-fleet, that cluster belongs to sandbox-fleet.

Note on existing cluster sets. Some environments already have ManagedClusterSet resources named default or global, managed by the OCM DefaultClusterSet feature gate. global in particular uses an empty label selector and selects all managed clusters, so it overlaps with sandbox-fleet. This article consistently targets sandbox-fleet for addons and ClusterProfile to keep things unambiguous.

printf '%s\n' \
  'apiVersion: cluster.open-cluster-management.io/v1beta2' \
  'kind: ManagedClusterSet' \
  'metadata:' \
  '  name: sandbox-fleet' \
  'spec:' \
  '  clusterSelector:' \
  '    selectorType: ExclusiveClusterSetLabel' | \
  kubectl apply --context "$HUB_CTX" -f -

kubectl label managedcluster cluster1 \
  cluster.open-cluster-management.io/clusterset=sandbox-fleet \
  --overwrite \
  --context "$HUB_CTX"

kubectl label managedcluster cluster2 \
  cluster.open-cluster-management.io/clusterset=sandbox-fleet \
  --overwrite \
  --context "$HUB_CTX"

Enter fullscreen mode Exit fullscreen mode

cluster-proxy v0.10.0 introduced support for configuring ClusterProfile access providers. That is the feature we need here, and it's why we pin to v0.10.0 so Part 2 can use ClusterProfile for dynamic access.

Why v0.10.0 needs a workaround

The latest ocm/cluster-proxy chart in the Helm repository is v0.10.0, but that chart has a schema mismatch: it renders spec.proxyAgent.additionalValues in ManagedProxyConfiguration, while the v0.10.0 CRD schema does not declare that field. Running helm install as-is fails with:

failed to create typed patch object (... ManagedProxyConfiguration): .spec.proxyAgent.additionalValues: field not declared in schema

The cluster-proxy main branch removed this output in Fix chart error. (#272). Once the next chart release ships, this workaround can be dropped.

Helm v3 can pass an executable path to --post-renderer, but Helm v4 changed post-renderers to plugins and no longer accepts a raw path directly. To stay compatible with both, this article uses helm template | kubectl apply and strips the offending field with perl.

Install cluster-proxy on the hub. Enabling ClusterProfileAccessProvider and userServer.enabled makes cluster-proxy connection information appear in ClusterProfile.status.accessProviders. That is the bridge between the inventory and real spoke API access:

kubectl create namespace open-cluster-management-addon \
  --context "$HUB_CTX" \
  --dry-run=client \
  -o yaml | kubectl apply --context "$HUB_CTX" -f -

printf '%s\n' \
  'apiVersion: cluster.open-cluster-management.io/v1beta2' \
  'kind: ManagedClusterSetBinding' \
  'metadata:' \
  '  name: sandbox-fleet' \
  '  namespace: open-cluster-management-addon' \
  'spec:' \
  '  clusterSet: sandbox-fleet' | \
  kubectl apply --context "$HUB_CTX" -f -

printf '%s\n' \
  'apiVersion: cluster.open-cluster-management.io/v1beta1' \
  'kind: Placement' \
  'metadata:' \
  '  name: cluster-proxy-placement' \
  '  namespace: open-cluster-management-addon' \
  'spec:' \
  '  clusterSets:' \
  '    - sandbox-fleet' | \
  kubectl apply --context "$HUB_CTX" -f -

Enter fullscreen mode Exit fullscreen mode

ManagedProxyConfiguration is a CRD provided by the cluster-proxy chart. Apply the CRDs first and wait for them to become Established. If the CRD and CR are sent through the same kubectl apply stream, Kubernetes discovery may not see the new CRD in time and can return no matches for kind "ManagedProxyConfiguration".

helm show crds ocm/cluster-proxy --version 0.10.0 | \
  kubectl apply --context "$HUB_CTX" -f -

kubectl wait --for=condition=Established \
  crd/managedproxyconfigurations.proxy.open-cluster-management.io \
  --context "$HUB_CTX" \
  --timeout=120s

kubectl wait --for=condition=Established \
  crd/managedproxyserviceresolvers.proxy.open-cluster-management.io \
  --context "$HUB_CTX" \
  --timeout=120s

helm template cluster-proxy ocm/cluster-proxy \
  -n open-cluster-management-addon \
  --version 0.10.0 \
  --set installByPlacement.placementName=cluster-proxy-placement \
  --set installByPlacement.placementNamespace=open-cluster-management-addon \
  --set featureGates.clusterProfileAccessProvider=true \
  --set userServer.enabled=true | \
  perl -0pe 's/\n    additionalValues:\n      enableImpersonation: "[^"]+"//g' | \
  kubectl apply --context "$HUB_CTX" -f -

Enter fullscreen mode Exit fullscreen mode

Wait for ManagedProxyConfiguration/cluster-proxy and the certificate Secrets used by clusteradm proxy:

kubectl wait --for=create \
  managedproxyconfiguration/cluster-proxy \
  --context "$HUB_CTX" \
  --timeout=120s

kubectl wait --for=create \
  secret/proxy-client \
  -n open-cluster-management-addon \
  --context "$HUB_CTX" \
  --timeout=120s

kubectl wait --for=create \
  secret/proxy-server \
  -n open-cluster-management-addon \
  --context "$HUB_CTX" \
  --timeout=120s

kubectl wait --for=create \
  secret/agent-server \
  -n open-cluster-management-addon \
  --context "$HUB_CTX" \
  --timeout=120s

Enter fullscreen mode Exit fullscreen mode

Check the addon status:

kubectl rollout status deployment/cluster-proxy-addon-manager \
  -n open-cluster-management-addon \
  --context "$HUB_CTX" \
  --timeout=180s

kubectl rollout status deployment/cluster-proxy \
  -n open-cluster-management-addon \
  --context "$HUB_CTX" \
  --timeout=180s

clusteradm get addon cluster-proxy --context "$HUB_CTX"
kubectl get managedclusteraddon -A --context "$HUB_CTX" | grep cluster-proxy

kubectl wait --for=condition=Available \
  managedclusteraddon/cluster-proxy \
  -n cluster1 \
  --context "$HUB_CTX" \
  --timeout=180s

kubectl wait --for=condition=Available \
  managedclusteraddon/cluster-proxy \
  -n cluster2 \
  --context "$HUB_CTX" \
  --timeout=180s

Enter fullscreen mode Exit fullscreen mode

Install the managed-serviceaccount addon

Install the managed-serviceaccount addon so clusteradm proxy kubectl can access the spoke clusters with a managed service account.

The managed-serviceaccount chart also defaults to the global cluster set and does not expose a Helm value to change it. We set agentInstallAll=false to disable automatic distribution, then explicitly create ManagedClusterAddOn resources for the target clusters:

helm install \
  --kube-context "$HUB_CTX" \
  -n open-cluster-management-managed-serviceaccount \
  --create-namespace \
  managed-serviceaccount \
  ocm/managed-serviceaccount \
  --set agentInstallAll=false

printf '%s\n' \
  'apiVersion: addon.open-cluster-management.io/v1alpha1' \
  'kind: ManagedClusterAddOn' \
  'metadata:' \
  '  name: managed-serviceaccount' \
  '  namespace: cluster1' \
  'spec:' \
  '  installNamespace: open-cluster-management-managed-serviceaccount' | \
  kubectl apply --context "$HUB_CTX" -f -

printf '%s\n' \
  'apiVersion: addon.open-cluster-management.io/v1alpha1' \
  'kind: ManagedClusterAddOn' \
  'metadata:' \
  '  name: managed-serviceaccount' \
  '  namespace: cluster2' \
  'spec:' \
  '  installNamespace: open-cluster-management-managed-serviceaccount' | \
  kubectl apply --context "$HUB_CTX" -f -

Enter fullscreen mode Exit fullscreen mode

This flow creates ManagedClusterAddOn resources directly. Check the clusters in sandbox-fleet and verify the addon targets line up with the fleet:

kubectl get managedclusters \
  --context "$HUB_CTX" \
  -L cluster.open-cluster-management.io/clusterset

Enter fullscreen mode Exit fullscreen mode

Check the addon status:

kubectl rollout status deployment/managed-serviceaccount-addon-manager \
  -n open-cluster-management-managed-serviceaccount \
  --context "$HUB_CTX" \
  --timeout=180s

clusteradm get addon managed-serviceaccount --context "$HUB_CTX"
kubectl get managedclusteraddon -A --context "$HUB_CTX" | grep managed-serviceaccount

kubectl wait --for=condition=Available \
  managedclusteraddon/managed-serviceaccount \
  -n cluster1 \
  --context "$HUB_CTX" \
  --timeout=180s

kubectl wait --for=condition=Available \
  managedclusteraddon/managed-serviceaccount \
  -n cluster2 \
  --context "$HUB_CTX" \
  --timeout=180s

Enter fullscreen mode Exit fullscreen mode

Check the proxy path health:

clusteradm proxy health --context "$HUB_CTX"

Enter fullscreen mode Exit fullscreen mode

Create a ManagedServiceAccount

The next three sections (Create a ManagedServiceAccount, Distribute RBAC to the spoke, Access the spoke API with clusteradm proxy) are a side quest, not Cluster Inventory API itself. They exist to verify that the connection endpoint that will appear in ClusterProfile.status.accessProviders is actually reachable. If you only care about the inventory data and plan to drive access from a controller later, you can skim these and pick up at Enable the ClusterProfile feature gate.

Create a ManagedServiceAccount named test for cluster1:

printf '%s\n' \
  'apiVersion: authentication.open-cluster-management.io/v1beta1' \
  'kind: ManagedServiceAccount' \
  'metadata:' \
  '  name: test' \
  '  namespace: cluster1' \
  'spec:' \
  '  rotation: {}' | \
  kubectl apply --context "$HUB_CTX" -f -

Enter fullscreen mode Exit fullscreen mode

Check that it has been created:

kubectl get managedserviceaccount -n cluster1 --context "$HUB_CTX"

Enter fullscreen mode Exit fullscreen mode

Wait for the hub-side Secret for the managed service account:

kubectl wait --for=create \
  secret/test \
  -n cluster1 \
  --context "$HUB_CTX" \
  --timeout=120s

Enter fullscreen mode Exit fullscreen mode

Check the status conditions and verify that the token Secret has been reported:

kubectl get managedserviceaccount test \
  -n cluster1 \
  --context "$HUB_CTX" \
  -o jsonpath='{range .status.conditions[*]}{.type}={.status}{"\n"}{end}'

Enter fullscreen mode Exit fullscreen mode

Distribute RBAC to the spoke

On the spoke cluster, the ManagedServiceAccount is realized as a regular Kubernetes ServiceAccount in the namespace specified by ManagedClusterAddOn.spec.installNamespace, which is open-cluster-management-managed-serviceaccount:

kubectl get serviceaccount test \
  -n open-cluster-management-managed-serviceaccount \
  --context "$C1_CTX"

Enter fullscreen mode Exit fullscreen mode

This verification grants cluster-admin. In production, grant only the Role or ClusterRole required for the target APIs.

printf '%s\n' \
  'apiVersion: rbac.authorization.k8s.io/v1' \
  'kind: ClusterRoleBinding' \
  'metadata:' \
  '  name: managed-sa-test' \
  'roleRef:' \
  '  apiGroup: rbac.authorization.k8s.io' \
  '  kind: ClusterRole' \
  '  name: cluster-admin' \
  'subjects:' \
  '  - kind: ServiceAccount' \
  '    name: test' \
  '    namespace: open-cluster-management-managed-serviceaccount' \
  > /tmp/clusterrolebinding-managed-sa-test.yaml

Enter fullscreen mode Exit fullscreen mode

Use clusteradm create work to apply the RBAC to cluster1:

clusteradm create work managed-sa-test-rbac \
  -f /tmp/clusterrolebinding-managed-sa-test.yaml \
  --clusters cluster1 \
  --context "$HUB_CTX"

Enter fullscreen mode Exit fullscreen mode

Check the ManifestWork and the RBAC on the spoke cluster:

kubectl get manifestwork -n cluster1 --context "$HUB_CTX"
kubectl wait --for=condition=Applied \
  manifestwork/managed-sa-test-rbac \
  -n cluster1 \
  --context "$HUB_CTX" \
  --timeout=60s
kubectl get clusterrolebinding managed-sa-test --context "$C1_CTX"

Enter fullscreen mode Exit fullscreen mode

Access the spoke API with clusteradm proxy

At this point, use clusteradm proxy kubectl from the hub side to access the cluster1 API:

clusteradm proxy kubectl \
  --context "$HUB_CTX" \
  --cluster=cluster1 \
  --sa=test \
  --args="get nodes"

Enter fullscreen mode Exit fullscreen mode

If the nodes in cluster1 are returned, hub-to-spoke API access through cluster-proxy and managed-serviceaccount is working.

Create the same ManagedServiceAccount and RBAC for cluster2:

printf '%s\n' \
  'apiVersion: authentication.open-cluster-management.io/v1beta1' \
  'kind: ManagedServiceAccount' \
  'metadata:' \
  '  name: test' \
  '  namespace: cluster2' \
  'spec:' \
  '  rotation: {}' | \
  kubectl apply --context "$HUB_CTX" -f -

clusteradm create work managed-sa-test-rbac \
  -f /tmp/clusterrolebinding-managed-sa-test.yaml \
  --clusters cluster2 \
  --context "$HUB_CTX"

kubectl wait --for=condition=Applied \
  manifestwork/managed-sa-test-rbac \
  -n cluster2 \
  --context "$HUB_CTX" \
  --timeout=60s

clusteradm proxy kubectl \
  --context "$HUB_CTX" \
  --cluster=cluster2 \
  --sa=test \
  --args="get nodes"

Enter fullscreen mode Exit fullscreen mode

Enable the ClusterProfile feature gate

Now we get to the headline feature: turning on the Cluster Inventory API.

ClusterProfile is handled by the hub-side registration controller. In the OCM repository, the feature gate name is ClusterProfile. We configure the registration feature gate on ClusterManager.

First, check the existing feature gates:

kubectl get clustermanager cluster-manager \
  --context "$HUB_CTX" \
  -o jsonpath='{range .spec.registrationConfiguration.featureGates[*]}{.feature}={.mode}{"\n"}{end}'

Enter fullscreen mode Exit fullscreen mode

kubectl patch clustermanager.operator.open-cluster-management.io cluster-manager \
  --context "$HUB_CTX" \
  --type=merge \
  -p '{"spec":{"registrationConfiguration":{"featureGates":[{"feature":"ResourceCleanup","mode":"Enable"},{"feature":"ClusterProfile","mode":"Enable"}]}}}'

Enter fullscreen mode Exit fullscreen mode

In the local-up.sh verification environment, ResourceCleanup was already configured, so the patch keeps it and adds ClusterProfile. Note that the featureGates array is replaced wholesale by this patch. If your environment explicitly configures additional feature gates, keep those entries in the same array.

With local-up.sh, the registration controller is the cluster-manager-registration-controller Deployment in the open-cluster-management-hub namespace. Check the name and wait for rollout:

kubectl get deployment \
  -n open-cluster-management-hub \
  --context "$HUB_CTX"

kubectl rollout status deployment/cluster-manager-registration-controller \
  -n open-cluster-management-hub \
  --context "$HUB_CTX" \
  --timeout=180s

Enter fullscreen mode Exit fullscreen mode

Check the ClusterManager configuration:

kubectl get clustermanager cluster-manager \
  --context "$HUB_CTX" \
  -o yaml

Enter fullscreen mode Exit fullscreen mode

Check that the ClusterProfile CRD exists:

sleep 10

kubectl wait --for=condition=Established \
  crd/clusterprofiles.multicluster.x-k8s.io \
  --context "$HUB_CTX" \
  --timeout=120s

Enter fullscreen mode Exit fullscreen mode

Check the API resource:

kubectl api-resources --context "$HUB_CTX" | grep -i clusterprofile

Enter fullscreen mode Exit fullscreen mode

The API group is multicluster.x-k8s.io, the SIG-Multicluster Cluster Inventory API itself. From here on, anything we put into ClusterProfile is consumable by tools that target the upstream API, not just OCM-specific ones.

Create the ManagedClusterSet binding

ClusterProfile resources are created in namespaces that have a ManagedClusterSetBinding. We use the previously created sandbox-fleet cluster set and bind it to a cluster-inventory namespace.

The label cluster.open-cluster-management.io/clusterset=sandbox-fleet puts a cluster into sandbox-fleet. The ManagedClusterSetBinding we create here makes sandbox-fleet referenceable from the cluster-inventory namespace. The ClusterProfile controller watches the binding in that namespace and creates ClusterProfile resources for the target clusters.

First, confirm that the sandbox-fleet cluster set exists and that cluster1 and cluster2 belong to it:

kubectl get managedclusterset sandbox-fleet --context "$HUB_CTX"
kubectl get managedclusters \
  --context "$HUB_CTX" \
  -L cluster.open-cluster-management.io/clusterset

Enter fullscreen mode Exit fullscreen mode

Create the namespace where ClusterProfile resources will be created:

kubectl create namespace cluster-inventory \
  --dry-run=client \
  -o yaml | kubectl apply --context "$HUB_CTX" -f -

Enter fullscreen mode Exit fullscreen mode

Bind the sandbox-fleet cluster set to the cluster-inventory namespace:

printf '%s\n' \
  'apiVersion: cluster.open-cluster-management.io/v1beta2' \
  'kind: ManagedClusterSetBinding' \
  'metadata:' \
  '  name: sandbox-fleet' \
  '  namespace: cluster-inventory' \
  'spec:' \
  '  clusterSet: sandbox-fleet' | \
  kubectl apply --context "$HUB_CTX" -f -

Enter fullscreen mode Exit fullscreen mode

Wait until the binding is Bound:

kubectl wait --for=condition=Bound \
  managedclustersetbinding/sandbox-fleet \
  -n cluster-inventory \
  --context "$HUB_CTX" \
  --timeout=60s

kubectl get managedclustersetbinding sandbox-fleet \
  -n cluster-inventory \
  --context "$HUB_CTX" \
  -o yaml

Enter fullscreen mode Exit fullscreen mode

The condition contains Bound=True.

Check ClusterProfile

ClusterProfile is a namespaced resource in multicluster.x-k8s.io/v1alpha1. With the previous steps, it is created in the cluster-inventory namespace:

kubectl wait --for=create \
  clusterprofile/cluster1 \
  -n cluster-inventory \
  --context "$HUB_CTX" \
  --timeout=120s

kubectl wait --for=create \
  clusterprofile/cluster2 \
  -n cluster-inventory \
  --context "$HUB_CTX" \
  --timeout=120s

Enter fullscreen mode Exit fullscreen mode

kubectl get clusterprofiles \
  -n cluster-inventory \
  --context "$HUB_CTX"

Enter fullscreen mode Exit fullscreen mode

ClusterProfile resources for cluster1 and cluster2 have been created:

NAME       AGE
cluster1   1m
cluster2   1m

Enter fullscreen mode Exit fullscreen mode

In this implementation, the created ClusterProfile has spec.clusterManager.name set to open-cluster-management and spec.displayName set to the managed cluster name. Extract those fields with jsonpath:

kubectl get clusterprofile cluster1 \
  -n cluster-inventory \
  --context "$HUB_CTX" \
  -o jsonpath='{.spec.clusterManager.name}{"\n"}{.spec.displayName}{"\n"}'

Enter fullscreen mode Exit fullscreen mode

The status synchronizes the Kubernetes version from ManagedCluster, properties derived from cluster claims, and conditions:

kubectl get clusterprofile cluster1 \
  -n cluster-inventory \
  --context "$HUB_CTX" \
  -o jsonpath='{.status.version.kubernetes}{"\n"}'

kubectl get clusterprofile cluster1 \
  -n cluster-inventory \
  --context "$HUB_CTX" \
  -o jsonpath='{range .status.properties[*]}{.name}={.value}{"\n"}{end}'

kubectl get clusterprofile cluster1 \
  -n cluster-inventory \
  --context "$HUB_CTX" \
  -o jsonpath='{range .status.conditions[*]}{.type}={.status}{"\n"}{end}'

Enter fullscreen mode Exit fullscreen mode

Create ClusterProperty and reflect it in ClusterProfile

ClusterProfile.status.properties also reflects ClusterProperty resources from spoke clusters. ClusterProperty is a cluster-scoped resource in about.k8s.io/v1alpha1. It is another SIG-Multicluster API, designed for spoke clusters to declare facts about themselves (region, account ID, environment, anything you'd otherwise jam into labels).

This is the punchline of the article: a property you set on the spoke flows all the way to the inventory entry on the hub, with no custom glue code.

First, enable the ClusterProperty feature gate on the spoke-side Klusterlets. Keep the ClusterClaim and AddonManagement feature gates that local-up.sh already enabled, and add ClusterProperty:

kubectl patch klusterlet klusterlet \
  --context "$C1_CTX" \
  --type=merge \
  -p '{"spec":{"registrationConfiguration":{"featureGates":[{"feature":"ClusterClaim","mode":"Enable"},{"feature":"ClusterProperty","mode":"Enable"},{"feature":"AddonManagement","mode":"Enable"}]}}}'

kubectl patch klusterlet klusterlet \
  --context "$C2_CTX" \
  --type=merge \
  -p '{"spec":{"registrationConfiguration":{"featureGates":[{"feature":"ClusterClaim","mode":"Enable"},{"feature":"ClusterProperty","mode":"Enable"},{"feature":"AddonManagement","mode":"Enable"}]}}}'

Enter fullscreen mode Exit fullscreen mode

Wait for the ClusterProperty CRD and for the registration agent rollout:

kubectl wait --for=condition=Established \
  crd/clusterproperties.about.k8s.io \
  --context "$C1_CTX" \
  --timeout=120s

kubectl wait --for=condition=Established \
  crd/clusterproperties.about.k8s.io \
  --context "$C2_CTX" \
  --timeout=120s

kubectl rollout status deployment/klusterlet-registration-agent \
  -n open-cluster-management-agent \
  --context "$C1_CTX" \
  --timeout=180s

kubectl rollout status deployment/klusterlet-registration-agent \
  -n open-cluster-management-agent \
  --context "$C2_CTX" \
  --timeout=180s

Enter fullscreen mode Exit fullscreen mode

Create AWS region and AWS account ID examples as ClusterProperty resources on the spoke clusters. The values are dummy values for verification:

printf '%s\n' \
  'apiVersion: about.k8s.io/v1alpha1' \
  'kind: ClusterProperty' \
  'metadata:' \
  '  name: aws.region.example.com' \
  'spec:' \
  '  value: ap-northeast-1' \
  '---' \
  'apiVersion: about.k8s.io/v1alpha1' \
  'kind: ClusterProperty' \
  'metadata:' \
  '  name: aws.account-id.example.com' \
  'spec:' \
  '  value: "111122223333"' | \
  kubectl apply --context "$C1_CTX" -f -

printf '%s\n' \
  'apiVersion: about.k8s.io/v1alpha1' \
  'kind: ClusterProperty' \
  'metadata:' \
  '  name: aws.region.example.com' \
  'spec:' \
  '  value: us-west-2' \
  '---' \
  'apiVersion: about.k8s.io/v1alpha1' \
  'kind: ClusterProperty' \
  'metadata:' \
  '  name: aws.account-id.example.com' \
  'spec:' \
  '  value: "444455556666"' | \
  kubectl apply --context "$C2_CTX" -f -

Enter fullscreen mode Exit fullscreen mode

Check the resources on the spoke clusters:

kubectl get clusterproperties --context "$C1_CTX"
kubectl get clusterproperties --context "$C2_CTX"

Enter fullscreen mode Exit fullscreen mode

The registration agent synchronizes the spoke-side ClusterProperty resources into ManagedCluster.status.clusterClaims on the hub:

kubectl wait --for=jsonpath='{.status.clusterClaims[?(@.name=="aws.region.example.com")].value}'=ap-northeast-1 \
  managedcluster/cluster1 \
  --context "$HUB_CTX" \
  --timeout=180s

kubectl wait --for=jsonpath='{.status.clusterClaims[?(@.name=="aws.account-id.example.com")].value}'=111122223333 \
  managedcluster/cluster1 \
  --context "$HUB_CTX" \
  --timeout=180s

kubectl wait --for=jsonpath='{.status.clusterClaims[?(@.name=="aws.region.example.com")].value}'=us-west-2 \
  managedcluster/cluster2 \
  --context "$HUB_CTX" \
  --timeout=180s

kubectl wait --for=jsonpath='{.status.clusterClaims[?(@.name=="aws.account-id.example.com")].value}'=444455556666 \
  managedcluster/cluster2 \
  --context "$HUB_CTX" \
  --timeout=180s

Enter fullscreen mode Exit fullscreen mode

Then ManagedCluster.status.clusterClaims is synchronized into ClusterProfile.status.properties:

kubectl wait --for=jsonpath='{.status.properties[?(@.name=="aws.region.example.com")].value}'=ap-northeast-1 \
  clusterprofile/cluster1 \
  -n cluster-inventory \
  --context "$HUB_CTX" \
  --timeout=180s

kubectl wait --for=jsonpath='{.status.properties[?(@.name=="aws.account-id.example.com")].value}'=111122223333 \
  clusterprofile/cluster1 \
  -n cluster-inventory \
  --context "$HUB_CTX" \
  --timeout=180s

kubectl wait --for=jsonpath='{.status.properties[?(@.name=="aws.region.example.com")].value}'=us-west-2 \
  clusterprofile/cluster2 \
  -n cluster-inventory \
  --context "$HUB_CTX" \
  --timeout=180s

kubectl wait --for=jsonpath='{.status.properties[?(@.name=="aws.account-id.example.com")].value}'=444455556666 \
  clusterprofile/cluster2 \
  -n cluster-inventory \
  --context "$HUB_CTX" \
  --timeout=180s

Enter fullscreen mode Exit fullscreen mode

Print only the AWS-related properties from ClusterProfile:

printf 'cluster1\n'
kubectl get clusterprofile cluster1 \
  -n cluster-inventory \
  --context "$HUB_CTX" \
  -o jsonpath='{range .status.properties[*]}{.name}={.value}{"\n"}{end}' | grep '^aws\.'

printf 'cluster2\n'
kubectl get clusterprofile cluster2 \
  -n cluster-inventory \
  --context "$HUB_CTX" \
  -o jsonpath='{range .status.properties[*]}{.name}={.value}{"\n"}{end}' | grep '^aws\.'

Enter fullscreen mode Exit fullscreen mode

In this environment:

cluster1
aws.account-id.example.com=111122223333
aws.region.example.com=ap-northeast-1
cluster2
aws.account-id.example.com=444455556666
aws.region.example.com=us-west-2

Enter fullscreen mode Exit fullscreen mode

Because ClusterProfileAccessProvider is enabled on cluster-proxy v0.10.0, status.accessProviders also contains the cluster-proxy user-server connection information:

kubectl wait --for=jsonpath='{.status.accessProviders[0].name}'=open-cluster-management \
  clusterprofile/cluster1 \
  -n cluster-inventory \
  --context "$HUB_CTX" \
  --timeout=120s

kubectl get clusterprofile cluster1 \
  -n cluster-inventory \
  --context "$HUB_CTX" \
  -o jsonpath='{range .status.accessProviders[*]}{.name}{"\n"}{.cluster.server}{"\n"}{end}'

Enter fullscreen mode Exit fullscreen mode

In this environment:

open-cluster-management
https://cluster-proxy-addon-user.open-cluster-management-addon:9092/cluster1

Enter fullscreen mode Exit fullscreen mode

Check the full ClusterProfile for cluster1:

kubectl get clusterprofile cluster1 \
  -n cluster-inventory \
  --context "$HUB_CTX" \
  -o yaml \
  --show-managed-fields=false

Enter fullscreen mode Exit fullscreen mode

In this environment, the YAML is shown below. The command above prints the full value; here, status.accessProviders[].cluster.certificate-authority-data is replaced with "<base64 CA bundle>" for brevity:

apiVersion: multicluster.x-k8s.io/v1alpha1
kind: ClusterProfile
metadata:
  creationTimestamp: "2026-05-05T07:09:27Z"
  generation: 1
  labels:
    multicluster.x-k8s.io/clusterset: sandbox-fleet
    open-cluster-management.io/cluster-name: cluster1
    x-k8s.io/cluster-manager: open-cluster-management
  name: cluster1
  namespace: cluster-inventory
  resourceVersion: "4889"
  uid: 29ec181b-7a42-4642-b957-d7057d756575
spec:
  clusterManager:
    name: open-cluster-management
  displayName: cluster1
status:
  accessProviders:
  - cluster:
      certificate-authority-data: "<base64 CA bundle>"
      extensions:
      - extension:
          clusterName: cluster1
        name: client.authentication.k8s.io/exec
      server: https://cluster-proxy-addon-user.open-cluster-management-addon:9092/cluster1
    name: open-cluster-management
  conditions:
  - lastTransitionTime: "2026-05-05T07:09:27Z"
    message: Managed cluster is available
    reason: ManagedClusterAvailable
    status: "True"
    type: ControlPlaneHealthy
  - lastTransitionTime: "2026-05-05T07:09:27Z"
    message: Managed cluster joined
    reason: ManagedClusterJoined
    status: "True"
    type: Joined
  properties:
  - name: aws.account-id.example.com
    value: "111122223333"
  - name: aws.region.example.com
    value: ap-northeast-1
  version:
    kubernetes: v1.35.0

Enter fullscreen mode Exit fullscreen mode

This YAML tells us several things:

  • metadata.labels["multicluster.x-k8s.io/clusterset"] shows that this ClusterProfile was created from sandbox-fleet.
  • spec.clusterManager.name is open-cluster-management, meaning this ClusterProfile is managed by OCM.
  • status.properties contains the values from the spoke-side ClusterProperty resources.
  • status.accessProviders[0].cluster.server is the cluster-specific endpoint of the cluster-proxy user-server.
  • status.accessProviders[0].cluster.extensions[0].extension.clusterName is the target spoke cluster name.
  • status.conditions reflects the original ManagedCluster Available and Joined conditions.

Everything a Cluster Inventory API consumer needs is in this single resource: identity, properties, kube version, conditions, and a reachable endpoint with its CA bundle. That is the promise of ClusterProfile, and OCM is delivering it end-to-end.

Compare clusteradm proxy with ClusterProfile

Finally, compare the ClusterProfile inventory view with actual spoke API access:

kubectl get clusterprofiles -n cluster-inventory --context "$HUB_CTX"

clusteradm proxy kubectl \
  --context "$HUB_CTX" \
  --cluster=cluster1 \
  --sa=test \
  --args="get ns"

Enter fullscreen mode Exit fullscreen mode

ClusterProfile is the hub-side inventory view. clusteradm proxy kubectl verifies the spoke API access path that ClusterProfile.status.accessProviders is pointing at.

What's next

We now have a working Cluster Inventory API setup with OCM as the cluster manager:

  • ClusterProfile resources auto-created per managed cluster.
  • Spoke ClusterProperty reflected into status.properties on the hub.
  • Real, reachable spoke endpoints in status.accessProviders.

In Part 2, we'll point multicluster-runtime at this inventory and write a controller that reconciles across cluster1 and cluster2 using nothing but ClusterProfile to discover and reach them. That is where the vendor-neutral inventory really pays off: the controller code will not contain a single OCM-specific import.

Cleanup

Delete the kind clusters:

kind delete cluster --name hub
kind delete cluster --name cluster1
kind delete cluster --name cluster2

Enter fullscreen mode Exit fullscreen mode