@skyhook-io/k8s-ui 1.4.2 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/src/components/resources/ResourcesView.tsx +267 -14
  3. package/src/components/resources/renderers/AWSMachineRenderer.tsx +96 -0
  4. package/src/components/resources/renderers/AWSMachineTemplateRenderer.tsx +39 -0
  5. package/src/components/resources/renderers/AWSManagedClusterRenderer.tsx +49 -0
  6. package/src/components/resources/renderers/AWSManagedControlPlaneRenderer.tsx +174 -0
  7. package/src/components/resources/renderers/AWSManagedMachinePoolRenderer.tsx +89 -0
  8. package/src/components/resources/renderers/AzureMachineRenderer.tsx +41 -0
  9. package/src/components/resources/renderers/AzureManagedControlPlaneRenderer.tsx +71 -0
  10. package/src/components/resources/renderers/AzureManagedMachinePoolRenderer.tsx +106 -0
  11. package/src/components/resources/renderers/CAPIClusterClassRenderer.tsx +156 -0
  12. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +240 -0
  13. package/src/components/resources/renderers/CAPIKubeadmConfigRenderer.tsx +97 -0
  14. package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +124 -0
  15. package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +128 -0
  16. package/src/components/resources/renderers/CAPIMachineDrainRuleRenderer.tsx +38 -0
  17. package/src/components/resources/renderers/CAPIMachineHealthCheckRenderer.tsx +146 -0
  18. package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +92 -0
  19. package/src/components/resources/renderers/CAPIMachineRenderer.tsx +170 -0
  20. package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +101 -0
  21. package/src/components/resources/renderers/GCPMachineRenderer.tsx +53 -0
  22. package/src/components/resources/renderers/GCPManagedControlPlaneRenderer.tsx +75 -0
  23. package/src/components/resources/renderers/GCPManagedMachinePoolRenderer.tsx +102 -0
  24. package/src/components/resources/renderers/aws-capi-cells.tsx +117 -0
  25. package/src/components/resources/renderers/azure-capi-cells.tsx +83 -0
  26. package/src/components/resources/renderers/capi-cells.tsx +158 -0
  27. package/src/components/resources/renderers/gcp-capi-cells.tsx +68 -0
  28. package/src/components/resources/renderers/index.ts +25 -0
  29. package/src/components/resources/resource-utils-aws-capi.ts +211 -0
  30. package/src/components/resources/resource-utils-azure-capi.ts +123 -0
  31. package/src/components/resources/resource-utils-capi.ts +305 -0
  32. package/src/components/resources/resource-utils-gcp-capi.ts +130 -0
  33. package/src/components/shared/ResourceRendererDispatch.tsx +97 -2
  34. package/src/components/topology/K8sResourceNode.tsx +8 -0
  35. package/src/components/topology/TopologyControls.tsx +16 -0
  36. package/src/components/topology/TopologyFilterSidebar.tsx +15 -0
  37. package/src/components/topology/TopologyGraph.tsx +2 -1
  38. package/src/components/topology/layout.ts +11 -0
  39. package/src/components/topology/topology.css +29 -0
  40. package/src/components/ui/Badge.tsx +10 -0
  41. package/src/components/ui/drawer-components.tsx +54 -17
  42. package/src/types/core.ts +18 -2
  43. package/src/utils/api-resources.ts +4 -0
  44. package/src/utils/resource-icons.ts +35 -0
@@ -0,0 +1,211 @@
1
+ // AWS CAPI Infrastructure Provider utility functions
2
+
3
+ import type { StatusBadge } from './resource-utils'
4
+ import { healthColors } from './resource-utils'
5
+ import { getCAPIConditions, getCAPIReadyStatus } from './resource-utils-capi'
6
+
7
+ // ============================================================================
8
+ // AWSManagedControlPlane
9
+ // ============================================================================
10
+
11
+ export function getAWSMCPStatus(resource: any): StatusBadge {
12
+ return getCAPIReadyStatus(resource)
13
+ }
14
+
15
+ export function getAWSMCPEKSClusterName(resource: any): string {
16
+ return resource.spec?.eksClusterName || '-'
17
+ }
18
+
19
+ export function getAWSMCPRegion(resource: any): string {
20
+ return resource.spec?.region || '-'
21
+ }
22
+
23
+ export function getAWSMCPVersion(resource: any): string {
24
+ return resource.spec?.version || '-'
25
+ }
26
+
27
+ export function getAWSMCPEndpointAccess(resource: any): string {
28
+ const pub = resource.spec?.endpointAccess?.public
29
+ const priv = resource.spec?.endpointAccess?.private
30
+ if (pub && priv) return 'Public & Private'
31
+ if (pub) return 'Public'
32
+ if (priv) return 'Private'
33
+ return '-'
34
+ }
35
+
36
+ export interface AWSAddon {
37
+ name: string
38
+ specVersion: string
39
+ statusVersion: string
40
+ status: string
41
+ arn: string
42
+ }
43
+
44
+ export function getAWSMCPAddons(resource: any): AWSAddon[] {
45
+ const specAddons = resource.spec?.addons || []
46
+ const statusAddons = resource.status?.addons || []
47
+ const statusMap = new Map(statusAddons.map((a: any) => [a.name, a]))
48
+
49
+ return specAddons.map((sa: any) => {
50
+ const st: any = statusMap.get(sa.name) || {}
51
+ return {
52
+ name: sa.name,
53
+ specVersion: sa.version || '-',
54
+ statusVersion: st.currentVersion || st.version || '-',
55
+ status: st.status || 'Unknown',
56
+ arn: st.arn || '-',
57
+ }
58
+ })
59
+ }
60
+
61
+ export interface AWSSubnet {
62
+ id: string
63
+ az: string
64
+ isPublic: boolean
65
+ cidrBlock: string
66
+ }
67
+
68
+ export function getAWSMCPSubnets(resource: any): AWSSubnet[] {
69
+ const subnets = resource.spec?.network?.subnets || []
70
+ return subnets.map((s: any) => ({
71
+ id: s.id || s.resourceID || '-',
72
+ az: s.availabilityZone || '-',
73
+ isPublic: !!s.isPublic,
74
+ cidrBlock: s.cidrBlock || '-',
75
+ }))
76
+ }
77
+
78
+ export interface AWSSecurityGroup {
79
+ role: string
80
+ id: string
81
+ name: string
82
+ }
83
+
84
+ export function getAWSMCPSecurityGroups(resource: any): AWSSecurityGroup[] {
85
+ const sgs = resource.status?.networkStatus?.securityGroups || {}
86
+ const result: AWSSecurityGroup[] = []
87
+ for (const [role, sg] of Object.entries(sgs) as [string, any][]) {
88
+ result.push({ role, id: sg.id || '-', name: sg.name || '-' })
89
+ }
90
+ return result
91
+ }
92
+
93
+ export function getAWSMCPNATGatewayIPs(resource: any): string[] {
94
+ return resource.status?.networkStatus?.natGatewaysIPs || []
95
+ }
96
+
97
+ export function getAWSMCPFailureDomains(resource: any): string[] {
98
+ const fd = resource.status?.failureDomains || {}
99
+ return Object.keys(fd).sort()
100
+ }
101
+
102
+ export function getAWSMCPVPC(resource: any): { id: string; cidrBlock: string } {
103
+ const vpc = resource.spec?.network?.vpc || {}
104
+ return { id: vpc.id || '-', cidrBlock: vpc.cidrBlock || '-' }
105
+ }
106
+
107
+ // ============================================================================
108
+ // AWSManagedMachinePool
109
+ // ============================================================================
110
+
111
+ export function getAWSMMPStatus(resource: any): StatusBadge {
112
+ return getCAPIReadyStatus(resource)
113
+ }
114
+
115
+ export function getAWSMMPInstanceType(resource: any): string {
116
+ return resource.spec?.instanceType || '-'
117
+ }
118
+
119
+ export function getAWSMMPCapacityType(resource: any): string {
120
+ return resource.spec?.capacityType || '-'
121
+ }
122
+
123
+ export function getAWSMMPAMIType(resource: any): string {
124
+ return resource.spec?.amiType || '-'
125
+ }
126
+
127
+ export function getAWSMMPNodegroupName(resource: any): string {
128
+ return resource.spec?.eksNodegroupName || '-'
129
+ }
130
+
131
+ export function getAWSMMPScaling(resource: any): { min: number; max: number } {
132
+ return {
133
+ min: resource.spec?.scaling?.minSize ?? 0,
134
+ max: resource.spec?.scaling?.maxSize ?? 0,
135
+ }
136
+ }
137
+
138
+ export function getAWSMMPReplicas(resource: any): string {
139
+ const ready = resource.status?.replicas ?? 0
140
+ const desired = resource.spec?.scaling?.maxSize
141
+ if (desired != null) return `${ready}/${desired}`
142
+ return String(ready)
143
+ }
144
+
145
+ // ============================================================================
146
+ // AWSMachine
147
+ // ============================================================================
148
+
149
+ export function getAWSMachineStatus(resource: any): StatusBadge {
150
+ const state = resource.status?.instanceState?.toLowerCase()
151
+ if (state === 'running') {
152
+ const conditions = getCAPIConditions(resource)
153
+ const readyCond = conditions.find((c: any) => c.type === 'Ready')
154
+ if (readyCond?.status === 'True') return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
155
+ if (readyCond?.status === 'False') return { text: readyCond.reason || 'NotReady', color: healthColors.unhealthy, level: 'unhealthy' }
156
+ return { text: 'Running', color: healthColors.healthy, level: 'healthy' }
157
+ }
158
+ if (state === 'pending') return { text: 'Pending', color: healthColors.degraded, level: 'degraded' }
159
+ if (state === 'terminated' || state === 'shutting-down') return { text: state, color: healthColors.unhealthy, level: 'unhealthy' }
160
+ if (state === 'stopping' || state === 'stopped') return { text: state, color: healthColors.degraded, level: 'degraded' }
161
+ return getCAPIReadyStatus(resource)
162
+ }
163
+
164
+ export function getAWSMachineInstanceType(resource: any): string {
165
+ return resource.spec?.instanceType || '-'
166
+ }
167
+
168
+ export function getAWSMachineInstanceState(resource: any): string {
169
+ return resource.status?.instanceState || '-'
170
+ }
171
+
172
+ export function getAWSMachineInstanceID(resource: any): string {
173
+ return resource.spec?.instanceID || '-'
174
+ }
175
+
176
+ // ============================================================================
177
+ // AWSMachineTemplate
178
+ // ============================================================================
179
+
180
+ export function getAWSMTInstanceType(resource: any): string {
181
+ return resource.spec?.template?.spec?.instanceType || '-'
182
+ }
183
+
184
+ export function getAWSMTCapacity(resource: any): string {
185
+ const cap = resource.status?.capacity
186
+ if (!cap) return '-'
187
+ const parts: string[] = []
188
+ if (cap.cpu) parts.push(`${cap.cpu} CPU`)
189
+ if (cap.memory) parts.push(cap.memory)
190
+ return parts.join(', ') || '-'
191
+ }
192
+
193
+ // ============================================================================
194
+ // AWSManagedCluster
195
+ // ============================================================================
196
+
197
+ export function getAWSManagedClusterStatus(resource: any): StatusBadge {
198
+ return getCAPIReadyStatus(resource)
199
+ }
200
+
201
+ export function getAWSManagedClusterEndpoint(resource: any): string {
202
+ const host = resource.spec?.controlPlaneEndpoint?.host
203
+ const port = resource.spec?.controlPlaneEndpoint?.port
204
+ if (host) return port && port !== 443 ? `${host}:${port}` : host
205
+ return '-'
206
+ }
207
+
208
+ export function getAWSManagedClusterFailureDomains(resource: any): string[] {
209
+ const fd = resource.status?.failureDomains || {}
210
+ return Object.keys(fd).sort()
211
+ }
@@ -0,0 +1,123 @@
1
+ // Azure CAPI Infrastructure Provider utility functions
2
+
3
+ import type { StatusBadge } from './resource-utils'
4
+ import { getCAPIReadyStatus } from './resource-utils-capi'
5
+
6
+ // ============================================================================
7
+ // AzureManagedControlPlane (AKS)
8
+ // ============================================================================
9
+
10
+ export function getAzureMCPStatus(resource: any): StatusBadge {
11
+ return getCAPIReadyStatus(resource)
12
+ }
13
+
14
+ export function getAzureMCPLocation(resource: any): string {
15
+ return resource.spec?.location || '-'
16
+ }
17
+
18
+ export function getAzureMCPVersion(resource: any): string {
19
+ return resource.spec?.version || '-'
20
+ }
21
+
22
+ export function getAzureMCPResourceGroup(resource: any): string {
23
+ return resource.spec?.resourceGroupName || '-'
24
+ }
25
+
26
+ export function getAzureMCPSKUTier(resource: any): string {
27
+ return resource.spec?.sku?.tier || '-'
28
+ }
29
+
30
+ export function getAzureMCPNetworkPlugin(resource: any): string {
31
+ return resource.spec?.networkPlugin || '-'
32
+ }
33
+
34
+ export function getAzureMCPNetworkPolicy(resource: any): string {
35
+ return resource.spec?.networkPolicy || '-'
36
+ }
37
+
38
+ export function getAzureMCPDNSPrefix(resource: any): string {
39
+ return resource.spec?.dnsPrefix || '-'
40
+ }
41
+
42
+ export function getAzureMCPUpgradeChannel(resource: any): string {
43
+ return resource.spec?.autoUpgradeProfile?.upgradeChannel || '-'
44
+ }
45
+
46
+ export function getAzureMCPPrivateCluster(resource: any): boolean {
47
+ return !!resource.spec?.apiServerAccessProfile?.enablePrivateCluster
48
+ }
49
+
50
+ // ============================================================================
51
+ // AzureManagedMachinePool (AKS node pool)
52
+ // ============================================================================
53
+
54
+ export function getAzureMMPStatus(resource: any): StatusBadge {
55
+ return getCAPIReadyStatus(resource)
56
+ }
57
+
58
+ export function getAzureMMPSKU(resource: any): string {
59
+ return resource.spec?.sku || '-'
60
+ }
61
+
62
+ export function getAzureMMPMode(resource: any): string {
63
+ return resource.spec?.mode || '-'
64
+ }
65
+
66
+ export function getAzureMMPOSDiskInfo(resource: any): string {
67
+ const diskType = resource.spec?.osDiskType || 'Managed'
68
+ const diskSize = resource.spec?.osDiskSizeGB
69
+ if (diskSize) return `${diskType} ${diskSize}GB`
70
+ return diskType
71
+ }
72
+
73
+ export function getAzureMMPScaling(resource: any): { min: number; max: number } {
74
+ const scaling = resource.spec?.scaling || {}
75
+ return {
76
+ min: scaling.minSize ?? 0,
77
+ max: scaling.maxSize ?? 0,
78
+ }
79
+ }
80
+
81
+ export function getAzureMMPReplicas(resource: any): string {
82
+ return String(resource.status?.replicas ?? 0)
83
+ }
84
+
85
+ export function getAzureMMPScaleSetPriority(resource: any): string {
86
+ return resource.spec?.scaleSetPriority || 'Regular'
87
+ }
88
+
89
+ export function getAzureMMPOSType(resource: any): string {
90
+ return resource.spec?.osType || 'Linux'
91
+ }
92
+
93
+ // ============================================================================
94
+ // AzureMachine
95
+ // ============================================================================
96
+
97
+ export function getAzureMachineStatus(resource: any): StatusBadge {
98
+ return getCAPIReadyStatus(resource)
99
+ }
100
+
101
+ export function getAzureMachineVMSize(resource: any): string {
102
+ return resource.spec?.vmSize || '-'
103
+ }
104
+
105
+ export function getAzureMachineProviderID(resource: any): string {
106
+ return resource.spec?.providerID || '-'
107
+ }
108
+
109
+ // ============================================================================
110
+ // AzureMachineTemplate
111
+ // ============================================================================
112
+
113
+ export function getAzureMTVMSize(resource: any): string {
114
+ return resource.spec?.template?.spec?.vmSize || '-'
115
+ }
116
+
117
+ // ============================================================================
118
+ // AzureManagedCluster
119
+ // ============================================================================
120
+
121
+ export function getAzureManagedClusterStatus(resource: any): StatusBadge {
122
+ return getCAPIReadyStatus(resource)
123
+ }
@@ -0,0 +1,305 @@
1
+ // Cluster API (CAPI) CRD utility functions
2
+
3
+ import type { StatusBadge } from './resource-utils'
4
+ import { healthColors } from './resource-utils'
5
+
6
+ // ============================================================================
7
+ // SHARED CAPI UTILITIES
8
+ // ============================================================================
9
+
10
+ // CAPI phase-to-health mapping
11
+ const PHASE_MAP: Record<string, { text: string; level: StatusBadge['level'] }> = {
12
+ provisioned: { text: 'Provisioned', level: 'healthy' },
13
+ running: { text: 'Running', level: 'healthy' },
14
+ scaled: { text: 'Scaled', level: 'healthy' },
15
+ provisioning: { text: 'Provisioning', level: 'degraded' },
16
+ pending: { text: 'Pending', level: 'degraded' },
17
+ scaling: { text: 'Scaling', level: 'degraded' },
18
+ upgrading: { text: 'Upgrading', level: 'degraded' },
19
+ deleting: { text: 'Deleting', level: 'degraded' },
20
+ failed: { text: 'Failed', level: 'unhealthy' },
21
+ }
22
+
23
+ export function getCAPIConditions(resource: any): any[] {
24
+ // v1beta2 uses status.v1beta2.conditions, v1beta1 uses status.conditions
25
+ return resource.status?.v1beta2?.conditions || resource.status?.conditions || []
26
+ }
27
+
28
+ export function getCAPIReadyCondition(resource: any): any | undefined {
29
+ const conditions = getCAPIConditions(resource)
30
+ return conditions.find((c: any) => c.type === 'Ready') || conditions.find((c: any) => c.type === 'Available')
31
+ }
32
+
33
+ function getCAPIPhaseStatus(resource: any): StatusBadge {
34
+ const phase = resource.status?.phase?.toLowerCase()
35
+ if (phase && PHASE_MAP[phase]) {
36
+ const m = PHASE_MAP[phase]
37
+ return { text: m.text, color: healthColors[m.level], level: m.level }
38
+ }
39
+
40
+ // Fall back to Ready condition
41
+ const readyCond = getCAPIReadyCondition(resource)
42
+ if (readyCond?.status === 'True') {
43
+ return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
44
+ }
45
+ if (readyCond?.status === 'False') {
46
+ return { text: readyCond.reason || 'NotReady', color: healthColors.unhealthy, level: 'unhealthy' }
47
+ }
48
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
49
+ }
50
+
51
+ export function getCAPIReadyStatus(resource: any): StatusBadge {
52
+ const readyCond = getCAPIReadyCondition(resource)
53
+ if (readyCond?.status === 'True') {
54
+ return { text: 'Ready', color: healthColors.healthy, level: 'healthy' }
55
+ }
56
+ if (readyCond?.status === 'False') {
57
+ return { text: readyCond.reason || 'NotReady', color: healthColors.unhealthy, level: 'unhealthy' }
58
+ }
59
+ return { text: 'Unknown', color: healthColors.unknown, level: 'unknown' }
60
+ }
61
+
62
+ // Normalize v1beta1 updatedReplicas vs v1beta2 upToDateReplicas
63
+ function getUpToDateReplicas(resource: any): number {
64
+ return resource.status?.upToDateReplicas ?? resource.status?.updatedReplicas ?? 0
65
+ }
66
+
67
+ // ============================================================================
68
+ // CAPI CLUSTER UTILITIES
69
+ // ============================================================================
70
+
71
+ export function getClusterStatus(resource: any): StatusBadge {
72
+ return getCAPIPhaseStatus(resource)
73
+ }
74
+
75
+ export function getClusterClass(resource: any): string {
76
+ return resource.spec?.topology?.class || '-'
77
+ }
78
+
79
+ export function getClusterVersion(resource: any): string {
80
+ return resource.spec?.topology?.version || '-'
81
+ }
82
+
83
+ export function getClusterCPReplicas(resource: any): string {
84
+ // v1beta2: status.controlPlane.readyReplicas / status.controlPlane.desiredReplicas
85
+ const cpReady = resource.status?.controlPlane?.readyReplicas ?? resource.status?.controlPlaneReady
86
+ const cpDesired = resource.status?.controlPlane?.desiredReplicas
87
+ if (cpDesired != null) return `${cpReady ?? 0}/${cpDesired}`
88
+ return typeof cpReady === 'boolean' ? (cpReady ? 'Ready' : 'NotReady') : '-'
89
+ }
90
+
91
+ export function getClusterWorkerReplicas(resource: any): string {
92
+ // v1beta2: status.workers.readyReplicas / status.workers.desiredReplicas
93
+ const wReady = resource.status?.workers?.readyReplicas ?? resource.status?.workersReady
94
+ const wDesired = resource.status?.workers?.desiredReplicas
95
+ if (wDesired != null) return `${wReady ?? 0}/${wDesired}`
96
+ return typeof wReady === 'boolean' ? (wReady ? 'Ready' : 'NotReady') : '-'
97
+ }
98
+
99
+ export function getClusterEndpoint(resource: any): string {
100
+ const host = resource.spec?.controlPlaneEndpoint?.host
101
+ const port = resource.spec?.controlPlaneEndpoint?.port
102
+ if (host) return port ? `${host}:${port}` : host
103
+ return '-'
104
+ }
105
+
106
+ // ============================================================================
107
+ // CAPI MACHINE UTILITIES
108
+ // ============================================================================
109
+
110
+ export function getMachineStatus(resource: any): StatusBadge {
111
+ return getCAPIPhaseStatus(resource)
112
+ }
113
+
114
+ export function getMachineRole(resource: any): string {
115
+ const labels = resource.metadata?.labels || {}
116
+ if (labels['cluster.x-k8s.io/control-plane'] !== undefined) return 'Control Plane'
117
+ if (labels['cluster.x-k8s.io/control-plane-name']) return 'Control Plane'
118
+ return 'Worker'
119
+ }
120
+
121
+ export function getMachineClusterName(resource: any): string {
122
+ return resource.metadata?.labels?.['cluster.x-k8s.io/cluster-name'] || resource.spec?.clusterName || '-'
123
+ }
124
+
125
+ export function getMachineNodeRef(resource: any): string {
126
+ return resource.status?.nodeRef?.name || '-'
127
+ }
128
+
129
+ export function getMachineVersion(resource: any): string {
130
+ return resource.spec?.version || '-'
131
+ }
132
+
133
+ export function getMachineProviderID(resource: any): string {
134
+ return resource.spec?.providerID || '-'
135
+ }
136
+
137
+ // ============================================================================
138
+ // PROVIDER DETECTION UTILITIES
139
+ // ============================================================================
140
+
141
+ export interface InfraProvider {
142
+ provider: string // 'AWS' | 'GCP' | 'Azure' | 'vSphere' | 'Docker' | unknown
143
+ region?: string // availability zone or region
144
+ instanceId?: string // cloud instance identifier
145
+ }
146
+
147
+ /** Parse spec.providerID to extract cloud provider, region, and instance ID */
148
+ export function parseProviderID(providerID: string): InfraProvider | null {
149
+ if (!providerID || providerID === '-') return null
150
+
151
+ // AWS: aws:///us-east-1a/i-0abcdef1234567890 (or aws://us-east-1a/...)
152
+ if (providerID.startsWith('aws://')) {
153
+ const parts = providerID.replace(/^aws:\/\/\/?/, '').split('/')
154
+ return { provider: 'AWS', region: parts[0] || undefined, instanceId: parts[1] || undefined }
155
+ }
156
+
157
+ // GCP: gce:///my-project/us-central1-a/my-instance (or gce://...)
158
+ if (providerID.startsWith('gce://')) {
159
+ const parts = providerID.replace(/^gce:\/\/\/?/, '').split('/')
160
+ return { provider: 'GCP', region: parts[1] || undefined, instanceId: parts[2] || undefined }
161
+ }
162
+
163
+ // Azure: azure:///subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/{name}
164
+ if (providerID.startsWith('azure://')) {
165
+ const vmMatch = providerID.match(/virtualMachines\/([^/]+)/)
166
+ const rgMatch = providerID.match(/resourceGroups\/([^/]+)/)
167
+ return { provider: 'Azure', region: rgMatch?.[1], instanceId: vmMatch?.[1] }
168
+ }
169
+
170
+ // vSphere: vsphere://42305a8e-...
171
+ if (providerID.startsWith('vsphere://')) {
172
+ return { provider: 'vSphere', instanceId: providerID.replace(/^vsphere:\/\/\/?/, '') }
173
+ }
174
+
175
+ // Docker (CAPD): docker:///container-name (or docker://...)
176
+ if (providerID.startsWith('docker://')) {
177
+ return { provider: 'Docker', instanceId: providerID.replace(/^docker:\/\/\/?/, '') }
178
+ }
179
+
180
+ return { provider: providerID.split(':')[0] || 'Unknown' }
181
+ }
182
+
183
+ /** Detect provider from an infrastructureRef kind name */
184
+ export function getProviderFromInfraKind(kind: string): string {
185
+ const k = kind.toLowerCase()
186
+ if (k.startsWith('aws') || k.startsWith('ec2')) return 'AWS'
187
+ if (k.startsWith('gcp') || k.startsWith('gce')) return 'GCP'
188
+ if (k.startsWith('azure') || k.startsWith('aks')) return 'Azure'
189
+ if (k.startsWith('vsphere')) return 'vSphere'
190
+ if (k.startsWith('docker')) return 'Docker'
191
+ if (k.startsWith('metal') || k.startsWith('byoh')) return 'Bare Metal'
192
+ return kind // Return raw kind as fallback
193
+ }
194
+
195
+ // ============================================================================
196
+ // CAPI MACHINEDEPLOYMENT UTILITIES
197
+ // ============================================================================
198
+
199
+ export function getMachineDeploymentStatus(resource: any): StatusBadge {
200
+ return getCAPIPhaseStatus(resource)
201
+ }
202
+
203
+ export function getMachineDeploymentReplicas(resource: any): string {
204
+ const desired = resource.spec?.replicas ?? 0
205
+ const ready = resource.status?.readyReplicas ?? 0
206
+ return `${ready}/${desired}`
207
+ }
208
+
209
+ export function getMachineDeploymentVersion(resource: any): string {
210
+ return resource.spec?.template?.spec?.version || '-'
211
+ }
212
+
213
+ export function getMachineDeploymentUpToDate(resource: any): string {
214
+ return String(getUpToDateReplicas(resource))
215
+ }
216
+
217
+ // ============================================================================
218
+ // CAPI KUBEADMCONTROLPLANE UTILITIES
219
+ // ============================================================================
220
+
221
+ export function getKCPStatus(resource: any): StatusBadge {
222
+ return getCAPIReadyStatus(resource)
223
+ }
224
+
225
+ export function getKCPReplicas(resource: any): string {
226
+ const desired = resource.spec?.replicas ?? 0
227
+ const ready = resource.status?.readyReplicas ?? 0
228
+ return `${ready}/${desired}`
229
+ }
230
+
231
+ export function getKCPVersion(resource: any): string {
232
+ return resource.spec?.version || '-'
233
+ }
234
+
235
+ export function getKCPInitialized(resource: any): boolean {
236
+ // v1beta2: status.initialization.controlPlaneInitialized; v1beta1: status.initialized
237
+ return resource.status?.initialization?.controlPlaneInitialized ?? resource.status?.initialized ?? false
238
+ }
239
+
240
+ // ============================================================================
241
+ // CAPI MACHINESET UTILITIES
242
+ // ============================================================================
243
+
244
+ export function getMachineSetStatus(resource: any): StatusBadge {
245
+ return getCAPIPhaseStatus(resource)
246
+ }
247
+
248
+ export function getMachineSetReplicas(resource: any): string {
249
+ const desired = resource.spec?.replicas ?? 0
250
+ const ready = resource.status?.readyReplicas ?? 0
251
+ return `${ready}/${desired}`
252
+ }
253
+
254
+ // ============================================================================
255
+ // CAPI MACHINEPOOL UTILITIES
256
+ // ============================================================================
257
+
258
+ export function getMachinePoolStatus(resource: any): StatusBadge {
259
+ return getCAPIPhaseStatus(resource)
260
+ }
261
+
262
+ export function getMachinePoolReplicas(resource: any): string {
263
+ const desired = resource.spec?.replicas ?? 0
264
+ const ready = resource.status?.readyReplicas ?? 0
265
+ return `${ready}/${desired}`
266
+ }
267
+
268
+ // ============================================================================
269
+ // CAPI CLUSTERCLASS UTILITIES
270
+ // ============================================================================
271
+
272
+ export function getClusterClassStatus(resource: any): StatusBadge {
273
+ return getCAPIReadyStatus(resource)
274
+ }
275
+
276
+ // ============================================================================
277
+ // CAPI MACHINEHEALTHCHECK UTILITIES
278
+ // ============================================================================
279
+
280
+ export function getMachineHealthCheckStatus(resource: any): StatusBadge {
281
+ return getCAPIReadyStatus(resource)
282
+ }
283
+
284
+ export function getMachineHealthCheckClusterName(resource: any): string {
285
+ return resource.spec?.clusterName || resource.metadata?.labels?.['cluster.x-k8s.io/cluster-name'] || '-'
286
+ }
287
+
288
+ export function getMachineHealthCheckHealthy(resource: any): string {
289
+ const expected = resource.status?.expectedMachines ?? 0
290
+ const healthy = resource.status?.currentHealthy ?? 0
291
+ return `${healthy}/${expected}`
292
+ }
293
+
294
+ export function getClusterProvider(resource: any): string {
295
+ const infraKind = resource.spec?.infrastructureRef?.kind || ''
296
+ if (infraKind) return getProviderFromInfraKind(infraKind)
297
+ return '-'
298
+ }
299
+
300
+ /** Parse CAPI compound condition messages ("* Foo: bar * Baz: qux") into a structured list */
301
+ export function parseCAPIConditionMessage(message: string): string[] | null {
302
+ if (!message || !message.includes('*')) return null
303
+ const items = message.split(/\s*\*\s*/).filter(Boolean).map(s => s.trim())
304
+ return items.length > 1 ? items : null
305
+ }