-
-
Notifications
You must be signed in to change notification settings - Fork 3
Test Results
Verdict: 🟢 ALL GREEN · generated 2026-07-26 08:41:29 · local kind fleet via podman, Open Cluster Management hub
| Passed | Expected-unavailable | Failed | Total steps |
|---|---|---|---|
| 53 | 1 | 0 | 57 |
Reproduce locally with
./hack/e2e-local.sh- one command stands up a real multi-cluster fleet on kind, drives every tool and prompt, breaks a workload and fixes it through the approval gate, then tears the fleet down.
One command stands up a real multi-cluster Open Cluster Management fleet on kind, then drives the MCP server through every tool and prompt: it reads the fleet freely, but forces every change through policy checks and a human-approved token. It then deliberately breaks a workload and shows the agent diagnose and fix it, before tearing the fleet down. Green means the whole loop worked.
- 🧰 Install & verify tools - podman, kind, kubectl, clusteradm, helm
- ☸️ Bootstrap a real fleet - 1 hub + N spoke clusters, OCM, Kyverno, policies
- ✨ Enrich & seed fixtures - add-ons, policies, ManifestWorks - so every tool has real data
- 🔍 Investigate (read tools) - inventory, health, placement, work, add-ons - free, no gate
- 🛡️ Make a change, gated - propose → Kyverno + guardrails → human token → apply (a bad token is refused)
- 💬 Prompts & audit - reusable runbooks and an append-only audit trail
- 💥 Break something - inject a failing rollout (ImagePullBackOff)
- 🩺 Diagnose - health + events pinpoint the bad image
- 🔧 Fix, through the gate - propose the pinned image, approve, apply
- ✅ Verify recovery - re-read health until the workload is healthy again
- 🧹 Clean up - delete only the clusters this run created
Required tool for the local fleet.
$ podman --version
already installed: podman version 6.0.2
Required tool for the local fleet.
$ kind version
already installed: kind v0.32.0 go1.26.3 darwin/arm64
Required tool for the local fleet.
$ kubectl version --client=true -o yaml 2>/dev/null | grep gitVersion | head -1
already installed: gitVersion: v1.34.1
Required tool for the local fleet.
$ helm version --short
already installed: v4.1.1+g5caf004
Required tool for the local fleet.
$ clusteradm version | head -1
already installed: clusteradm version :v1.3.1-0-g90bdc31
Required tool for the local fleet.
$ oc version --client 2>/dev/null | head -1
already installed: Client Version: 4.18.0-202504282205.p0.geb9bc9b.assembly.stream.el9-eb9bc9b
Install the MCP server and its ocm-mcp CLI into an isolated virtualenv.
$ pip install -e .
ocm-mcp-server 0.2.1
kind needs a container runtime; here it is Podman (Docker is not required).
$ podman info
podman version 6.0.2
Stand up a real OCM hub with spokes so every tool talks to genuine clusters, not mocks.
$ CONTAINER_ENGINE=podman SPOKES=2 ./hack/bootstrap.sh --no-jaeger
[1;36m==> Deploying the demo app to all spokes (namespace 'shop')[0m
namespace/shop created
deployment.apps/payments created
service/payments created
namespace/shop created
deployment.apps/payments created
service/payments created
[1;36m==> Done. Configure your MCP client environment:[0m
export OCM_MCP_HUB_CONTEXT=kind-hub
export OCM_MCP_SPOKE_CONTEXTS=cluster1=kind-cluster1,cluster2=kind-cluster2
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 # if Jaeger enabled
# then register the server with your MCP client, e.g. for Claude Code:
# see examples/claude-code.mcp.json
Verify the fleet:
kubectl --context kind-hub get managedclusters
Proof the spokes are registered and Available on the hub.
$ kubectl --context kind-hub get managedclusters
NAME HUB ACCEPTED MANAGED CLUSTER URLS JOINED AVAILABLE AGE
cluster1 true https://cluster1-control-plane:6443 True True 104s
cluster2 true https://cluster2-control-plane:6443 True True 80s
Install the governance policy add-on, turn on ManifestWorkReplicaSet, and add minimal CRDs plus labelled sample objects for the ACM (ManagedClusterInfo) and HyperShift (HostedCluster/NodePool) APIs - so the add-on, policy, rollout, ACM, and HyperShift tools all return real objects. On a real hub those come from ACM/MCE or HyperShift; on kind they are clearly-labelled e2e fixtures.
$ clusteradm install/enable hub-addon; kubectl patch clustermanager; kubectl apply CRDs + samples
enrichment: governance-policy-framework(enabled) ManifestWorkReplicaSet(enabled) acm+hypershift-fixtures
Bind the 'global' cluster set into a namespace and add a Placement that selects every cluster - so the placement tools have a real decision to show.
$ kubectl apply Placement + ManagedClusterSetBinding
binding=created, placement=created
Deploy a marker ManifestWork so the work tools report on something real.
$ propose_manifestwork
REJECTED by hub admission (Kyverno policy):
{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Internal error occurred: failed calling webhook \"manifestworkvalidators.admission.work.open-cluster-management.io\": failed to call webhook: Post \"https://cluster-manager-work-webhook.open-cluster-management-hub.svc:9443/validate-work-open-cluster-management-io-v1-manifestwork?timeout=10s\": context deadline exceeded","reason":"InternalError","details":{"causes":[{"message":"failed calling webhook \"manifestworkvalidators.admission.work.open-cluster-management.io\": failed to call webhook: Post \"https://cluster-manager-work-webhook.open-cluster-management-hub.svc:9443/validate-work-open-cluster-management-io-v1-manifestwork?timeout=10s\": context deadline exceeded"}]},"code":500}
A single template fanned across every placement-selected cluster - so list_manifestworkreplicasets shows a rollout.
$ kubectl apply ManifestWorkReplicaSet
result=created
A sample inform Policy so the policy tools report on a real object.
$ kubectl apply Policy
result=created
The fleet roster: every managed cluster, its version and health.
$ tool: list_clusters
[
{
"name": "cluster1",
"labels": {
"cluster.open-cluster-management.io/clusterset": "default",
"feature.open-cluster-management.io/addon-governance-policy-framework": "unreachable"
},
"available": "True",
"joined": "True",
"kubernetes_version": "v1.36.1",
"capacity": {
"cpu": "4",
"memory": "8099496Ki"
}
},
{
"name": "cluster2",
"labels": {
"cluster.open-cluster-management.io/clusterset": "default",
"feature.open-cluster-management.io/addon-governance-policy-framework": "unreachable"
},
"available": "True",
"joined": "True",
"kubernetes_version": "v1.36.1",
"capacity": {
"cpu": "4",
"memory": "8099496Ki"
}
}
]
Zoom into one cluster: acceptance, taints, capacity, claims.
$ tool: get_cluster
{
"name": "cluster1",
"labels": {
"cluster.open-cluster-management.io/clusterset": "default",
"feature.open-cluster-management.io/addon-governance-policy-framework": "unreachable"
},
"hub_accepts_client": true,
"taints": [],
"conditions": {
"HubAcceptedManagedCluster": "True",
"ManagedClusterJoined": "True",
"ManagedClusterConditionAvailable": "True",
"ManagedClusterConditionClockSynced": "True"
},
"kubernetes_version": "v1.36.1",
"capacity": {
"cpu": "4",
"ephemeral-storage": "104266732Ki",
"hugepages-1Gi": "0",
"hugepages-2Mi": "0",
"hugepages-32Mi": "0",
"hugepages-64Ki": "0",
"memory": "8099496Ki",
"pods": "110"
},
"allocatable": {
"cpu": "4",
"ephemeral-storage": "104266732Ki",
"hugepages-1Gi": "0",
"hugepages-2Mi": "0",
"hugepages-32Mi": "0",
"hugepages-64Ki": "0",
"memory": "8099496Ki",
"pods": "110"
},
"cluster_claims": {}
}
Groupings of clusters used for placement.
$ tool: list_cluster_sets
[
{
"name": "default",
"selector_type": "ExclusiveClusterSetLabel",
"conditions": {
"ClusterSetEmpty": "False"
},
"members": [
"cluster1",
"cluster2"
]
},
{
"name": "global",
"selector_type": "LabelSelector",
"conditions": {
"ClusterSetEmpty": "False"
},
"members": []
}
]
Which cluster sets a namespace may schedule to.
$ tool: list_cluster_set_bindings
[
{
"namespace": "default",
"name": "global",
"cluster_set": "global",
"conditions": {
"Bound": "True"
}
}
]
Facts each cluster self-reports (id, platform, version).
$ tool: list_cluster_claims
[
{
"cluster": "cluster1",
"claims": {}
},
{
"cluster": "cluster2",
"claims": {}
}
]
The on-call view: unhealthy pods and degraded deployments.
$ tool: get_cluster_health
{
"cluster": "cluster1",
"hub_conditions": {
"HubAcceptedManagedCluster": "True",
"ManagedClusterJoined": "True",
"ManagedClusterConditionAvailable": "True",
"ManagedClusterConditionClockSynced": "True"
},
"unhealthy_pods": [],
"degraded_deployments": [
{
"namespace": "shop",
"name": "payments",
"ready": "0/2"
}
],
"spoke_view": "ok"
}
Recent Kubernetes events - the 'why' behind failures.
$ tool: query_events
[
{
"namespace": "shop",
"type": "Normal",
"reason": "Pulled",
"object": "Pod/payments-df965d5f-6t7rx",
"count": 1,
"message": "Successfully pulled image \"registry.k8s.io/e2e-test-images/agnhost:2.47\" in 202ms (21.229s including waiting). Image size: 50637101 bytes."
},
{
"namespace": "shop",
"type": "Normal",
"reason": "Created",
"object": "Pod/payments-df965d5f-6t7rx",
"count": 1,
"message": "Container created"
},
{
"namespace": "shop",
"type": "Normal",
"reason": "Started",
"object": "Pod/payments-df965d5f-6t7rx",
"count": 1,
"message": "Container started"
},
{
"namespace": "shop",
"type": "Normal",
"reason": "Pulled",
"object": "Pod/payments-df965d5f-tghxp",
"count": 1,
"message": "Successfully pulled image \"registry.k8s.io/e2e-test-images/agnhost:2.47\" in 21.032s (21.032s including waiting). Image size: 50637101 bytes."
},
{
"namespace": "shop",
"type": "Normal",
"reason": "Created",
"object": "Pod/payments-df965d5f-tghxp",
"count": 1,
"message": "Container created"
... (15 more lines)
What the hub is currently delivering to this cluster.
$ tool: list_manifestworks
[
{
"name": "demo-mwrstpl7f",
"applied": "True",
"available": "True",
"resources": [
"ConfigMap/e2e-seed (shop)"
]
}
]
Fleet-wide rollouts (a template fanned across clusters).
$ tool: list_manifestworkreplicasets
[
{
"namespace": "default",
"name": "demo-mwrs",
"summary": {
"applied": 2,
"available": 2,
"total": 2
},
"conditions": {
"PlacementVerified": "True",
"PlacementRolledOut": "False",
"ManifestworkApplied": "True"
}
}
]
Fleet-level add-on definitions.
$ tool: list_cluster_management_addons
[
{
"name": "config-policy-controller",
"install_strategy": "Manual",
"conditions": {}
},
{
"name": "governance-policy-framework",
"install_strategy": "Manual",
"conditions": {}
}
]
Per-cluster add-on health across the fleet.
$ tool: get_addon_health
[
{
"cluster": "cluster1",
"addon": "governance-policy-framework",
"available": "Unknown",
"degraded": "False",
"progressing": "False"
},
{
"cluster": "cluster2",
"addon": "governance-policy-framework",
"available": "Unknown",
"degraded": "False",
"progressing": "False"
}
]
Every add-on installed on one cluster, with health.
$ tool: list_addons_for_cluster
[
{
"addon": "governance-policy-framework",
"install_namespace": "open-cluster-management-agent-addon",
"available": "Unknown",
"degraded": "False"
}
]
Clusters waiting to be admitted to the hub.
$ tool: list_pending_csrs
No pending CSRs - every cluster is already admitted (a healthy fleet).
Placements and how many clusters each selects.
$ tool: list_placements
[
{
"namespace": "default",
"name": "demo-all",
"cluster_sets": [
"global"
],
"number_of_clusters": null,
"selected": 2,
"conditions": {
"PlacementMisconfigured": "False",
"PlacementSatisfied": "True"
}
}
]
Exactly which clusters a Placement chose.
$ tool: get_placement_decision
{
"placement": "demo-all",
"namespace": "default",
"selected_clusters": [
"cluster1",
"cluster2"
],
"count": 2
}
Governance Policies and per-cluster compliance.
$ tool: list_policies
[
{
"namespace": "default",
"name": "require-cm",
"remediation": "inform",
"compliant": null,
"per_cluster": {}
}
]
Only the NonCompliant / Pending policy-cluster pairs.
$ tool: list_policy_violations
No violations - evaluated policies are compliant (or evaluation is in progress).
Extended inventory (OpenShift version, nodes) - ACM only.
$ tool: get_cluster_info
{
"cluster": "cluster1",
"console_url": "https://console-openshift-console.apps.cluster1.example.com",
"kube_vendor": "OpenShift",
"cloud_vendor": "BareMetal",
"openshift_version": "4.16.7",
"node_count": 1,
"nodes": [
{
"name": "cluster1-control-plane",
"capacity": {
"cpu": "8",
"memory": "16Gi"
},
"labels": {
"node-role.kubernetes.io/control-plane": ""
}
}
],
"conditions": {
"ManagedClusterInfoSynced": "True"
}
}
HyperShift hosted control planes - when the hub hosts them.
$ tool: list_hosted_clusters
[
{
"name": "demo-hcp",
"namespace": "clusters",
"version": "4.16.7",
"version_state": "Completed",
"conditions": {
"Available": "True"
}
}
]
HyperShift worker node pools - when the hub hosts HCPs.
$ tool: list_node_pools
[
{
"name": "demo-hcp-workers",
"namespace": "clusters",
"cluster": "demo-hcp",
"desired_replicas": 2,
"current_replicas": 2,
"conditions": {
"Ready": "True"
}
}
]
Generic allow-listed reader over any OCM type.
$ tool: list_resources
[
{
"name": "cluster1",
"labels": {
"cluster.open-cluster-management.io/clusterset": "default",
"feature.open-cluster-management.io/addon-governance-policy-framework": "unreachable"
},
"conditions": {
"HubAcceptedManagedCluster": "True",
"ManagedClusterJoined": "True",
"ManagedClusterConditionAvailable": "True",
"ManagedClusterConditionClockSynced": "True"
}
},
{
"name": "cluster2",
"labels": {
"cluster.open-cluster-management.io/clusterset": "default",
"feature.open-cluster-management.io/addon-governance-policy-framework": "unreachable"
},
"conditions": {
"HubAcceptedManagedCluster": "True",
"ManagedClusterJoined": "True",
"ManagedClusterConditionAvailable": "True",
"ManagedClusterConditionClockSynced": "True"
}
}
]
Generic get of one allow-listed OCM object.
$ tool: get_resource
{
"apiVersion": "cluster.open-cluster-management.io/v1",
"kind": "ManagedCluster",
"metadata": {
"creationTimestamp": "2026-07-26T03:08:21Z",
"finalizers": [
"cluster.open-cluster-management.io/resource-cleanup",
"cluster.open-cluster-management.io/api-resource-cleanup"
],
"generation": 4,
"labels": {
"cluster.open-cluster-management.io/clusterset": "default",
"feature.open-cluster-management.io/addon-governance-policy-framework": "unreachable"
},
"managedFields": [
{
"apiVersion": "cluster.open-cluster-management.io/v1",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:spec": {
"f:hubAcceptsClient": {}
}
},
"manager": "clusteradm",
"operation": "Update",
"time": "2026-07-26T03:08:55Z"
},
{
"apiVersion": "cluster.open-cluster-management.io/v1",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:status": {
".": {},
"f:allocatable": {
".": {},
"f:cpu": {},
"f:ephemeral-storage": {},
"f:hugepages-1Gi": {},
"f:hugepages-2Mi": {},
"f:hugepages-32Mi": {},
... (7 more lines)
The agent PROPOSES a change. It must pass static guardrails and a Kyverno dry-run first; nothing is applied yet.
$ tool: propose_manifestwork
proposal_id=fd619b573fc441468466ac07a1f46137
status=pending_approval
Prove the gate holds: apply with an invalid token must be refused by the server, not applied.
$ tool: apply_manifestwork(bad token)
REJECTED: Malformed approval token.
A human on a trusted terminal mints an approval token: an Ed25519 signature binding the exact content and operation. The server holds only the public key, so it can verify a token but can never mint one - and neither can the agent.
$ ocm-mcp approve fd619b573fc441468466ac07a1f46137
token minted (...IvhtLnAIv0CQ)
With a valid token, the server verifies it and delivers the change as an OCM ManifestWork.
$ tool: apply_manifestwork
{
"status": "applied",
"cluster": "cluster1",
"manifestwork": "e2e-demo",
"note": "Verify rollout with get_cluster_health / get_manifestwork. To undo, call propose_rollback then apply the rollback with a fresh approval."
}
Confirm the hub actually applied it on the spoke.
$ tool: get_manifestwork
{
"cluster": "cluster1",
"name": "e2e-demo",
"conditions": {
"Applied": "True",
"Available": "True"
},
"resources": [
{
"resource": "ConfigMap/ocm-mcp-e2e",
"namespace": "shop",
"conditions": {
"Applied": "True",
"Available": "True",
"StatusFeedbackSynced": "True"
},
"status_feedback": {}
}
]
}
Propose adding a NoSelect taint so Placements stop scheduling here - still gated by approval.
$ tool: propose_cluster_action
{
"proposal_id": "f7f8b7c8f50c4b69890b152aec1907d3",
"status": "pending_approval",
"action": "cordon",
"next_step": "Ask the human operator to run: ocm-mcp approve f7f8b7c8f50c4b69890b152aec1907d3 and provide you the approval token, then call apply_cluster_action."
}
Applied after approval; the taint now keeps new work off this cluster.
$ tool: apply_cluster_action
taints=[{"effect": "NoSelect", "key": "ocm-mcp-server.io/cordoned", "timeAdded": "2026-07-26T03:10:39Z", "value": "true"}]
Remove the taint so the cluster is schedulable again.
$ tool: apply_cluster_action(uncordon)
taints_after=[]
A ready-made, safety-first runbook an agent can start from.
$ mcp prompt: diagnose_fleet
You are operating a Kubernetes fleet through the OCM hub. Investigate only - do not propose or apply anything yet.
1. Call list_clusters to see availability, version, and capacity of every cluster.
2. Call get_addon_health to spot Degraded or unavailable add-ons across the fleet.
3. For any cluster that is Unavailable or not joined, call get_cluster to read its conditions, then get_cluster_health for unhealthy pods and degraded deployments.
4. For each unhealthy workload, use query_events and get_pod_logs to find the cause.
5. Produce a concise report: per cluster, what is wrong, the evidence, and the smallest safe remediation you would propose. Do not act on it yet.
A ready-made, safety-first runbook an agent can start from.
$ mcp prompt: remediate_with_approval
A fleet operator reports: payments degraded on cluster1
Follow the safe remediation workflow:
1. Investigate with the read tools (list_clusters, get_cluster_health, query_events, get_pod_logs, get_manifestwork) until you can name the root cause with evidence.
2. Decide the smallest change that fixes it. For a workload change, call propose_manifestwork with pinned images and a precise summary. For a cluster lifecycle change (cordon, uncordon, set a label, accept a cluster), call propose_cluster_action.
3. If the proposal is REJECTED by static guardrails or Kyverno, read the reason, correct the manifest, and propose again. Never try to bypass a rejection.
4. Tell the operator the proposal id and ask them to run `ocm-mcp approve <id>` and give you the token. Wait for it.
5. Call the matching apply tool with the token. Then verify recovery with reads.
6. Finish by calling get_audit_trail an
... (truncated)
A ready-made, safety-first runbook an agent can start from.
$ mcp prompt: why_not_scheduled
Explain why cluster 'cluster1' was or was not selected by Placement 'demo-all' in namespace 'default'. Use only reads:
1. get_placement_decision('demo-all', 'default') for the clusters actually chosen.
2. list_placements to read the Placement's clusterSets and selection intent.
3. get_cluster('cluster1') for its labels, ClusterClaims, and taints.
4. list_cluster_set_bindings to confirm the Placement's namespace is bound to the ClusterSet the cluster belongs to.
5. list_addon_placement_scores('cluster1') if the Placement uses AddOn prioritizers.
6. Conclude with the specific reason: not in a bound ClusterSet, failed a predicate, carries a NoSelect taint, or simply out-scored by others.
Proposals still awaiting human approval.
$ tool: list_pending_proposals
[
{
"id": "b5f924e6d7b4461cb049fc7d264fc059",
"cluster": "cluster1",
"kind": "action",
"action": "uncordon",
"name": "uncordon-cluster1",
"summary": "Undo."
},
{
"id": "b84e8f66ab8b4a4a9dc271f006674511",
"cluster": "cluster1",
"kind": "action",
"action": "uncordon",
"name": "uncordon-cluster1",
"summary": "Undo."
}
]
The append-only log of every tool call so far - the agent writes its incident report from this record, not from memory.
$ tool: get_audit_trail
[
{"tool": "apply_manifestwork", "args": {"proposal_id": "fd619b573fc441468466ac07a1f46137", "approval_token": "<redacted>"}, "outcome": "rejected", "error": "", "duration_ms": 0, "ts": 1785035433.996419, "actor": "sandeepbazar:94984", "seq": 3, "prev": "31be6b299d6caead2e7b59a93afb8ba27c3cbafb41e9f2a3036071b8b45ba307", "hash": "b246ec932f37b8f53b3c7e92d8d7316cdca149012beeaf91fb8950d5c2edb130"},
{"tool": "apply_manifestwork", "args": {"proposal_id": "fd619b573fc441468466ac07a1f46137", "approval_token": "<redacted>"}, "outcome": "ok", "error": "", "duration_ms": 5, "ts": 1785035434.003003, "actor": "sandeepbazar:94984", "seq": 4, "prev": "b246ec932f37b8f53b3c7e92d8d7316cdca149012beeaf91fb8950d5c2edb130", "hash": "69428c0f7b28ca983b7caa06e630d0ef01a4f6c7d4ecaa7528799d1046df2fd1"},
{"tool": "get_manifestwork", "args": {"cluster": "cluster1", "name": "e2e-demo"}, "outcome": "ok", "error": "", "duration_ms": 4, "ts": 1785035439.0112379, "actor": "sandeepbazar:94984", "seq": 5, "prev": "69428c0f7b28ca983b7caa06e630d0ef01a4f6c7d4ecaa7528799d1046df2fd1", "hash": "bd2a558795ec107bf62aba5b9093204073dabb83f00b13074730b666b268bb25"},
{"tool": "propose_cluster_action", "args": {"cluster": "cluster1", "action": "cordon", "summary": "Cordon for maintenance.", "params_json": "{}"}, "outcome": "ok", "error": "", "duration_ms": 10, "ts": 1785035439.022474, "actor": "sandeepbazar:94984", "seq": 6
... (truncated)
Deploy a 'payments-v2' with a broken image tag, exactly the kind of bad rollout that pages someone at 2 a.m.
$ ./chaos/inject.sh failing-rollout cluster1
deployment.apps/payments-v2 created
injected: failing-rollout into cluster1
The agent asks 'what is unhealthy here?' and sees payments-v2 is not running.
$ tool: get_cluster_health(cluster1)
{
"unhealthy_pods": [
{
"namespace": "shop",
"name": "payments-v2-68bf75f6cc-c82rm",
"phase": "Pending",
"waiting": [
"ErrImagePull"
],
"restarts": 0
},
{
"namespace": "shop",
"name": "payments-v2-68bf75f6cc-xg557",
"phase": "Pending",
"waiting": [
"ErrImagePull"
],
"restarts": 0
}
],
"degraded_deployments": [
{
"namespace": "shop",
"name": "payments-v2",
"ready": "0/2"
}
]
}
Events reveal the root cause in plain English: the image cannot be pulled.
$ tool: query_events(cluster1, shop)
[
{
"namespace": "shop",
"type": "Normal",
"reason": "BackOff",
"object": "Pod/payments-v2-68bf75f6cc-c82rm",
"count": 1,
"message": "Back-off pulling image \"registry.k8s.io/e2e-test-images/agnhost:2.47-nonexistent\""
},
{
"namespace": "shop",
"type": "Warning",
"reason": "Failed",
"object": "Pod/payments-v2-68bf75f6cc-c82rm",
"count": 1,
"message": "Error: ImagePullBackOff"
},
{
"namespace": "shop",
"type": "Normal",
"reason": "BackOff",
"object": "Pod/payments-v2-68bf75f6cc-xg557",
"count": 1,
"message": "Back-off pulling image \"registry.k8s.io/e2e-test-images/agnhost:2.47-nonexistent\""
},
{
"namespace": "shop",
"type": "Warning",
"reason": "Failed",
"object": "Pod/payments-v2-68bf75f6cc-xg557",
"count": 1,
"message": "Error: ImagePullBackOff"
},
{
"namespace": "shop",
"type": "Warning",
"reason": "Failed",
"object": "Pod/payments-v2-68bf75f6cc-c82rm",
"count": 1,
"message": "Failed to pull image \"registry.k8s.io/e2e-test-images/agnhost:2.47-nonexistent\": rpc error: code = NotFound desc = failed to pull and unpack image \"registry.k8s.io/e2e-test-images/agnhost:2.47-nonexistent\": failed to resolve reference \"registry.k8s.io/e2e-test-images/agnhost:2.47-nonexistent\": registry.k8s.io/e2e-test-images/agnhost:2.47-nonexistent: not found
... (1 more lines)
The agent proposes the smallest safe fix (a pinned, known-good image). It clears the guardrails and a human approves it.
$ propose_manifestwork -> ocm-mcp approve
proposal_id=952a811191664c83a311fbe1ce6310b5
guardrails=passed kyverno_dry_run=passed approved=yes
Delivered to the cluster through the hub as a ManifestWork.
$ tool: apply_manifestwork(token)
status=applied
Re-read health until payments-v2 is running again - the incident is closed, with a full audit trail of everything that happened.
$ tool: get_cluster_health(cluster1)
payments-v2 is Running again - fix confirmed end to end.
Delete only the clusters created by this run (recorded in .e2e-run/created-clusters); never touch pre-existing clusters.
$ kind delete cluster --name ...
deleted: hub cluster1 cluster2