{"record":{"id":"54933f5a9e18c4f6","repo":"rancher/rancher","slug":"mgmt-cluster-s-carries-only-one-of-s-s-both-mu","errorCode":null,"errorMessage":"mgmt cluster %s carries only one of %s/%s; both must be set for a CAPI-native cluster","messagePattern":"mgmt cluster (.+?) carries only one of (.+?)/(.+?); both must be set for a CAPI-native cluster","errorType":"http","errorClass":null,"httpStatus":500,"severity":"error","filePath":"pkg/capr/configserver/identity.go","lineNumber":104,"sourceCode":"\t// navigate mgmt shell → provv1.Cluster → CAPI cluster in fleet-default. Classify as\n\t// KindV2Prov so onSecretChange falls through to that path rather than mistakenly running the\n\t// imported RKE2/K3s (mgmt v3 Node) handler.\n\tif mgmtCluster.Annotations[\"provisioning.cattle.io/administrated\"] == \"true\" {\n\t\treturn &LifecycleContext{\n\t\t\tKind:            KindV2Prov,\n\t\t\tTargetNamespace: tokenNamespace,\n\t\t\tMgmtCluster:     mgmtCluster,\n\t\t}, nil\n\t}\n\n\townerName := mgmtCluster.Labels[capr.CAPIClusterOwnerLabel]\n\townerNS := mgmtCluster.Labels[capr.CAPIClusterOwnerNSLabel]\n\n\t// Both labels must be set together — mixed state is misconfiguration and we refuse to\n\t// silently default to \"imported\", which has historically been the case with the prior\n\t// regex-based failure mode.\n\tif (ownerName == \"\") != (ownerNS == \"\") {\n\t\treturn nil, fmt.Errorf(\n\t\t\t\"mgmt cluster %s carries only one of %s/%s; both must be set for a CAPI-native cluster\",\n\t\t\tmgmtCluster.Name, capr.CAPIClusterOwnerLabel, capr.CAPIClusterOwnerNSLabel)\n\t}\n\n\tif ownerName == \"\" {\n\t\treturn &LifecycleContext{\n\t\t\tKind:            KindImported,\n\t\t\tTargetNamespace: mgmtCluster.Name,\n\t\t\tMgmtCluster:     mgmtCluster,\n\t\t}, nil\n\t}\n\n\tcapiCluster, err := capiClusterCache.Get(ownerNS, ownerName)\n\tif apierrors.IsNotFound(err) {\n\t\treturn nil, fmt.Errorf(\n\t\t\t\"mgmt cluster %s references CAPI cluster %s/%s, but that cluster was not found\",\n\t\t\tmgmtCluster.Name, ownerNS, ownerName)\n\t}","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/rancher/rancher/blob/932558d4e68565aff2d2f36e89ec4a391b06e7c5/pkg/capr/configserver/identity.go#L86-L122","documentation":"identity.go classifies a management cluster using two labels (capr.CAPIClusterOwnerLabel and capr.CAPIClusterOwnerNSLabel): both set means CAPI-native, both absent means imported. Exactly one set is treated as misconfiguration and rejected rather than silently defaulting to 'imported' — an explicit improvement over the prior regex-based failure mode. The error names the cluster and both label keys.","triggerScenarios":"Any configserver request (machine config fetch) for an mgmt cluster whose owner labels are half-written: manual kubectl label of only one key, a migration that crashed between the two writes, or a partial restore.","commonSituations":"Operators editing labels by hand; upgrade/migration jobs interrupted mid-label; backup restore reapplying only some labels.","solutions":["Inspect the labels: kubectl get mgmtcluster <name> -o jsonpath='{.metadata.labels}'","Set the missing counterpart label if the cluster really is CAPI-native, or remove both to mark it imported — whichever reflects reality","Retry the machine config request once the pair is consistent"],"exampleFix":"# half-labelled (broken) -> consistent CAPI-native\nkubectl label mgmtcluster <name> cluster-api.cattle.io/cluster-owner=<capiClusterName>\nkubectl label mgmtcluster <name> cluster-api.cattle.io/cluster-owner-ns=<capiClusterNamespace>\n\n# or mark imported by removing both\nkubectl label mgmtcluster <name> cluster-api.cattle.io/cluster-owner- cluster-api.cattle.io/cluster-owner-ns-","handlingStrategy":"validation","validationCode":"ownerName := mgmt.Labels[capr.CAPIClusterOwnerLabel]\nownerNS := mgmt.Labels[capr.CAPIClusterOwnerNSLabel]\nif (ownerName == \"\") != (ownerNS == \"\") {\n    return fmt.Errorf(\"refusing request: mgmt cluster %s has half-set owner labels; set both or neither\", mgmt.Name)\n}","typeGuard":"func ownerLabelsConsistent(l map[string]string) bool {\n    name, ns := l[capr.CAPIClusterOwnerLabel], l[capr.CAPIClusterOwnerNSLabel]\n    return (name == \"\" && ns == \"\") || (name != \"\" && ns != \"\")\n}","tryCatchPattern":"lc, err := getLifecycleContext(mgmt)\nif err != nil && strings.Contains(err.Error(), \"both must be set\") {\n    // repair labels to a consistent state, then retry the request\n    if repairErr := fixOwnerLabels(mgmt); repairErr != nil {\n        return repairErr\n    }\n    lc, err = getLifecycleContext(mgmt)\n}\nif err != nil {\n    return err\n}","preventionTips":["Never label mgmt clusters by hand — let provisioning write both owner keys atomically","Lint restored objects for owner-label pairs being both-set or both-absent","Include this check in upgrade validation jobs that migrate old regex-based classifications"],"tags":["configserver","labels","misconfiguration","kubernetes","rke2"],"backgroundTag":null,"analyzedSha":"932558d4e68565aff2d2f36e89ec4a391b06e7c5","analyzedAt":"2026-08-16T04:37:02.125Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}