diff --git a/plugins/Kubernetes/v1/configValidation.json b/plugins/Kubernetes/v1/configValidation.json new file mode 100644 index 00000000..017fb792 --- /dev/null +++ b/plugins/Kubernetes/v1/configValidation.json @@ -0,0 +1,13 @@ +{ + "steps": [ + { + "displayName": "Authenticate", + "dataStream": { + "name": "namespaces" + }, + "required": true, + "error": "Could not connect to the Kubernetes API server. Check the API Server URL is reachable, the Bearer Token is valid, and enable 'Ignore certificate errors' if the cluster uses a self-signed certificate.", + "success": "Connected to the Kubernetes API server successfully." + } + ] +} diff --git a/plugins/Kubernetes/v1/correlationRules/relate-daemonset-to-pod.json b/plugins/Kubernetes/v1/correlationRules/relate-daemonset-to-pod.json new file mode 100644 index 00000000..4d66e622 --- /dev/null +++ b/plugins/Kubernetes/v1/correlationRules/relate-daemonset-to-pod.json @@ -0,0 +1,20 @@ +{ + "displayName": "DaemonSet owns Pod", + "source": { + "types": "Pod" + }, + "target": { + "types": "DaemonSet" + }, + "conditions": [ + { + "sourceProperty": "ownerUid", + "operator": "equals", + "targetProperty": "uid" + } + ], + "labels": { + "forward": "owned by", + "reverse": "runs" + } +} diff --git a/plugins/Kubernetes/v1/correlationRules/relate-deployment-to-replicaset.json b/plugins/Kubernetes/v1/correlationRules/relate-deployment-to-replicaset.json new file mode 100644 index 00000000..d1c79f0b --- /dev/null +++ b/plugins/Kubernetes/v1/correlationRules/relate-deployment-to-replicaset.json @@ -0,0 +1,20 @@ +{ + "displayName": "Deployment owns ReplicaSet", + "source": { + "types": "ReplicaSet" + }, + "target": { + "types": "Deployment" + }, + "conditions": [ + { + "sourceProperty": "ownerUid", + "operator": "equals", + "targetProperty": "uid" + } + ], + "labels": { + "forward": "owned by", + "reverse": "manages" + } +} diff --git a/plugins/Kubernetes/v1/correlationRules/relate-ingress-to-service.json b/plugins/Kubernetes/v1/correlationRules/relate-ingress-to-service.json new file mode 100644 index 00000000..27da7300 --- /dev/null +++ b/plugins/Kubernetes/v1/correlationRules/relate-ingress-to-service.json @@ -0,0 +1,25 @@ +{ + "displayName": "Ingress routes to Service", + "source": { + "types": "Ingress" + }, + "target": { + "types": "Service" + }, + "conditions": [ + { + "sourceProperty": "primaryBackendServiceName", + "operator": "equals", + "targetProperty": "name" + }, + { + "sourceProperty": "namespace", + "operator": "equals", + "targetProperty": "namespace" + } + ], + "labels": { + "forward": "routes to", + "reverse": "exposed via" + } +} diff --git a/plugins/Kubernetes/v1/correlationRules/relate-namespace-to-objects.json b/plugins/Kubernetes/v1/correlationRules/relate-namespace-to-objects.json new file mode 100644 index 00000000..08871e2e --- /dev/null +++ b/plugins/Kubernetes/v1/correlationRules/relate-namespace-to-objects.json @@ -0,0 +1,30 @@ +{ + "displayName": "Namespace contains Object", + "source": { + "types": "Namespace" + }, + "target": { + "types": [ + "Pod", + "Deployment", + "ReplicaSet", + "DaemonSet", + "StatefulSet", + "Service", + "Ingress", + "Resource Quota", + "Limit Range" + ] + }, + "conditions": [ + { + "sourceProperty": "name", + "operator": "equals", + "targetProperty": "namespace" + } + ], + "labels": { + "forward": "contains", + "reverse": "belongs to" + } +} diff --git a/plugins/Kubernetes/v1/correlationRules/relate-node-to-pod.json b/plugins/Kubernetes/v1/correlationRules/relate-node-to-pod.json new file mode 100644 index 00000000..ed7c42d4 --- /dev/null +++ b/plugins/Kubernetes/v1/correlationRules/relate-node-to-pod.json @@ -0,0 +1,20 @@ +{ + "displayName": "Node runs Pod", + "source": { + "types": "Pod" + }, + "target": { + "types": "Node" + }, + "conditions": [ + { + "sourceProperty": "nodeName", + "operator": "equals", + "targetProperty": "name" + } + ], + "labels": { + "forward": "scheduled on", + "reverse": "hosts" + } +} diff --git a/plugins/Kubernetes/v1/correlationRules/relate-replicaset-to-pod.json b/plugins/Kubernetes/v1/correlationRules/relate-replicaset-to-pod.json new file mode 100644 index 00000000..f746761e --- /dev/null +++ b/plugins/Kubernetes/v1/correlationRules/relate-replicaset-to-pod.json @@ -0,0 +1,20 @@ +{ + "displayName": "ReplicaSet owns Pod", + "source": { + "types": "Pod" + }, + "target": { + "types": "ReplicaSet" + }, + "conditions": [ + { + "sourceProperty": "ownerUid", + "operator": "equals", + "targetProperty": "uid" + } + ], + "labels": { + "forward": "owned by", + "reverse": "runs" + } +} diff --git a/plugins/Kubernetes/v1/correlationRules/relate-service-to-pod.json b/plugins/Kubernetes/v1/correlationRules/relate-service-to-pod.json new file mode 100644 index 00000000..173a413f --- /dev/null +++ b/plugins/Kubernetes/v1/correlationRules/relate-service-to-pod.json @@ -0,0 +1,25 @@ +{ + "displayName": "Service routes to Pod", + "source": { + "types": "Service" + }, + "target": { + "types": "Pod" + }, + "conditions": [ + { + "sourceProperty": "selectorAppLabel", + "operator": "equals", + "targetProperty": "appLabel" + }, + { + "sourceProperty": "namespace", + "operator": "equals", + "targetProperty": "namespace" + } + ], + "labels": { + "forward": "routes to", + "reverse": "exposed by" + } +} diff --git a/plugins/Kubernetes/v1/correlationRules/relate-statefulset-to-pod.json b/plugins/Kubernetes/v1/correlationRules/relate-statefulset-to-pod.json new file mode 100644 index 00000000..afbe1a0c --- /dev/null +++ b/plugins/Kubernetes/v1/correlationRules/relate-statefulset-to-pod.json @@ -0,0 +1,20 @@ +{ + "displayName": "StatefulSet owns Pod", + "source": { + "types": "Pod" + }, + "target": { + "types": "StatefulSet" + }, + "conditions": [ + { + "sourceProperty": "ownerUid", + "operator": "equals", + "targetProperty": "uid" + } + ], + "labels": { + "forward": "owned by", + "reverse": "runs" + } +} diff --git a/plugins/Kubernetes/v1/custom_types.json b/plugins/Kubernetes/v1/custom_types.json new file mode 100644 index 00000000..ccd1fe41 --- /dev/null +++ b/plugins/Kubernetes/v1/custom_types.json @@ -0,0 +1,86 @@ +[ + { + "name": "Namespace", + "sourceType": "Namespace", + "icon": "folder", + "singular": "Namespace", + "plural": "Namespaces" + }, + { + "name": "Node", + "sourceType": "Node", + "icon": "server", + "singular": "Node", + "plural": "Nodes" + }, + { + "name": "Deployment", + "sourceType": "Deployment", + "icon": "rocket", + "singular": "Deployment", + "plural": "Deployments" + }, + { + "name": "ReplicaSet", + "sourceType": "ReplicaSet", + "icon": "clone", + "singular": "ReplicaSet", + "plural": "ReplicaSets" + }, + { + "name": "DaemonSet", + "sourceType": "DaemonSet", + "icon": "layer-group", + "singular": "DaemonSet", + "plural": "DaemonSets" + }, + { + "name": "StatefulSet", + "sourceType": "StatefulSet", + "icon": "database", + "singular": "StatefulSet", + "plural": "StatefulSets" + }, + { + "name": "Pod", + "sourceType": "Pod", + "icon": "cube", + "singular": "Pod", + "plural": "Pods" + }, + { + "name": "Service", + "sourceType": "Service", + "icon": "network-wired", + "singular": "Service", + "plural": "Services" + }, + { + "name": "Ingress", + "sourceType": "Ingress", + "icon": "route", + "singular": "Ingress", + "plural": "Ingresses" + }, + { + "name": "Persistent Volume", + "sourceType": "Persistent Volume", + "icon": "database", + "singular": "Persistent Volume", + "plural": "Persistent Volumes" + }, + { + "name": "Resource Quota", + "sourceType": "Resource Quota", + "icon": "gauge", + "singular": "Resource Quota", + "plural": "Resource Quotas" + }, + { + "name": "Limit Range", + "sourceType": "Limit Range", + "icon": "sliders", + "singular": "Limit Range", + "plural": "Limit Ranges" + } +] diff --git a/plugins/Kubernetes/v1/dataStreams/daemonsets.json b/plugins/Kubernetes/v1/dataStreams/daemonsets.json new file mode 100644 index 00000000..e1644612 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/daemonsets.json @@ -0,0 +1,81 @@ +{ + "name": "daemonsets", + "displayName": "DaemonSets", + "description": "Kubernetes daemonsets, one row per daemonset", + "tags": [ + "Resources" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/apis/apps/v1/daemonsets", + "postRequestScript": "daemonsets.js", + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "250" + }, + "in": { + "realm": "payload", + "path": "metadata.continue" + }, + "out": { + "realm": "queryArg", + "path": "continue" + } + } + }, + "matches": "none", + "metadata": [ + { + "name": "uid", + "displayName": "UID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "namespace", + "displayName": "Namespace", + "shape": "string" + }, + { + "name": "desiredNumberScheduled", + "displayName": "Desired Number Scheduled", + "shape": "number" + }, + { + "name": "numberReady", + "displayName": "Number Ready", + "shape": "number" + }, + { + "name": "daemonSetHealth", + "displayName": "Health", + "shape": [ + "state", + { + "map": { + "success": [ + "success" + ], + "error": [ + "error" + ], + "unknown": [ + "unknown" + ] + } + } + ] + } + ], + "timeframes": false +} diff --git a/plugins/Kubernetes/v1/dataStreams/deployments.json b/plugins/Kubernetes/v1/dataStreams/deployments.json new file mode 100644 index 00000000..ba0912e9 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/deployments.json @@ -0,0 +1,81 @@ +{ + "name": "deployments", + "displayName": "Deployments", + "description": "Kubernetes deployments, one row per deployment", + "tags": [ + "Resources" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/apis/apps/v1/deployments", + "postRequestScript": "deployments.js", + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "250" + }, + "in": { + "realm": "payload", + "path": "metadata.continue" + }, + "out": { + "realm": "queryArg", + "path": "continue" + } + } + }, + "matches": "none", + "metadata": [ + { + "name": "uid", + "displayName": "UID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "namespace", + "displayName": "Namespace", + "shape": "string" + }, + { + "name": "desiredReplicas", + "displayName": "Desired Replicas", + "shape": "number" + }, + { + "name": "availableReplicas", + "displayName": "Available Replicas", + "shape": "number" + }, + { + "name": "availableStatus", + "displayName": "Available", + "shape": [ + "state", + { + "map": { + "success": [ + "True" + ], + "error": [ + "False" + ], + "unknown": [ + "Unknown" + ] + } + } + ] + } + ], + "timeframes": false +} diff --git a/plugins/Kubernetes/v1/dataStreams/events.json b/plugins/Kubernetes/v1/dataStreams/events.json new file mode 100644 index 00000000..552759d5 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/events.json @@ -0,0 +1,91 @@ +{ + "name": "events", + "displayName": "Events", + "description": "Kubernetes cluster events, one row per event", + "tags": [ + "Events" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/api/v1/events", + "postRequestScript": "events.js", + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "250" + }, + "in": { + "realm": "payload", + "path": "metadata.continue" + }, + "out": { + "realm": "queryArg", + "path": "continue" + } + } + }, + "matches": "none", + "metadata": [ + { + "name": "uid", + "displayName": "UID", + "shape": "string", + "role": "id" + }, + { + "name": "reason", + "displayName": "Reason", + "shape": "string", + "role": "label" + }, + { + "name": "type", + "displayName": "Type", + "shape": [ + "state", + { + "map": { + "success": [ + "Normal" + ], + "warning": [ + "Warning" + ] + } + } + ] + }, + { + "name": "message", + "displayName": "Message", + "shape": "string" + }, + { + "name": "lastTimestamp", + "displayName": "Last Seen", + "shape": "date", + "role": "timestamp" + }, + { + "name": "involvedObjectUid", + "displayName": "Involved Object UID", + "shape": "string", + "visible": false + }, + { + "name": "involvedObjectKind", + "displayName": "Involved Object Kind", + "shape": "string" + }, + { + "name": "involvedObjectName", + "displayName": "Involved Object Name", + "shape": "string", + "visible": false + } + ], + "timeframes": false +} diff --git a/plugins/Kubernetes/v1/dataStreams/ingresses.json b/plugins/Kubernetes/v1/dataStreams/ingresses.json new file mode 100644 index 00000000..0daa64ba --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/ingresses.json @@ -0,0 +1,67 @@ +{ + "name": "ingresses", + "displayName": "Ingresses", + "description": "Kubernetes ingresses, one row per ingress", + "tags": [ + "Resources" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/apis/networking.k8s.io/v1/ingresses", + "postRequestScript": "ingresses.js", + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "250" + }, + "in": { + "realm": "payload", + "path": "metadata.continue" + }, + "out": { + "realm": "queryArg", + "path": "continue" + } + } + }, + "matches": "none", + "metadata": [ + { + "name": "uid", + "displayName": "UID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "namespace", + "displayName": "Namespace", + "shape": "string" + }, + { + "name": "ingressClassName", + "displayName": "Ingress Class", + "shape": "string" + }, + { + "name": "hosts", + "displayName": "Hosts", + "shape": "string" + }, + { + "name": "primaryBackendServiceName", + "displayName": "Primary Backend Service", + "shape": "string", + "visible": false + } + ], + "timeframes": false +} diff --git a/plugins/Kubernetes/v1/dataStreams/limitranges.json b/plugins/Kubernetes/v1/dataStreams/limitranges.json new file mode 100644 index 00000000..a4bdd480 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/limitranges.json @@ -0,0 +1,96 @@ +{ + "name": "limitranges", + "displayName": "Limit Ranges", + "description": "Kubernetes limit ranges, one row per limit range", + "tags": [ + "Quotas" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/api/v1/limitranges", + "postRequestScript": "limitranges.js", + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "250" + }, + "in": { + "realm": "payload", + "path": "metadata.continue" + }, + "out": { + "realm": "queryArg", + "path": "continue" + } + } + }, + "matches": "none", + "metadata": [ + { + "name": "uid", + "displayName": "UID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "namespace", + "displayName": "Namespace", + "shape": "string" + }, + { + "name": "limitType", + "displayName": "Limit Type", + "shape": "string" + }, + { + "name": "defaultCpuMillicores", + "displayName": "Default CPU Limit (m)", + "shape": "number" + }, + { + "name": "defaultMemoryBytes", + "displayName": "Default Memory Limit", + "shape": "bytes" + }, + { + "name": "defaultRequestCpuMillicores", + "displayName": "Default CPU Request (m)", + "shape": "number" + }, + { + "name": "defaultRequestMemoryBytes", + "displayName": "Default Memory Request", + "shape": "bytes" + }, + { + "name": "maxCpuMillicores", + "displayName": "Max CPU (m)", + "shape": "number" + }, + { + "name": "maxMemoryBytes", + "displayName": "Max Memory", + "shape": "bytes" + }, + { + "name": "minCpuMillicores", + "displayName": "Min CPU (m)", + "shape": "number" + }, + { + "name": "minMemoryBytes", + "displayName": "Min Memory", + "shape": "bytes" + } + ], + "timeframes": false +} diff --git a/plugins/Kubernetes/v1/dataStreams/namespaceContents.json b/plugins/Kubernetes/v1/dataStreams/namespaceContents.json new file mode 100644 index 00000000..23b728f7 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/namespaceContents.json @@ -0,0 +1,389 @@ +{ + "name": "namespaceContents", + "displayName": "Namespace Contents", + "description": "Objects of a chosen kind running in a namespace, one row per object", + "tags": [ + "Resources" + ], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "{{ '/' + ({ ingresses: 'apis/networking.k8s.io/v1', deployments: 'apis/apps/v1', daemonsets: 'apis/apps/v1', statefulsets: 'apis/apps/v1', replicasets: 'apis/apps/v1' }[detailType] || 'api/v1') + '/namespaces/' + object.name + '/' + detailType }}", + "postRequestScript": "namespaceContents.js", + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "100" + }, + "in": { + "realm": "payload", + "path": "metadata.continue" + }, + "out": { + "realm": "queryArg", + "path": "continue" + } + } + }, + "matches": { + "sourceType": { + "type": "equals", + "value": "Namespace" + } + }, + "ui": [ + { + "allowCustomValues": false, + "data": { + "source": "fixed", + "values": [ + { + "label": "Deployments", + "value": "deployments" + }, + { + "label": "DaemonSets", + "value": "daemonsets" + }, + { + "label": "StatefulSets", + "value": "statefulsets" + }, + { + "label": "ReplicaSets", + "value": "replicasets" + }, + { + "label": "Services", + "value": "services" + }, + { + "label": "Ingresses", + "value": "ingresses" + }, + { + "label": "Persistent Volume Claims", + "value": "persistentvolumeclaims" + }, + { + "label": "Resource Quotas", + "value": "resourcequotas" + }, + { + "label": "Limit Ranges", + "value": "limitranges" + }, + { + "label": "Pods", + "value": "pods" + } + ] + }, + "defaultValue": "pods", + "isClearable": false, + "isMulti": false, + "label": "Detail Type", + "name": "detailType", + "type": "autocomplete", + "validation": { + "required": true + } + } + ], + "metadata": [ + { + "name": "uid", + "displayName": "UID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "namespace", + "displayName": "Namespace", + "shape": "string" + }, + { + "name": "desiredReplicas", + "displayName": "Desired Replicas", + "shape": "number" + }, + { + "name": "availableReplicas", + "displayName": "Available Replicas", + "shape": "number" + }, + { + "name": "availableStatus", + "displayName": "Available", + "shape": [ + "state", + { + "map": { + "success": [ + "True" + ], + "error": [ + "False" + ], + "unknown": [ + "Unknown" + ] + } + } + ] + }, + { + "name": "desiredNumberScheduled", + "displayName": "Desired Scheduled", + "shape": "number" + }, + { + "name": "numberReady", + "displayName": "Ready", + "shape": "number" + }, + { + "name": "daemonSetHealth", + "displayName": "Health", + "shape": [ + "state", + { + "map": { + "success": [ + "success" + ], + "error": [ + "error" + ], + "unknown": [ + "unknown" + ] + } + } + ] + }, + { + "name": "readyReplicas", + "displayName": "Ready Replicas", + "shape": "number" + }, + { + "name": "statefulSetHealth", + "displayName": "Health", + "shape": [ + "state", + { + "map": { + "success": [ + "success" + ], + "error": [ + "error" + ], + "unknown": [ + "unknown" + ] + } + } + ] + }, + { + "name": "ownerUid", + "displayName": "Owner UID", + "shape": "string", + "visible": false + }, + { + "name": "serviceType", + "displayName": "Type", + "shape": "string" + }, + { + "name": "selectorAppLabel", + "displayName": "Selector App Label", + "shape": "string", + "visible": false + }, + { + "name": "ingressClassName", + "displayName": "Ingress Class", + "shape": "string" + }, + { + "name": "hosts", + "displayName": "Hosts", + "shape": "string" + }, + { + "name": "primaryBackendServiceName", + "displayName": "Primary Backend Service", + "shape": "string", + "visible": false + }, + { + "name": "phase", + "displayName": "Phase", + "shape": [ + "state", + { + "map": { + "success": [ + "Bound" + ], + "warning": [ + "Pending" + ], + "error": [ + "Lost" + ] + } + } + ] + }, + { + "name": "requestedStorageBytes", + "displayName": "Requested Storage", + "shape": "bytes" + }, + { + "name": "volumeName", + "displayName": "Volume Name", + "shape": "string", + "visible": false + }, + { + "name": "cpuRequestHardMillicores", + "displayName": "CPU Requests Limit (m)", + "shape": "number" + }, + { + "name": "cpuRequestUsedMillicores", + "displayName": "CPU Requests Used (m)", + "shape": "number" + }, + { + "name": "memoryRequestHardBytes", + "displayName": "Memory Requests Limit", + "shape": "bytes" + }, + { + "name": "memoryRequestUsedBytes", + "displayName": "Memory Requests Used", + "shape": "bytes" + }, + { + "name": "cpuLimitHardMillicores", + "displayName": "CPU Limits Cap (m)", + "shape": "number" + }, + { + "name": "cpuLimitUsedMillicores", + "displayName": "CPU Limits Used (m)", + "shape": "number" + }, + { + "name": "memoryLimitHardBytes", + "displayName": "Memory Limits Cap", + "shape": "bytes" + }, + { + "name": "memoryLimitUsedBytes", + "displayName": "Memory Limits Used", + "shape": "bytes" + }, + { + "name": "podsHard", + "displayName": "Pod Limit", + "shape": "number" + }, + { + "name": "podsUsed", + "displayName": "Pods Used", + "shape": "number" + }, + { + "name": "limitType", + "displayName": "Limit Type", + "shape": "string" + }, + { + "name": "defaultCpuMillicores", + "displayName": "Default CPU Limit (m)", + "shape": "number" + }, + { + "name": "defaultMemoryBytes", + "displayName": "Default Memory Limit", + "shape": "bytes" + }, + { + "name": "defaultRequestCpuMillicores", + "displayName": "Default CPU Request (m)", + "shape": "number" + }, + { + "name": "defaultRequestMemoryBytes", + "displayName": "Default Memory Request", + "shape": "bytes" + }, + { + "name": "maxCpuMillicores", + "displayName": "Max CPU (m)", + "shape": "number" + }, + { + "name": "maxMemoryBytes", + "displayName": "Max Memory", + "shape": "bytes" + }, + { + "name": "minCpuMillicores", + "displayName": "Min CPU (m)", + "shape": "number" + }, + { + "name": "minMemoryBytes", + "displayName": "Min Memory", + "shape": "bytes" + }, + { + "name": "nodeName", + "displayName": "Node", + "shape": "string" + }, + { + "name": "podPhase", + "displayName": "Pod Phase", + "shape": "string" + }, + { + "name": "containerHealth", + "displayName": "Health", + "shape": [ + "state", + { + "map": { + "success": [ + "healthy" + ], + "warning": [ + "pending" + ], + "error": [ + "crashloop" + ] + } + } + ] + } + ], + "timeframes": false +} diff --git a/plugins/Kubernetes/v1/dataStreams/namespaces.json b/plugins/Kubernetes/v1/dataStreams/namespaces.json new file mode 100644 index 00000000..2a3144d3 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/namespaces.json @@ -0,0 +1,51 @@ +{ + "name": "namespaces", + "displayName": "Namespaces", + "description": "Kubernetes namespaces, one row per namespace", + "tags": [ + "Resources" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/api/v1/namespaces", + "postRequestScript": "namespaces.js", + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "250" + }, + "in": { + "realm": "payload", + "path": "metadata.continue" + }, + "out": { + "realm": "queryArg", + "path": "continue" + } + } + }, + "matches": "none", + "metadata": [ + { + "name": "uid", + "displayName": "UID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "phase", + "displayName": "Phase", + "shape": "string" + } + ], + "timeframes": false +} diff --git a/plugins/Kubernetes/v1/dataStreams/nodeMetrics.json b/plugins/Kubernetes/v1/dataStreams/nodeMetrics.json new file mode 100644 index 00000000..55694f8a --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/nodeMetrics.json @@ -0,0 +1,47 @@ +{ + "name": "nodeMetrics", + "displayName": "Node Metrics", + "description": "Live CPU and memory usage per cluster node, one row per node", + "tags": [ + "Metrics" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/apis/metrics.k8s.io/v1beta1/nodes", + "postRequestScript": "nodeMetrics.js" + }, + "matches": "none", + "ui": [ + { + "type": "objects", + "name": "node", + "label": "Filter by Node", + "matches": { + "sourceType": { + "type": "equals", + "value": "Node" + } + } + } + ], + "metadata": [ + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "cpuUsageMillicores", + "displayName": "CPU Usage (m)", + "shape": "number" + }, + { + "name": "memoryUsageBytes", + "displayName": "Memory Usage", + "shape": "bytes" + } + ], + "timeframes": false +} diff --git a/plugins/Kubernetes/v1/dataStreams/nodes.json b/plugins/Kubernetes/v1/dataStreams/nodes.json new file mode 100644 index 00000000..35a7e30c --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/nodes.json @@ -0,0 +1,191 @@ +{ + "name": "nodes", + "displayName": "Nodes", + "description": "Kubernetes cluster nodes, one row per node", + "tags": [ + "Resources" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/api/v1/nodes", + "postRequestScript": "nodes.js", + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "250" + }, + "in": { + "realm": "payload", + "path": "metadata.continue" + }, + "out": { + "realm": "queryArg", + "path": "continue" + } + } + }, + "matches": "none", + "metadata": [ + { + "name": "uid", + "displayName": "UID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "kubeletVersion", + "displayName": "Kubelet Version", + "shape": "string" + }, + { + "name": "osImage", + "displayName": "OS Image", + "shape": "string" + }, + { + "name": "unschedulable", + "displayName": "Unschedulable", + "shape": "boolean" + }, + { + "name": "readyStatus", + "displayName": "Ready", + "shape": [ + "state", + { + "map": { + "success": [ + "True" + ], + "error": [ + "False" + ], + "unknown": [ + "Unknown" + ] + } + } + ] + }, + { + "name": "memoryPressureStatus", + "displayName": "Memory Pressure", + "shape": [ + "state", + { + "map": { + "success": [ + "False" + ], + "error": [ + "True" + ], + "unknown": [ + "Unknown" + ] + } + } + ] + }, + { + "name": "diskPressureStatus", + "displayName": "Disk Pressure", + "shape": [ + "state", + { + "map": { + "success": [ + "False" + ], + "error": [ + "True" + ], + "unknown": [ + "Unknown" + ] + } + } + ] + }, + { + "name": "pidPressureStatus", + "displayName": "PID Pressure", + "shape": [ + "state", + { + "map": { + "success": [ + "False" + ], + "error": [ + "True" + ], + "unknown": [ + "Unknown" + ] + } + } + ] + }, + { + "name": "networkUnavailableStatus", + "displayName": "Network Unavailable", + "shape": [ + "state", + { + "map": { + "success": [ + "False" + ], + "error": [ + "True" + ], + "unknown": [ + "Unknown" + ] + } + } + ] + }, + { + "name": "allocatableCpuMillicores", + "displayName": "Allocatable CPU (m)", + "shape": "number" + }, + { + "name": "allocatableMemoryBytes", + "displayName": "Allocatable Memory", + "shape": "bytes" + }, + { + "name": "allocatableEphemeralStorageBytes", + "displayName": "Allocatable Ephemeral Storage", + "shape": "bytes" + }, + { + "name": "capacityCpuMillicores", + "displayName": "Capacity CPU (m)", + "shape": "number" + }, + { + "name": "capacityMemoryBytes", + "displayName": "Capacity Memory", + "shape": "bytes" + }, + { + "name": "capacityEphemeralStorageBytes", + "displayName": "Capacity Ephemeral Storage", + "shape": "bytes" + } + ], + "timeframes": false +} diff --git a/plugins/Kubernetes/v1/dataStreams/persistentvolumes.json b/plugins/Kubernetes/v1/dataStreams/persistentvolumes.json new file mode 100644 index 00000000..d4b6c85e --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/persistentvolumes.json @@ -0,0 +1,84 @@ +{ + "name": "persistentvolumes", + "displayName": "Persistent Volumes", + "description": "Kubernetes persistent volumes, one row per volume", + "tags": [ + "Resources" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/api/v1/persistentvolumes", + "postRequestScript": "persistentvolumes.js", + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "250" + }, + "in": { + "realm": "payload", + "path": "metadata.continue" + }, + "out": { + "realm": "queryArg", + "path": "continue" + } + } + }, + "matches": "none", + "metadata": [ + { + "name": "uid", + "displayName": "UID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "phase", + "displayName": "Phase", + "shape": [ + "state", + { + "map": { + "success": [ + "Bound", + "Available" + ], + "warning": [ + "Released" + ], + "error": [ + "Failed" + ] + } + } + ] + }, + { + "name": "capacityBytes", + "displayName": "Capacity", + "shape": "bytes" + }, + { + "name": "claimName", + "displayName": "Claim Name", + "shape": "string", + "visible": false + }, + { + "name": "claimNamespace", + "displayName": "Claim Namespace", + "shape": "string", + "visible": false + } + ], + "timeframes": false +} diff --git a/plugins/Kubernetes/v1/dataStreams/podMetrics.json b/plugins/Kubernetes/v1/dataStreams/podMetrics.json new file mode 100644 index 00000000..b428107e --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/podMetrics.json @@ -0,0 +1,52 @@ +{ + "name": "podMetrics", + "displayName": "Pod Metrics", + "description": "Live CPU and memory usage per pod, one row per pod", + "tags": [ + "Metrics" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/apis/metrics.k8s.io/v1beta1/pods", + "postRequestScript": "podMetrics.js" + }, + "matches": "none", + "ui": [ + { + "type": "objects", + "name": "pod", + "label": "Pod (optional)", + "matches": { + "sourceType": { + "type": "equals", + "value": "Pod" + } + } + } + ], + "metadata": [ + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "namespace", + "displayName": "Namespace", + "shape": "string" + }, + { + "name": "cpuUsageMillicores", + "displayName": "CPU Usage (m)", + "shape": "number" + }, + { + "name": "memoryUsageBytes", + "displayName": "Memory Usage", + "shape": "bytes" + } + ], + "timeframes": false +} diff --git a/plugins/Kubernetes/v1/dataStreams/pods.json b/plugins/Kubernetes/v1/dataStreams/pods.json new file mode 100644 index 00000000..c0977a6f --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/pods.json @@ -0,0 +1,118 @@ +{ + "name": "pods", + "displayName": "Pods", + "description": "Kubernetes pods, one row per pod", + "tags": [ + "Resources" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/api/v1/pods", + "postRequestScript": "pods.js", + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "250" + }, + "in": { + "realm": "payload", + "path": "metadata.continue" + }, + "out": { + "realm": "queryArg", + "path": "continue" + } + } + }, + "matches": "none", + "metadata": [ + { + "name": "uid", + "displayName": "UID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "namespace", + "displayName": "Namespace", + "shape": "string" + }, + { + "name": "nodeName", + "displayName": "Node", + "shape": "string" + }, + { + "name": "ownerUid", + "displayName": "Owner UID", + "shape": "string", + "visible": false + }, + { + "name": "appLabel", + "displayName": "App Label", + "shape": "string", + "visible": false + }, + { + "name": "phase", + "displayName": "Phase", + "shape": "string" + }, + { + "name": "containerHealth", + "displayName": "Health", + "shape": [ + "state", + { + "map": { + "success": [ + "healthy" + ], + "warning": [ + "pending" + ], + "error": [ + "crashloop" + ] + } + } + ] + }, + { + "name": "restartCount", + "displayName": "Restarts", + "shape": "number" + }, + { + "name": "cpuRequestMillicores", + "displayName": "CPU Request (m)", + "shape": "number" + }, + { + "name": "memoryRequestBytes", + "displayName": "Memory Request", + "shape": "bytes" + }, + { + "name": "cpuLimitMillicores", + "displayName": "CPU Limit (m)", + "shape": "number" + }, + { + "name": "memoryLimitBytes", + "displayName": "Memory Limit", + "shape": "bytes" + } + ], + "timeframes": false +} diff --git a/plugins/Kubernetes/v1/dataStreams/podsByParent.json b/plugins/Kubernetes/v1/dataStreams/podsByParent.json new file mode 100644 index 00000000..917a9be3 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/podsByParent.json @@ -0,0 +1,99 @@ +{ + "name": "podsByParent", + "displayName": "Pods by Parent", + "description": "Pods belonging to a node, daemonset, statefulset, or service, one row per pod", + "tags": [ + "Resources" + ], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "{{ object.namespace ? ('/api/v1/namespaces/' + object.namespace + '/pods') : '/api/v1/pods' }}", + "getArgs": [ + "{{ !object.namespace && {key: 'fieldSelector', value: 'spec.nodeName=' + object.name} }}" + ], + "postRequestScript": "podsByParent.js", + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "100" + }, + "in": { + "realm": "payload", + "path": "metadata.continue" + }, + "out": { + "realm": "queryArg", + "path": "continue" + } + } + }, + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Node", + "DaemonSet", + "StatefulSet", + "Service" + ] + } + }, + "metadata": [ + { + "name": "uid", + "displayName": "UID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "namespace", + "displayName": "Namespace", + "shape": "string" + }, + { + "name": "nodeName", + "displayName": "Node", + "shape": "string" + }, + { + "name": "ownerName", + "displayName": "Owner", + "shape": "string" + }, + { + "name": "phase", + "displayName": "Phase", + "shape": "string" + }, + { + "name": "containerHealth", + "displayName": "Health", + "shape": [ + "state", + { + "map": { + "success": [ + "healthy" + ], + "warning": [ + "pending" + ], + "error": [ + "crashloop" + ] + } + } + ] + } + ], + "timeframes": false +} diff --git a/plugins/Kubernetes/v1/dataStreams/pvClaims.json b/plugins/Kubernetes/v1/dataStreams/pvClaims.json new file mode 100644 index 00000000..738bfd01 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/pvClaims.json @@ -0,0 +1,55 @@ +{ + "name": "pvClaims", + "displayName": "Persistent Volume Claims", + "description": "The claim bound to a persistent volume, one row when claimed", + "tags": [ + "Resources" + ], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "/api/v1/namespaces/{{object.claimNamespace}}/persistentvolumeclaims/{{object.claimName}}", + "postRequestScript": "pvClaims.js", + "errorHandling": { + "type": "path", + "realm": "payload", + "path": "message" + } + }, + "matches": { + "sourceType": { + "type": "equals", + "value": "Persistent Volume" + } + }, + "metadata": [ + { + "name": "uid", + "displayName": "UID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "pvName", + "displayName": "Persistent Volume", + "shape": "string" + }, + { + "name": "phase", + "displayName": "Phase", + "shape": "string" + }, + { + "name": "requestedStorageBytes", + "displayName": "Requested Storage", + "shape": "bytes" + } + ], + "timeframes": false +} diff --git a/plugins/Kubernetes/v1/dataStreams/replicasets.json b/plugins/Kubernetes/v1/dataStreams/replicasets.json new file mode 100644 index 00000000..e89bd866 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/replicasets.json @@ -0,0 +1,80 @@ +{ + "name": "replicasets", + "displayName": "ReplicaSets", + "description": "Kubernetes replica sets, one row per replica set", + "tags": [ + "Resources" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/apis/apps/v1/replicasets", + "postRequestScript": "replicasets.js", + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "250" + }, + "in": { + "realm": "payload", + "path": "metadata.continue" + }, + "out": { + "realm": "queryArg", + "path": "continue" + } + } + }, + "matches": "none", + "ui": [ + { + "type": "objects", + "name": "deployment", + "label": "Filter by Deployment", + "matches": { + "sourceType": { + "type": "equals", + "value": "Deployment" + } + } + } + ], + "metadata": [ + { + "name": "uid", + "displayName": "UID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "namespace", + "displayName": "Namespace", + "shape": "string" + }, + { + "name": "ownerUid", + "displayName": "Owner UID", + "shape": "string", + "visible": false + }, + { + "name": "desiredReplicas", + "displayName": "Desired Replicas", + "shape": "number" + }, + { + "name": "readyReplicas", + "displayName": "Ready Replicas", + "shape": "number" + } + ], + "timeframes": false +} diff --git a/plugins/Kubernetes/v1/dataStreams/resourcequotas.json b/plugins/Kubernetes/v1/dataStreams/resourcequotas.json new file mode 100644 index 00000000..fcca6354 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/resourcequotas.json @@ -0,0 +1,101 @@ +{ + "name": "resourcequotas", + "displayName": "Resource Quotas", + "description": "Kubernetes resource quotas, one row per quota", + "tags": [ + "Quotas" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/api/v1/resourcequotas", + "postRequestScript": "resourcequotas.js", + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "250" + }, + "in": { + "realm": "payload", + "path": "metadata.continue" + }, + "out": { + "realm": "queryArg", + "path": "continue" + } + } + }, + "matches": "none", + "metadata": [ + { + "name": "uid", + "displayName": "UID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "namespace", + "displayName": "Namespace", + "shape": "string" + }, + { + "name": "cpuRequestHardMillicores", + "displayName": "CPU Requests Limit (m)", + "shape": "number" + }, + { + "name": "cpuRequestUsedMillicores", + "displayName": "CPU Requests Used (m)", + "shape": "number" + }, + { + "name": "memoryRequestHardBytes", + "displayName": "Memory Requests Limit", + "shape": "bytes" + }, + { + "name": "memoryRequestUsedBytes", + "displayName": "Memory Requests Used", + "shape": "bytes" + }, + { + "name": "cpuLimitHardMillicores", + "displayName": "CPU Limits Cap (m)", + "shape": "number" + }, + { + "name": "cpuLimitUsedMillicores", + "displayName": "CPU Limits Used (m)", + "shape": "number" + }, + { + "name": "memoryLimitHardBytes", + "displayName": "Memory Limits Cap", + "shape": "bytes" + }, + { + "name": "memoryLimitUsedBytes", + "displayName": "Memory Limits Used", + "shape": "bytes" + }, + { + "name": "podsHard", + "displayName": "Pod Limit", + "shape": "number" + }, + { + "name": "podsUsed", + "displayName": "Pods Used", + "shape": "number" + } + ], + "timeframes": false +} diff --git a/plugins/Kubernetes/v1/dataStreams/scripts/daemonsets.js b/plugins/Kubernetes/v1/dataStreams/scripts/daemonsets.js new file mode 100644 index 00000000..d6a03ad7 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/scripts/daemonsets.js @@ -0,0 +1,24 @@ +result = (data.items || []).map((item) => { + const desiredNumberScheduled = + (item.status && item.status.desiredNumberScheduled) || 0; + const numberReady = (item.status && item.status.numberReady) || 0; + + // No eligible nodes means zero scheduled pods is the intended state, not a failure. + const daemonSetHealth = + desiredNumberScheduled === 0 + ? "success" + : numberReady === desiredNumberScheduled + ? "success" + : numberReady < desiredNumberScheduled + ? "error" + : "unknown"; + + return { + uid: item.metadata.uid, + name: item.metadata.name, + namespace: item.metadata.namespace, + desiredNumberScheduled, + numberReady, + daemonSetHealth, + }; +}); diff --git a/plugins/Kubernetes/v1/dataStreams/scripts/deployments.js b/plugins/Kubernetes/v1/dataStreams/scripts/deployments.js new file mode 100644 index 00000000..4b06ec0f --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/scripts/deployments.js @@ -0,0 +1,13 @@ +result = (data.items || []).map((item) => { + const conditions = (item.status && item.status.conditions) || []; + const available = conditions.find((c) => c.type === "Available"); + + return { + uid: item.metadata.uid, + name: item.metadata.name, + namespace: item.metadata.namespace, + desiredReplicas: (item.spec && item.spec.replicas) || 0, + availableReplicas: (item.status && item.status.availableReplicas) || 0, + availableStatus: available ? available.status : "Unknown", + }; +}); diff --git a/plugins/Kubernetes/v1/dataStreams/scripts/events.js b/plugins/Kubernetes/v1/dataStreams/scripts/events.js new file mode 100644 index 00000000..b6c5977c --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/scripts/events.js @@ -0,0 +1,14 @@ +result = (data.items || []).map((item) => { + const involvedObject = item.involvedObject || {}; + + return { + uid: item.metadata.uid, + reason: item.reason, + type: item.type, + message: item.message, + lastTimestamp: item.lastTimestamp, + involvedObjectUid: involvedObject.uid, + involvedObjectKind: involvedObject.kind, + involvedObjectName: involvedObject.name, + }; +}); diff --git a/plugins/Kubernetes/v1/dataStreams/scripts/ingresses.js b/plugins/Kubernetes/v1/dataStreams/scripts/ingresses.js new file mode 100644 index 00000000..34eb6252 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/scripts/ingresses.js @@ -0,0 +1,41 @@ +result = (data.items || []).map((item) => { + const rules = (item.spec && item.spec.rules) || []; + + const hosts = rules + .map((rule) => rule.host) + .filter(Boolean) + .join(","); + + const defaultBackendService = + item.spec && + item.spec.defaultBackend && + item.spec.defaultBackend.service && + item.spec.defaultBackend.service.name; + + const firstRulePath = + rules[0] && + rules[0].http && + rules[0].http.paths && + rules[0].http.paths[0]; + + const firstRuleService = + firstRulePath && + firstRulePath.backend && + firstRulePath.backend.service && + firstRulePath.backend.service.name; + + // Only the default backend or the first rule's first path is exposed — + // later rules/paths routing to other services aren't represented. + const primaryBackendServiceName = + defaultBackendService || firstRuleService || undefined; + + return { + uid: item.metadata.uid, + name: item.metadata.name, + namespace: item.metadata.namespace, + ingressClassName: + (item.spec && item.spec.ingressClassName) || undefined, + hosts: hosts, + primaryBackendServiceName: primaryBackendServiceName, + }; +}); diff --git a/plugins/Kubernetes/v1/dataStreams/scripts/limitranges.js b/plugins/Kubernetes/v1/dataStreams/scripts/limitranges.js new file mode 100644 index 00000000..bdfe659c --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/scripts/limitranges.js @@ -0,0 +1,66 @@ +// Kubernetes Quantity grammar: mantissa is [+-]?(digits[.digits?] | .digits), +// exponent accepts lowercase or uppercase e/E, suffix is a binary (Ki..Ei) or +// decimal (n/u/m/k/M/G/T/P/E) multiplier — never both on the same value. +function parseK8sQuantity(qty) { + if (qty === undefined || qty === null) return undefined; + const match = String(qty).match( + /^([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+))(?:([eE][+-]?[0-9]+)|(Ki|Mi|Gi|Ti|Pi|Ei|n|u|m|k|M|G|T|P|E))?$/, + ); + if (!match) return undefined; + const mantissa = parseFloat(match[1]); + if (Number.isNaN(mantissa)) return undefined; + if (match[2]) return mantissa * Math.pow(10, parseInt(match[2].slice(1), 10)); + const suffix = match[3] || ""; + if (suffix === "") return mantissa; + const multipliers = { + n: 1e-9, + u: 1e-6, + m: 1e-3, + k: 1e3, + M: 1e6, + G: 1e9, + T: 1e12, + P: 1e15, + E: 1e18, + Ki: 1024, + Mi: 1024 ** 2, + Gi: 1024 ** 3, + Ti: 1024 ** 4, + Pi: 1024 ** 5, + Ei: 1024 ** 6, + }; + return mantissa * multipliers[suffix]; +} + +function parseCpuMillicores(qty) { + const cores = parseK8sQuantity(qty); + return cores === undefined ? undefined : cores * 1000; +} + +function parseMemoryBytes(qty) { + return parseK8sQuantity(qty); +} + +result = (data.items || []).map((item) => { + const limits = (item.spec && item.spec.limits) || []; + const limit = limits.find((l) => l.type === "Container") || limits[0] || {}; + const def = limit.default || {}; + const defaultRequest = limit.defaultRequest || {}; + const max = limit.max || {}; + const min = limit.min || {}; + + return { + uid: item.metadata.uid, + name: item.metadata.name, + namespace: item.metadata.namespace, + limitType: limit.type, + defaultCpuMillicores: parseCpuMillicores(def.cpu), + defaultMemoryBytes: parseMemoryBytes(def.memory), + defaultRequestCpuMillicores: parseCpuMillicores(defaultRequest.cpu), + defaultRequestMemoryBytes: parseMemoryBytes(defaultRequest.memory), + maxCpuMillicores: parseCpuMillicores(max.cpu), + maxMemoryBytes: parseMemoryBytes(max.memory), + minCpuMillicores: parseCpuMillicores(min.cpu), + minMemoryBytes: parseMemoryBytes(min.memory), + }; +}); diff --git a/plugins/Kubernetes/v1/dataStreams/scripts/namespaceContents.js b/plugins/Kubernetes/v1/dataStreams/scripts/namespaceContents.js new file mode 100644 index 00000000..0b4d6429 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/scripts/namespaceContents.js @@ -0,0 +1,247 @@ +// Kubernetes Quantity grammar: mantissa is [+-]?(digits[.digits?] | .digits), +// exponent accepts lowercase or uppercase e/E, suffix is a binary (Ki..Ei) or +// decimal (n/u/m/k/M/G/T/P/E) multiplier — never both on the same value. +function parseK8sQuantity(qty) { + if (qty === undefined || qty === null) return undefined; + const match = String(qty).match( + /^([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+))(?:([eE][+-]?[0-9]+)|(Ki|Mi|Gi|Ti|Pi|Ei|n|u|m|k|M|G|T|P|E))?$/, + ); + if (!match) return undefined; + const mantissa = parseFloat(match[1]); + if (Number.isNaN(mantissa)) return undefined; + if (match[2]) return mantissa * Math.pow(10, parseInt(match[2].slice(1), 10)); + const suffix = match[3] || ""; + if (suffix === "") return mantissa; + const multipliers = { + n: 1e-9, + u: 1e-6, + m: 1e-3, + k: 1e3, + M: 1e6, + G: 1e9, + T: 1e12, + P: 1e15, + E: 1e18, + Ki: 1024, + Mi: 1024 ** 2, + Gi: 1024 ** 3, + Ti: 1024 ** 4, + Pi: 1024 ** 5, + Ei: 1024 ** 6, + }; + return mantissa * multipliers[suffix]; +} + +function parseCpuMillicores(qty) { + const cores = parseK8sQuantity(qty); + return cores === undefined ? undefined : cores * 1000; +} + +function parseMemoryBytes(qty) { + return parseK8sQuantity(qty); +} + +function parseCount(value) { + return value !== undefined ? parseInt(value, 10) : undefined; +} + +function computeContainerHealth(pod) { + const phase = pod.status && pod.status.phase; + const statuses = (pod.status && pod.status.containerStatuses) || []; + + for (const cs of statuses) { + const waitingReason = + cs.state && cs.state.waiting && cs.state.waiting.reason; + if (waitingReason === "CrashLoopBackOff") return "crashloop"; + if (waitingReason) return "waiting:" + waitingReason; + + const terminatedReason = + cs.state && cs.state.terminated && cs.state.terminated.reason; + if (terminatedReason === "OOMKilled") return "crashloop"; + } + + if (phase === "Running") return "healthy"; + if (phase === "Pending") return "pending"; + return phase ? phase.toLowerCase() : "unknown"; +} + +const detailType = "{{detailType}}"; + +result = (data.items || []).map((item) => { + const base = { + uid: item.metadata.uid, + name: item.metadata.name, + namespace: item.metadata.namespace, + }; + + if (detailType === "deployments") { + const conditions = (item.status && item.status.conditions) || []; + const available = conditions.find((c) => c.type === "Available"); + return { + ...base, + desiredReplicas: (item.spec && item.spec.replicas) || 0, + availableReplicas: + (item.status && item.status.availableReplicas) || 0, + availableStatus: available ? available.status : "Unknown", + }; + } + + if (detailType === "daemonsets") { + const desiredNumberScheduled = + (item.status && item.status.desiredNumberScheduled) || 0; + const numberReady = (item.status && item.status.numberReady) || 0; + // No eligible nodes means zero scheduled pods is the intended state, not a failure. + const daemonSetHealth = + desiredNumberScheduled === 0 + ? "success" + : numberReady === desiredNumberScheduled + ? "success" + : numberReady < desiredNumberScheduled + ? "error" + : "unknown"; + return { + ...base, + desiredNumberScheduled, + numberReady, + daemonSetHealth, + }; + } + + if (detailType === "statefulsets") { + const desiredReplicas = (item.spec && item.spec.replicas) || 0; + const readyReplicas = (item.status && item.status.readyReplicas) || 0; + const statefulSetHealth = + readyReplicas === desiredReplicas + ? "success" + : readyReplicas < desiredReplicas + ? "error" + : "unknown"; + return { ...base, desiredReplicas, readyReplicas, statefulSetHealth }; + } + + if (detailType === "replicasets") { + const owner = ((item.metadata && item.metadata.ownerReferences) || []).find( + (ref) => ref.controller === true, + ); + return { + ...base, + ownerUid: owner ? owner.uid : undefined, + desiredReplicas: (item.spec && item.spec.replicas) || 0, + readyReplicas: (item.status && item.status.readyReplicas) || 0, + }; + } + + if (detailType === "services") { + const selector = (item.spec && item.spec.selector) || {}; + return { + ...base, + serviceType: item.spec && item.spec.type, + selectorAppLabel: + selector["app"] || + selector["app.kubernetes.io/name"] || + selector["k8s-app"] || + undefined, + }; + } + + if (detailType === "ingresses") { + const rules = (item.spec && item.spec.rules) || []; + const hosts = rules + .map((rule) => rule.host) + .filter(Boolean) + .join(","); + const defaultBackendService = + item.spec && + item.spec.defaultBackend && + item.spec.defaultBackend.service && + item.spec.defaultBackend.service.name; + const firstRulePath = + rules[0] && + rules[0].http && + rules[0].http.paths && + rules[0].http.paths[0]; + const firstRuleService = + firstRulePath && + firstRulePath.backend && + firstRulePath.backend.service && + firstRulePath.backend.service.name; + // Only the default backend or the first rule's first path is exposed — + // later rules/paths routing to other services aren't represented. + return { + ...base, + ingressClassName: + (item.spec && item.spec.ingressClassName) || undefined, + hosts, + primaryBackendServiceName: + defaultBackendService || firstRuleService || undefined, + }; + } + + if (detailType === "persistentvolumeclaims") { + const requests = + (item.spec && + item.spec.resources && + item.spec.resources.requests) || + {}; + return { + ...base, + phase: (item.status && item.status.phase) || "Unknown", + volumeName: (item.spec && item.spec.volumeName) || undefined, + requestedStorageBytes: parseMemoryBytes(requests.storage), + }; + } + + if (detailType === "resourcequotas") { + const hard = + (item.status && item.status.hard) || + (item.spec && item.spec.hard) || + {}; + const used = (item.status && item.status.used) || {}; + return { + ...base, + cpuRequestHardMillicores: parseCpuMillicores(hard["requests.cpu"]), + cpuRequestUsedMillicores: parseCpuMillicores(used["requests.cpu"]), + memoryRequestHardBytes: parseMemoryBytes(hard["requests.memory"]), + memoryRequestUsedBytes: parseMemoryBytes(used["requests.memory"]), + cpuLimitHardMillicores: parseCpuMillicores(hard["limits.cpu"]), + cpuLimitUsedMillicores: parseCpuMillicores(used["limits.cpu"]), + memoryLimitHardBytes: parseMemoryBytes(hard["limits.memory"]), + memoryLimitUsedBytes: parseMemoryBytes(used["limits.memory"]), + podsHard: parseCount(hard.pods), + podsUsed: parseCount(used.pods), + }; + } + + if (detailType === "limitranges") { + const limits = (item.spec && item.spec.limits) || []; + const limit = + limits.find((l) => l.type === "Container") || limits[0] || {}; + const def = limit.default || {}; + const defaultRequest = limit.defaultRequest || {}; + const max = limit.max || {}; + const min = limit.min || {}; + return { + ...base, + limitType: limit.type, + defaultCpuMillicores: parseCpuMillicores(def.cpu), + defaultMemoryBytes: parseMemoryBytes(def.memory), + defaultRequestCpuMillicores: parseCpuMillicores(defaultRequest.cpu), + defaultRequestMemoryBytes: parseMemoryBytes(defaultRequest.memory), + maxCpuMillicores: parseCpuMillicores(max.cpu), + maxMemoryBytes: parseMemoryBytes(max.memory), + minCpuMillicores: parseCpuMillicores(min.cpu), + minMemoryBytes: parseMemoryBytes(min.memory), + }; + } + + if (detailType === "pods") { + return { + ...base, + nodeName: item.spec && item.spec.nodeName, + podPhase: item.status && item.status.phase, + containerHealth: computeContainerHealth(item), + }; + } + + return base; +}); diff --git a/plugins/Kubernetes/v1/dataStreams/scripts/namespaces.js b/plugins/Kubernetes/v1/dataStreams/scripts/namespaces.js new file mode 100644 index 00000000..8c040bdf --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/scripts/namespaces.js @@ -0,0 +1,5 @@ +result = (data.items || []).map((item) => ({ + uid: item.metadata.uid, + name: item.metadata.name, + phase: item.status && item.status.phase, +})); diff --git a/plugins/Kubernetes/v1/dataStreams/scripts/nodeMetrics.js b/plugins/Kubernetes/v1/dataStreams/scripts/nodeMetrics.js new file mode 100644 index 00000000..9d4603f6 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/scripts/nodeMetrics.js @@ -0,0 +1,58 @@ +// Kubernetes Quantity grammar: mantissa is [+-]?(digits[.digits?] | .digits), +// exponent accepts lowercase or uppercase e/E, suffix is a binary (Ki..Ei) or +// decimal (n/u/m/k/M/G/T/P/E) multiplier — never both on the same value. +function parseK8sQuantity(qty) { + if (qty === undefined || qty === null) return undefined; + const match = String(qty).match( + /^([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+))(?:([eE][+-]?[0-9]+)|(Ki|Mi|Gi|Ti|Pi|Ei|n|u|m|k|M|G|T|P|E))?$/, + ); + if (!match) return undefined; + const mantissa = parseFloat(match[1]); + if (Number.isNaN(mantissa)) return undefined; + if (match[2]) return mantissa * Math.pow(10, parseInt(match[2].slice(1), 10)); + const suffix = match[3] || ""; + if (suffix === "") return mantissa; + const multipliers = { + n: 1e-9, + u: 1e-6, + m: 1e-3, + k: 1e3, + M: 1e6, + G: 1e9, + T: 1e12, + P: 1e15, + E: 1e18, + Ki: 1024, + Mi: 1024 ** 2, + Gi: 1024 ** 3, + Ti: 1024 ** 4, + Pi: 1024 ** 5, + Ei: 1024 ** 6, + }; + return mantissa * multipliers[suffix]; +} + +function parseCpuMillicores(qty) { + const cores = parseK8sQuantity(qty); + return cores === undefined ? undefined : cores * 1000; +} + +function parseMemoryBytes(qty) { + return parseK8sQuantity(qty); +} + +const rows = (data.items || []).map((item) => { + const usage = item.usage || {}; + return { + name: item.metadata && item.metadata.name, + cpuUsageMillicores: parseCpuMillicores(usage.cpu), + memoryUsageBytes: parseMemoryBytes(usage.memory), + }; +}); + +// Object-picker fields can be arrays. Normalize them before key comparison. +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +const selected = (context.config && context.config.node) || []; +const nodeNames = new Set(selected.map((o) => unwrap(o.name)).filter(Boolean)); + +result = nodeNames.size ? rows.filter((r) => nodeNames.has(r.name)) : rows; diff --git a/plugins/Kubernetes/v1/dataStreams/scripts/nodes.js b/plugins/Kubernetes/v1/dataStreams/scripts/nodes.js new file mode 100644 index 00000000..917189fd --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/scripts/nodes.js @@ -0,0 +1,80 @@ +// Kubernetes Quantity grammar: mantissa is [+-]?(digits[.digits?] | .digits), +// exponent accepts lowercase or uppercase e/E, suffix is a binary (Ki..Ei) or +// decimal (n/u/m/k/M/G/T/P/E) multiplier — never both on the same value. +function parseK8sQuantity(qty) { + if (qty === undefined || qty === null) return undefined; + const match = String(qty).match( + /^([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+))(?:([eE][+-]?[0-9]+)|(Ki|Mi|Gi|Ti|Pi|Ei|n|u|m|k|M|G|T|P|E))?$/, + ); + if (!match) return undefined; + const mantissa = parseFloat(match[1]); + if (Number.isNaN(mantissa)) return undefined; + if (match[2]) return mantissa * Math.pow(10, parseInt(match[2].slice(1), 10)); + const suffix = match[3] || ""; + if (suffix === "") return mantissa; + const multipliers = { + n: 1e-9, + u: 1e-6, + m: 1e-3, + k: 1e3, + M: 1e6, + G: 1e9, + T: 1e12, + P: 1e15, + E: 1e18, + Ki: 1024, + Mi: 1024 ** 2, + Gi: 1024 ** 3, + Ti: 1024 ** 4, + Pi: 1024 ** 5, + Ei: 1024 ** 6, + }; + return mantissa * multipliers[suffix]; +} + +function parseCpuMillicores(qty) { + const cores = parseK8sQuantity(qty); + return cores === undefined ? undefined : cores * 1000; +} + +function parseMemoryBytes(qty) { + return parseK8sQuantity(qty); +} + +function conditionStatus(conditions, type) { + const condition = conditions.find((c) => c.type === type); + return condition ? condition.status : "Unknown"; +} + +result = (data.items || []).map((item) => { + const conditions = (item.status && item.status.conditions) || []; + const nodeInfo = (item.status && item.status.nodeInfo) || {}; + const allocatable = (item.status && item.status.allocatable) || {}; + const capacity = (item.status && item.status.capacity) || {}; + + return { + uid: item.metadata.uid, + name: item.metadata.name, + kubeletVersion: nodeInfo.kubeletVersion, + osImage: nodeInfo.osImage, + unschedulable: !!(item.spec && item.spec.unschedulable), + readyStatus: conditionStatus(conditions, "Ready"), + memoryPressureStatus: conditionStatus(conditions, "MemoryPressure"), + diskPressureStatus: conditionStatus(conditions, "DiskPressure"), + pidPressureStatus: conditionStatus(conditions, "PIDPressure"), + networkUnavailableStatus: conditionStatus( + conditions, + "NetworkUnavailable", + ), + allocatableCpuMillicores: parseCpuMillicores(allocatable.cpu), + allocatableMemoryBytes: parseMemoryBytes(allocatable.memory), + allocatableEphemeralStorageBytes: parseMemoryBytes( + allocatable["ephemeral-storage"], + ), + capacityCpuMillicores: parseCpuMillicores(capacity.cpu), + capacityMemoryBytes: parseMemoryBytes(capacity.memory), + capacityEphemeralStorageBytes: parseMemoryBytes( + capacity["ephemeral-storage"], + ), + }; +}); diff --git a/plugins/Kubernetes/v1/dataStreams/scripts/persistentvolumes.js b/plugins/Kubernetes/v1/dataStreams/scripts/persistentvolumes.js new file mode 100644 index 00000000..1c631931 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/scripts/persistentvolumes.js @@ -0,0 +1,47 @@ +// Kubernetes Quantity grammar: mantissa is [+-]?(digits[.digits?] | .digits), +// exponent accepts lowercase or uppercase e/E, suffix is a binary (Ki..Ei) or +// decimal (n/u/m/k/M/G/T/P/E) multiplier — never both on the same value. +function parseMemoryBytes(qty) { + if (qty === undefined || qty === null) return undefined; + const match = String(qty).match( + /^([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+))(?:([eE][+-]?[0-9]+)|(Ki|Mi|Gi|Ti|Pi|Ei|n|u|m|k|M|G|T|P|E))?$/, + ); + if (!match) return undefined; + const mantissa = parseFloat(match[1]); + if (Number.isNaN(mantissa)) return undefined; + if (match[2]) return mantissa * Math.pow(10, parseInt(match[2].slice(1), 10)); + const suffix = match[3] || ""; + if (suffix === "") return mantissa; + const multipliers = { + n: 1e-9, + u: 1e-6, + m: 1e-3, + k: 1e3, + M: 1e6, + G: 1e9, + T: 1e12, + P: 1e15, + E: 1e18, + Ki: 1024, + Mi: 1024 ** 2, + Gi: 1024 ** 3, + Ti: 1024 ** 4, + Pi: 1024 ** 5, + Ei: 1024 ** 6, + }; + return mantissa * multipliers[suffix]; +} + +result = (data.items || []).map((item) => { + const claimRef = (item.spec && item.spec.claimRef) || {}; + const capacity = (item.spec && item.spec.capacity) || {}; + + return { + uid: item.metadata.uid, + name: item.metadata.name, + phase: (item.status && item.status.phase) || "Unknown", + claimName: claimRef.name || undefined, + claimNamespace: claimRef.namespace || undefined, + capacityBytes: parseMemoryBytes(capacity.storage), + }; +}); diff --git a/plugins/Kubernetes/v1/dataStreams/scripts/podMetrics.js b/plugins/Kubernetes/v1/dataStreams/scripts/podMetrics.js new file mode 100644 index 00000000..6233a26e --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/scripts/podMetrics.js @@ -0,0 +1,87 @@ +// Kubernetes Quantity grammar: mantissa is [+-]?(digits[.digits?] | .digits), +// exponent accepts lowercase or uppercase e/E, suffix is a binary (Ki..Ei) or +// decimal (n/u/m/k/M/G/T/P/E) multiplier — never both on the same value. +function parseK8sQuantity(qty) { + if (qty === undefined || qty === null) return undefined; + const match = String(qty).match( + /^([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+))(?:([eE][+-]?[0-9]+)|(Ki|Mi|Gi|Ti|Pi|Ei|n|u|m|k|M|G|T|P|E))?$/, + ); + if (!match) return undefined; + const mantissa = parseFloat(match[1]); + if (Number.isNaN(mantissa)) return undefined; + if (match[2]) return mantissa * Math.pow(10, parseInt(match[2].slice(1), 10)); + const suffix = match[3] || ""; + if (suffix === "") return mantissa; + const multipliers = { + n: 1e-9, + u: 1e-6, + m: 1e-3, + k: 1e3, + M: 1e6, + G: 1e9, + T: 1e12, + P: 1e15, + E: 1e18, + Ki: 1024, + Mi: 1024 ** 2, + Gi: 1024 ** 3, + Ti: 1024 ** 4, + Pi: 1024 ** 5, + Ei: 1024 ** 6, + }; + return mantissa * multipliers[suffix]; +} + +function parseCpuMillicores(qty) { + const cores = parseK8sQuantity(qty); + return cores === undefined ? undefined : cores * 1000; +} + +function parseMemoryBytes(qty) { + return parseK8sQuantity(qty); +} + +function sumCpuMillicores(containers) { + let total; + for (const c of containers || []) { + const parsed = parseCpuMillicores(c.usage && c.usage.cpu); + if (parsed !== undefined) total = (total || 0) + parsed; + } + return total; +} + +function sumMemoryBytes(containers) { + let total; + for (const c of containers || []) { + const parsed = parseMemoryBytes(c.usage && c.usage.memory); + if (parsed !== undefined) total = (total || 0) + parsed; + } + return total; +} + +const rows = (data.items || []).map((item) => { + const containers = item.containers || []; + return { + name: item.metadata && item.metadata.name, + namespace: item.metadata && item.metadata.namespace, + cpuUsageMillicores: sumCpuMillicores(containers), + memoryUsageBytes: sumMemoryBytes(containers), + }; +}); + +// Object-picker fields can be arrays. Normalize them before key comparison. +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +const selected = (context.config && context.config.pod) || []; +const podKeys = new Set( + selected + .map((o) => { + const name = unwrap(o.name); + const namespace = unwrap(o.namespace); + return name ? `${namespace}/${name}` : undefined; + }) + .filter(Boolean), +); + +result = podKeys.size + ? rows.filter((r) => podKeys.has(`${r.namespace}/${r.name}`)) + : rows; diff --git a/plugins/Kubernetes/v1/dataStreams/scripts/pods.js b/plugins/Kubernetes/v1/dataStreams/scripts/pods.js new file mode 100644 index 00000000..cc61ff69 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/scripts/pods.js @@ -0,0 +1,129 @@ +// Kubernetes Quantity grammar: mantissa is [+-]?(digits[.digits?] | .digits), +// exponent accepts lowercase or uppercase e/E, suffix is a binary (Ki..Ei) or +// decimal (n/u/m/k/M/G/T/P/E) multiplier — never both on the same value. +function parseK8sQuantity(qty) { + if (qty === undefined || qty === null) return undefined; + const match = String(qty).match( + /^([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+))(?:([eE][+-]?[0-9]+)|(Ki|Mi|Gi|Ti|Pi|Ei|n|u|m|k|M|G|T|P|E))?$/, + ); + if (!match) return undefined; + const mantissa = parseFloat(match[1]); + if (Number.isNaN(mantissa)) return undefined; + if (match[2]) return mantissa * Math.pow(10, parseInt(match[2].slice(1), 10)); + const suffix = match[3] || ""; + if (suffix === "") return mantissa; + const multipliers = { + n: 1e-9, + u: 1e-6, + m: 1e-3, + k: 1e3, + M: 1e6, + G: 1e9, + T: 1e12, + P: 1e15, + E: 1e18, + Ki: 1024, + Mi: 1024 ** 2, + Gi: 1024 ** 3, + Ti: 1024 ** 4, + Pi: 1024 ** 5, + Ei: 1024 ** 6, + }; + return mantissa * multipliers[suffix]; +} + +function parseCpuMillicores(qty) { + const cores = parseK8sQuantity(qty); + return cores === undefined ? undefined : cores * 1000; +} + +function parseMemoryBytes(qty) { + return parseK8sQuantity(qty); +} + +function sumContainerResource(containers, resourceType, field, parseFn) { + let total; + for (const c of containers || []) { + const raw = + c.resources && + c.resources[resourceType] && + c.resources[resourceType][field]; + const parsed = parseFn(raw); + if (parsed !== undefined) total = (total || 0) + parsed; + } + return total; +} + +function computeContainerHealth(pod) { + const phase = pod.status && pod.status.phase; + const statuses = (pod.status && pod.status.containerStatuses) || []; + + for (const cs of statuses) { + const waitingReason = + cs.state && cs.state.waiting && cs.state.waiting.reason; + if (waitingReason === "CrashLoopBackOff") return "crashloop"; + if (waitingReason) return "waiting:" + waitingReason; + + const terminatedReason = + cs.state && cs.state.terminated && cs.state.terminated.reason; + if (terminatedReason === "OOMKilled") return "crashloop"; + } + + if (phase === "Running") return "healthy"; + if (phase === "Pending") return "pending"; + return phase ? phase.toLowerCase() : "unknown"; +} + +result = (data.items || []).map((item) => { + const owner = ((item.metadata && item.metadata.ownerReferences) || []).find( + (ref) => ref.controller === true, + ); + const labels = (item.metadata && item.metadata.labels) || {}; + const containers = (item.spec && item.spec.containers) || []; + const containerStatuses = + (item.status && item.status.containerStatuses) || []; + const restartCount = containerStatuses.reduce( + (sum, cs) => sum + (cs.restartCount || 0), + 0, + ); + + return { + uid: item.metadata.uid, + name: item.metadata.name, + namespace: item.metadata.namespace, + nodeName: item.spec && item.spec.nodeName, + ownerUid: owner ? owner.uid : undefined, + appLabel: + labels["app"] || + labels["app.kubernetes.io/name"] || + labels["k8s-app"] || + undefined, + phase: item.status && item.status.phase, + containerHealth: computeContainerHealth(item), + restartCount: restartCount, + cpuRequestMillicores: sumContainerResource( + containers, + "requests", + "cpu", + parseCpuMillicores, + ), + memoryRequestBytes: sumContainerResource( + containers, + "requests", + "memory", + parseMemoryBytes, + ), + cpuLimitMillicores: sumContainerResource( + containers, + "limits", + "cpu", + parseCpuMillicores, + ), + memoryLimitBytes: sumContainerResource( + containers, + "limits", + "memory", + parseMemoryBytes, + ), + }; +}); diff --git a/plugins/Kubernetes/v1/dataStreams/scripts/podsByParent.js b/plugins/Kubernetes/v1/dataStreams/scripts/podsByParent.js new file mode 100644 index 00000000..6bbfb5d6 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/scripts/podsByParent.js @@ -0,0 +1,61 @@ +function computeContainerHealth(pod) { + const phase = pod.status && pod.status.phase; + const statuses = (pod.status && pod.status.containerStatuses) || []; + + for (const cs of statuses) { + const waitingReason = + cs.state && cs.state.waiting && cs.state.waiting.reason; + if (waitingReason === "CrashLoopBackOff") return "crashloop"; + if (waitingReason) return "waiting:" + waitingReason; + + const terminatedReason = + cs.state && cs.state.terminated && cs.state.terminated.reason; + if (terminatedReason === "OOMKilled") return "crashloop"; + } + + if (phase === "Running") return "healthy"; + if (phase === "Pending") return "pending"; + return phase ? phase.toLowerCase() : "unknown"; +} + +// Object-picker fields can be arrays. Normalize them before key comparison. +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +const ownerName = context.objects[0].name; +const ownerNamespace = unwrap(context.objects[0].namespace); +const selectorAppLabel = unwrap(context.objects[0].selectorAppLabel); + +let items = data.items || []; + +if (ownerNamespace) { + if (selectorAppLabel !== undefined) { + items = items.filter((item) => { + const labels = (item.metadata && item.metadata.labels) || {}; + const podAppLabel = + labels["app"] || + labels["app.kubernetes.io/name"] || + labels["k8s-app"] || + undefined; + + return ( + podAppLabel !== undefined && podAppLabel === selectorAppLabel + ); + }); + } else { + const ownerUid = unwrap(context.objects[0].uid); + items = items.filter((item) => { + const owner = ((item.metadata && item.metadata.ownerReferences) || + []).find((ref) => ref.controller === true); + return owner && owner.uid === ownerUid; + }); + } +} + +result = items.map((item) => ({ + uid: item.metadata.uid, + name: item.metadata.name, + namespace: item.metadata.namespace, + nodeName: item.spec && item.spec.nodeName, + ownerName: ownerName, + phase: item.status && item.status.phase, + containerHealth: computeContainerHealth(item), +})); diff --git a/plugins/Kubernetes/v1/dataStreams/scripts/pvClaims.js b/plugins/Kubernetes/v1/dataStreams/scripts/pvClaims.js new file mode 100644 index 00000000..2d4190a0 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/scripts/pvClaims.js @@ -0,0 +1,54 @@ +// Kubernetes Quantity grammar: mantissa is [+-]?(digits[.digits?] | .digits), +// exponent accepts lowercase or uppercase e/E, suffix is a binary (Ki..Ei) or +// decimal (n/u/m/k/M/G/T/P/E) multiplier — never both on the same value. +function parseMemoryBytes(qty) { + if (qty === undefined || qty === null) return undefined; + const match = String(qty).match( + /^([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+))(?:([eE][+-]?[0-9]+)|(Ki|Mi|Gi|Ti|Pi|Ei|n|u|m|k|M|G|T|P|E))?$/, + ); + if (!match) return undefined; + const mantissa = parseFloat(match[1]); + if (Number.isNaN(mantissa)) return undefined; + if (match[2]) return mantissa * Math.pow(10, parseInt(match[2].slice(1), 10)); + const suffix = match[3] || ""; + if (suffix === "") return mantissa; + const multipliers = { + n: 1e-9, + u: 1e-6, + m: 1e-3, + k: 1e3, + M: 1e6, + G: 1e9, + T: 1e12, + P: 1e15, + E: 1e18, + Ki: 1024, + Mi: 1024 ** 2, + Gi: 1024 ** 3, + Ti: 1024 ** 4, + Pi: 1024 ** 5, + Ei: 1024 ** 6, + }; + return mantissa * multipliers[suffix]; +} + +const isSingleClaim = + data && data.kind === "PersistentVolumeClaim" && data.metadata; +const pvName = context.objects[0].name; + +result = isSingleClaim + ? [ + { + uid: data.metadata.uid, + name: data.metadata.name, + pvName: pvName, + phase: (data.status && data.status.phase) || "Unknown", + requestedStorageBytes: parseMemoryBytes( + data.spec && + data.spec.resources && + data.spec.resources.requests && + data.spec.resources.requests.storage, + ), + }, + ] + : []; diff --git a/plugins/Kubernetes/v1/dataStreams/scripts/replicasets.js b/plugins/Kubernetes/v1/dataStreams/scripts/replicasets.js new file mode 100644 index 00000000..5a919230 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/scripts/replicasets.js @@ -0,0 +1,25 @@ +const rows = (data.items || []).map((item) => { + const owner = ((item.metadata && item.metadata.ownerReferences) || []).find( + (ref) => ref.controller === true, + ); + + return { + uid: item.metadata.uid, + name: item.metadata.name, + namespace: item.metadata.namespace, + ownerUid: owner ? owner.uid : undefined, + desiredReplicas: (item.spec && item.spec.replicas) || 0, + readyReplicas: (item.status && item.status.readyReplicas) || 0, + }; +}); + +// Object-picker fields can be arrays. Normalize them before key comparison. +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +const selected = (context.config && context.config.deployment) || []; +const deploymentUids = new Set( + selected.map((o) => unwrap(o.rawId)).filter(Boolean), +); + +result = deploymentUids.size + ? rows.filter((r) => deploymentUids.has(r.ownerUid)) + : rows; diff --git a/plugins/Kubernetes/v1/dataStreams/scripts/resourcequotas.js b/plugins/Kubernetes/v1/dataStreams/scripts/resourcequotas.js new file mode 100644 index 00000000..8ee5a783 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/scripts/resourcequotas.js @@ -0,0 +1,70 @@ +// Kubernetes Quantity grammar: mantissa is [+-]?(digits[.digits?] | .digits), +// exponent accepts lowercase or uppercase e/E, suffix is a binary (Ki..Ei) or +// decimal (n/u/m/k/M/G/T/P/E) multiplier — never both on the same value. +function parseK8sQuantity(qty) { + if (qty === undefined || qty === null) return undefined; + const match = String(qty).match( + /^([+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+))(?:([eE][+-]?[0-9]+)|(Ki|Mi|Gi|Ti|Pi|Ei|n|u|m|k|M|G|T|P|E))?$/, + ); + if (!match) return undefined; + const mantissa = parseFloat(match[1]); + if (Number.isNaN(mantissa)) return undefined; + if (match[2]) return mantissa * Math.pow(10, parseInt(match[2].slice(1), 10)); + const suffix = match[3] || ""; + if (suffix === "") return mantissa; + const multipliers = { + n: 1e-9, + u: 1e-6, + m: 1e-3, + k: 1e3, + M: 1e6, + G: 1e9, + T: 1e12, + P: 1e15, + E: 1e18, + Ki: 1024, + Mi: 1024 ** 2, + Gi: 1024 ** 3, + Ti: 1024 ** 4, + Pi: 1024 ** 5, + Ei: 1024 ** 6, + }; + return mantissa * multipliers[suffix]; +} + +function parseCpuMillicores(qty) { + const cores = parseK8sQuantity(qty); + return cores === undefined ? undefined : cores * 1000; +} + +function parseMemoryBytes(qty) { + return parseK8sQuantity(qty); +} + +function parseCount(value) { + return value !== undefined ? parseInt(value, 10) : undefined; +} + +result = (data.items || []).map((item) => { + const hard = + (item.status && item.status.hard) || + (item.spec && item.spec.hard) || + {}; + const used = (item.status && item.status.used) || {}; + + return { + uid: item.metadata.uid, + name: item.metadata.name, + namespace: item.metadata.namespace, + cpuRequestHardMillicores: parseCpuMillicores(hard["requests.cpu"]), + cpuRequestUsedMillicores: parseCpuMillicores(used["requests.cpu"]), + memoryRequestHardBytes: parseMemoryBytes(hard["requests.memory"]), + memoryRequestUsedBytes: parseMemoryBytes(used["requests.memory"]), + cpuLimitHardMillicores: parseCpuMillicores(hard["limits.cpu"]), + cpuLimitUsedMillicores: parseCpuMillicores(used["limits.cpu"]), + memoryLimitHardBytes: parseMemoryBytes(hard["limits.memory"]), + memoryLimitUsedBytes: parseMemoryBytes(used["limits.memory"]), + podsHard: parseCount(hard.pods), + podsUsed: parseCount(used.pods), + }; +}); diff --git a/plugins/Kubernetes/v1/dataStreams/scripts/services.js b/plugins/Kubernetes/v1/dataStreams/scripts/services.js new file mode 100644 index 00000000..595c29ff --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/scripts/services.js @@ -0,0 +1,31 @@ +const rows = (data.items || []).map((item) => { + const selector = (item.spec && item.spec.selector) || {}; + + return { + uid: item.metadata.uid, + name: item.metadata.name, + namespace: item.metadata.namespace, + serviceType: item.spec && item.spec.type, + selectorAppLabel: + selector["app"] || + selector["app.kubernetes.io/name"] || + selector["k8s-app"] || + undefined, + }; +}); + +// Object-picker fields can be arrays. Normalize them before key comparison. +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +const selected = (context.config && context.config.ingress) || []; +const backends = new Set( + selected + .map( + (o) => + `${unwrap(o.namespace)}/${unwrap(o.primaryBackendServiceName)}`, + ) + .filter((key) => key !== "undefined/undefined"), +); + +result = backends.size + ? rows.filter((r) => backends.has(`${r.namespace}/${r.name}`)) + : rows; diff --git a/plugins/Kubernetes/v1/dataStreams/scripts/statefulsets.js b/plugins/Kubernetes/v1/dataStreams/scripts/statefulsets.js new file mode 100644 index 00000000..2a18522b --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/scripts/statefulsets.js @@ -0,0 +1,20 @@ +result = (data.items || []).map((item) => { + const desiredReplicas = (item.spec && item.spec.replicas) || 0; + const readyReplicas = (item.status && item.status.readyReplicas) || 0; + + const statefulSetHealth = + readyReplicas === desiredReplicas + ? "success" + : readyReplicas < desiredReplicas + ? "error" + : "unknown"; + + return { + uid: item.metadata.uid, + name: item.metadata.name, + namespace: item.metadata.namespace, + desiredReplicas, + readyReplicas, + statefulSetHealth, + }; +}); diff --git a/plugins/Kubernetes/v1/dataStreams/services.json b/plugins/Kubernetes/v1/dataStreams/services.json new file mode 100644 index 00000000..8a423f05 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/services.json @@ -0,0 +1,75 @@ +{ + "name": "services", + "displayName": "Services", + "description": "Kubernetes services, one row per service", + "tags": [ + "Resources" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/api/v1/services", + "postRequestScript": "services.js", + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "250" + }, + "in": { + "realm": "payload", + "path": "metadata.continue" + }, + "out": { + "realm": "queryArg", + "path": "continue" + } + } + }, + "matches": "none", + "ui": [ + { + "type": "objects", + "name": "ingress", + "label": "Filter by Ingress", + "matches": { + "sourceType": { + "type": "equals", + "value": "Ingress" + } + } + } + ], + "metadata": [ + { + "name": "uid", + "displayName": "UID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "namespace", + "displayName": "Namespace", + "shape": "string" + }, + { + "name": "serviceType", + "displayName": "Type", + "shape": "string" + }, + { + "name": "selectorAppLabel", + "displayName": "Selector App Label", + "shape": "string", + "visible": false + } + ], + "timeframes": false +} diff --git a/plugins/Kubernetes/v1/dataStreams/statefulsets.json b/plugins/Kubernetes/v1/dataStreams/statefulsets.json new file mode 100644 index 00000000..02f9db08 --- /dev/null +++ b/plugins/Kubernetes/v1/dataStreams/statefulsets.json @@ -0,0 +1,81 @@ +{ + "name": "statefulsets", + "displayName": "StatefulSets", + "description": "Kubernetes statefulsets, one row per statefulset", + "tags": [ + "Resources" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "/apis/apps/v1/statefulsets", + "postRequestScript": "statefulsets.js", + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "250" + }, + "in": { + "realm": "payload", + "path": "metadata.continue" + }, + "out": { + "realm": "queryArg", + "path": "continue" + } + } + }, + "matches": "none", + "metadata": [ + { + "name": "uid", + "displayName": "UID", + "shape": "string", + "role": "id" + }, + { + "name": "name", + "displayName": "Name", + "shape": "string", + "role": "label" + }, + { + "name": "namespace", + "displayName": "Namespace", + "shape": "string" + }, + { + "name": "desiredReplicas", + "displayName": "Desired Replicas", + "shape": "number" + }, + { + "name": "readyReplicas", + "displayName": "Ready Replicas", + "shape": "number" + }, + { + "name": "statefulSetHealth", + "displayName": "Health", + "shape": [ + "state", + { + "map": { + "success": [ + "success" + ], + "error": [ + "error" + ], + "unknown": [ + "unknown" + ] + } + } + ] + } + ], + "timeframes": false +} diff --git a/plugins/Kubernetes/v1/defaultContent/Perspectives/daemonset.dash.json b/plugins/Kubernetes/v1/defaultContent/Perspectives/daemonset.dash.json new file mode 100644 index 00000000..d8b902d3 --- /dev/null +++ b/plugins/Kubernetes/v1/defaultContent/Perspectives/daemonset.dash.json @@ -0,0 +1,147 @@ +{ + "name": "DaemonSet", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[DaemonSet]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "4476c80f-e060-4c3d-9835-47e709f9afa7", + "x": 0, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "DaemonSet Details", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[DaemonSet]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[DaemonSets]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[DaemonSet]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "2ae75b83-e2e6-4728-bcbd-d59420c5baa6", + "x": 2, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "DaemonSet Health", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[DaemonSet]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[DaemonSets]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[DaemonSet]}}" + }, + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "labelColumn": "name", + "stateColumn": "daemonSetHealth", + "sublabel": "namespace", + "linkColumn": "none", + "columns": 1 + } + } + } + } + }, + { + "i": "42205123-7bff-4af9-b900-2ee54ca6f91a", + "x": 0, + "y": 3, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pods on This DaemonSet", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[DaemonSet]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[podsByParent]}}", + "name": "podsByParent", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[DaemonSets]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[DaemonSet]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "phase", + "containerHealth" + ], + "hiddenColumns": [ + "uid", + "namespace", + "ownerName" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/Kubernetes/v1/defaultContent/Perspectives/deployment.dash.json b/plugins/Kubernetes/v1/defaultContent/Perspectives/deployment.dash.json new file mode 100644 index 00000000..93c50c36 --- /dev/null +++ b/plugins/Kubernetes/v1/defaultContent/Perspectives/deployment.dash.json @@ -0,0 +1,234 @@ +{ + "name": "Deployment", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[Deployment]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "cd241b6f-d785-4af9-ae7c-840a22735e13", + "x": 0, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Deployment Details", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Deployment]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Deployments]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Deployment]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "32f88234-4c8e-4b9b-9c54-470daede8253", + "x": 2, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Availability", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Deployment]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Deployments]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Deployment]}}" + }, + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "labelColumn": "name", + "stateColumn": "availableStatus", + "sublabel": "namespace", + "linkColumn": "none", + "columns": 1 + } + } + } + } + }, + { + "i": "44cf3468-d589-4d38-b857-ab517940bd5a", + "x": 0, + "y": 3, + "w": 2, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Desired Replicas", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Deployment]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Deployments]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Deployment]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "desiredReplicas", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "697f1c8f-3385-40df-a3e0-62962296a12f", + "x": 2, + "y": 3, + "w": 2, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Available Replicas", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Deployment]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Deployments]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Deployment]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "availableReplicas", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "33e247ed-bc9d-4dbf-91e0-ffd1fd7f82bc", + "x": 0, + "y": 5, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "ReplicaSets", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Deployment]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[replicasets]}}", + "name": "replicasets", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "deployment": { + "variable": "{{variables.[Deployment]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Deployments]}}" + } + } + }, + "scope": { + "scope": "{{scopes.[Deployments]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Deployment]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "desiredReplicas", + "readyReplicas" + ], + "hiddenColumns": [ + "uid", + "namespace", + "ownerUid" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/Kubernetes/v1/defaultContent/Perspectives/ingress.dash.json b/plugins/Kubernetes/v1/defaultContent/Perspectives/ingress.dash.json new file mode 100644 index 00000000..9b2fe007 --- /dev/null +++ b/plugins/Kubernetes/v1/defaultContent/Perspectives/ingress.dash.json @@ -0,0 +1,190 @@ +{ + "name": "Ingress", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[Ingress]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "98b1f462-1d7e-494c-b5ab-f493aaaa0bc5", + "x": 0, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Ingress Details", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Ingress]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Ingresses]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Ingress]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "ee117994-02b6-417d-9663-cb3327066ff9", + "x": 2, + "y": 0, + "w": 2, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Hosts", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Ingress]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Ingresses]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Ingress]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "hosts", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "51a53730-57b4-41e3-b5de-3e4d0236d2f8", + "x": 2, + "y": 2, + "w": 2, + "h": 1, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Ingress Class", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Ingress]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Ingresses]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Ingress]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "ingressClassName", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "f56fe34a-7ccd-4524-9dad-eeeb086e2791", + "x": 0, + "y": 3, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Backend Service", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Ingress]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[services]}}", + "name": "services", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "ingress": { + "variable": "{{variables.[Ingress]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Ingresses]}}" + } + } + }, + "scope": { + "scope": "{{scopes.[Ingresses]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Ingress]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "serviceType" + ], + "hiddenColumns": [ + "uid", + "namespace", + "selectorAppLabel" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/Kubernetes/v1/defaultContent/Perspectives/manifest.json b/plugins/Kubernetes/v1/defaultContent/Perspectives/manifest.json new file mode 100644 index 00000000..352d7f18 --- /dev/null +++ b/plugins/Kubernetes/v1/defaultContent/Perspectives/manifest.json @@ -0,0 +1,40 @@ +{ + "items": [ + { + "name": "node", + "type": "dashboard" + }, + { + "name": "namespace", + "type": "dashboard" + }, + { + "name": "deployment", + "type": "dashboard" + }, + { + "name": "daemonset", + "type": "dashboard" + }, + { + "name": "statefulset", + "type": "dashboard" + }, + { + "name": "pod", + "type": "dashboard" + }, + { + "name": "service", + "type": "dashboard" + }, + { + "name": "ingress", + "type": "dashboard" + }, + { + "name": "persistentvolume", + "type": "dashboard" + } + ] +} diff --git a/plugins/Kubernetes/v1/defaultContent/Perspectives/namespace.dash.json b/plugins/Kubernetes/v1/defaultContent/Perspectives/namespace.dash.json new file mode 100644 index 00000000..5b2a5197 --- /dev/null +++ b/plugins/Kubernetes/v1/defaultContent/Perspectives/namespace.dash.json @@ -0,0 +1,636 @@ +{ + "name": "Namespace", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[Namespace]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "b2dc0df5-8002-4898-b950-79f463355479", + "x": 0, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Namespace Details", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Namespace]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Namespaces]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Namespace]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "f2d52c78-0958-4f52-bbbf-f60ba14aba2b", + "x": 2, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Phase", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Namespace]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Namespaces]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Namespace]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "phase", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "8334bdc7-4cfb-42b9-8414-6313f656b5a2", + "x": 0, + "y": 3, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pods", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Namespace]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[namespaceContents]}}", + "name": "namespaceContents", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "detailType": "pods" + } + }, + "scope": { + "scope": "{{scopes.[Namespaces]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Namespace]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "nodeName", + "podPhase", + "containerHealth" + ], + "hiddenColumns": [ + "uid", + "namespace" + ] + } + } + } + } + }, + { + "i": "139997bf-2121-4a59-9d2a-33c7769b5cc3", + "x": 2, + "y": 3, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Deployments", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Namespace]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[namespaceContents]}}", + "name": "namespaceContents", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "detailType": "deployments" + } + }, + "scope": { + "scope": "{{scopes.[Namespaces]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Namespace]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "desiredReplicas", + "availableReplicas", + "availableStatus" + ], + "hiddenColumns": [ + "uid", + "namespace" + ] + } + } + } + } + }, + { + "i": "df851456-b70f-47de-9625-719e7afae0d5", + "x": 0, + "y": 6, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Services", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Namespace]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[namespaceContents]}}", + "name": "namespaceContents", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "detailType": "services" + } + }, + "scope": { + "scope": "{{scopes.[Namespaces]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Namespace]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "serviceType" + ], + "hiddenColumns": [ + "uid", + "namespace", + "selectorAppLabel" + ] + } + } + } + } + }, + { + "i": "44b093af-6bdf-4b53-901d-fd6e300b0cba", + "x": 2, + "y": 6, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "ReplicaSets", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Namespace]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[namespaceContents]}}", + "name": "namespaceContents", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "detailType": "replicasets" + } + }, + "scope": { + "scope": "{{scopes.[Namespaces]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Namespace]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "desiredReplicas", + "readyReplicas" + ], + "hiddenColumns": [ + "uid", + "namespace", + "ownerUid" + ] + } + } + } + } + }, + { + "i": "66028426-0883-4493-8019-daa13f057997", + "x": 0, + "y": 9, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "DaemonSets", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Namespace]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[namespaceContents]}}", + "name": "namespaceContents", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "detailType": "daemonsets" + } + }, + "scope": { + "scope": "{{scopes.[Namespaces]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Namespace]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "desiredNumberScheduled", + "numberReady", + "daemonSetHealth" + ], + "hiddenColumns": [ + "uid", + "namespace" + ] + } + } + } + } + }, + { + "i": "836d57ad-e0bc-476b-8582-e20bb7eba45e", + "x": 2, + "y": 9, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "StatefulSets", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Namespace]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[namespaceContents]}}", + "name": "namespaceContents", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "detailType": "statefulsets" + } + }, + "scope": { + "scope": "{{scopes.[Namespaces]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Namespace]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "desiredReplicas", + "readyReplicas", + "statefulSetHealth" + ], + "hiddenColumns": [ + "uid", + "namespace" + ] + } + } + } + } + }, + { + "i": "05948074-2ee1-4880-ad2a-21ab65f7c020", + "x": 0, + "y": 12, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Ingresses", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Namespace]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[namespaceContents]}}", + "name": "namespaceContents", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "detailType": "ingresses" + } + }, + "scope": { + "scope": "{{scopes.[Namespaces]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Namespace]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "ingressClassName", + "hosts" + ], + "hiddenColumns": [ + "uid", + "namespace", + "primaryBackendServiceName" + ] + } + } + } + } + }, + { + "i": "fff5276b-d7ad-435a-8c33-7d5f10d1b45a", + "x": 2, + "y": 12, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Persistent Volume Claims", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Namespace]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[namespaceContents]}}", + "name": "namespaceContents", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "detailType": "persistentvolumeclaims" + } + }, + "scope": { + "scope": "{{scopes.[Namespaces]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Namespace]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "phase", + "requestedStorageBytes" + ], + "hiddenColumns": [ + "uid", + "namespace", + "volumeName" + ] + } + } + } + } + }, + { + "i": "9a6f0c3e-3f0a-4e8f-9c0a-6a5c8f7f4a11", + "x": 0, + "y": 15, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Resource Quotas", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Namespace]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[namespaceContents]}}", + "name": "namespaceContents", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "detailType": "resourcequotas" + } + }, + "scope": { + "scope": "{{scopes.[Namespaces]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Namespace]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "cpuRequestUsedMillicores", + "cpuRequestHardMillicores", + "memoryRequestUsedBytes", + "memoryRequestHardBytes", + "podsUsed", + "podsHard" + ], + "hiddenColumns": [ + "uid", + "namespace", + "cpuLimitHardMillicores", + "cpuLimitUsedMillicores", + "memoryLimitHardBytes", + "memoryLimitUsedBytes" + ] + } + } + } + } + }, + { + "i": "3b2d9e7a-1c4e-4a9d-8b6f-2e7a9c1d5f22", + "x": 2, + "y": 15, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Limit Ranges", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Namespace]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[namespaceContents]}}", + "name": "namespaceContents", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "detailType": "limitranges" + } + }, + "scope": { + "scope": "{{scopes.[Namespaces]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Namespace]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "limitType", + "defaultRequestCpuMillicores", + "defaultCpuMillicores", + "defaultRequestMemoryBytes", + "defaultMemoryBytes" + ], + "hiddenColumns": [ + "uid", + "namespace", + "maxCpuMillicores", + "maxMemoryBytes", + "minCpuMillicores", + "minMemoryBytes" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/Kubernetes/v1/defaultContent/Perspectives/node.dash.json b/plugins/Kubernetes/v1/defaultContent/Perspectives/node.dash.json new file mode 100644 index 00000000..6b658cb3 --- /dev/null +++ b/plugins/Kubernetes/v1/defaultContent/Perspectives/node.dash.json @@ -0,0 +1,282 @@ +{ + "name": "Node", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[Node]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "a52c4614-b082-4f9d-bb07-600e1cd5c8ef", + "x": 0, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Node Details", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Node]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties", + "metadata": [ + { + "name": "allocatableMemoryBytes", + "shape": [ + "bytes", + { + "decimalPlaces": 2 + } + ] + }, + { + "name": "allocatableEphemeralStorageBytes", + "shape": [ + "bytes", + { + "decimalPlaces": 2 + } + ] + }, + { + "name": "capacityMemoryBytes", + "shape": [ + "bytes", + { + "decimalPlaces": 2 + } + ] + }, + { + "name": "capacityEphemeralStorageBytes", + "shape": [ + "bytes", + { + "decimalPlaces": 2 + } + ] + } + ] + }, + "scope": { + "scope": "{{scopes.[Nodes]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Node]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "efeabeb1-6bd8-422e-a59f-d99f349f060f", + "x": 2, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Node Health", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Node]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Nodes]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Node]}}" + }, + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "labelColumn": "name", + "stateColumn": "readyStatus", + "sublabel": "kubeletVersion", + "linkColumn": "none", + "columns": 1 + } + } + } + } + }, + { + "i": "42664be2-4900-4d0c-89a0-82aabd0d3c14", + "x": 0, + "y": 3, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pods on This Node", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Node]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[podsByParent]}}", + "name": "podsByParent", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Nodes]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Node]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "namespace", + "phase", + "containerHealth" + ], + "hiddenColumns": [ + "uid", + "ownerName", + "nodeName" + ] + } + } + } + } + }, + { + "i": "209bafd6-b3ee-47d0-bb31-b5e0e5a36d2a", + "x": 0, + "y": 6, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "CPU Usage", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Node]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[nodeMetrics]}}", + "name": "nodeMetrics", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "node": { + "variable": "{{variables.[Node]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Nodes]}}" + } + } + }, + "scope": { + "scope": "{{scopes.[Nodes]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Node]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "cpuUsageMillicores", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "80ca16c8-07ff-44b9-b790-28c826f3e1b2", + "x": 2, + "y": 6, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Memory Usage", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Node]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[nodeMetrics]}}", + "name": "nodeMetrics", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "node": { + "variable": "{{variables.[Node]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Nodes]}}" + } + } + }, + "scope": { + "scope": "{{scopes.[Nodes]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Node]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "memoryUsageBytes", + "comparisonColumn": "none" + } + } + } + } + } + ] + } +} diff --git a/plugins/Kubernetes/v1/defaultContent/Perspectives/persistentvolume.dash.json b/plugins/Kubernetes/v1/defaultContent/Perspectives/persistentvolume.dash.json new file mode 100644 index 00000000..5088ffbd --- /dev/null +++ b/plugins/Kubernetes/v1/defaultContent/Perspectives/persistentvolume.dash.json @@ -0,0 +1,208 @@ +{ + "name": "Persistent Volume", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[PersistentVolume]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "31f71241-e6bd-4f6c-bd9c-ac7c3b789268", + "x": 0, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Persistent Volume Details", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[PersistentVolume]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties", + "metadata": [ + { + "name": "capacityBytes", + "shape": [ + "bytes", + { + "decimalPlaces": 2 + } + ] + } + ] + }, + "scope": { + "scope": "{{scopes.[PersistentVolumes]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[PersistentVolume]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "be4eaab8-f7a6-4084-8c17-663de6fbf178", + "x": 2, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Phase", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[PersistentVolume]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[PersistentVolumes]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[PersistentVolume]}}" + }, + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "labelColumn": "name", + "stateColumn": "phase", + "sublabel": "none", + "linkColumn": "none", + "columns": 1 + } + } + } + } + }, + { + "i": "d556e179-d132-43e9-bd55-40a048d889ea", + "x": 0, + "y": 3, + "w": 4, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Capacity", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[PersistentVolume]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties", + "metadata": [ + { + "name": "capacityBytes", + "shape": [ + "bytes", + { + "decimalPlaces": 2 + } + ] + } + ] + }, + "scope": { + "scope": "{{scopes.[PersistentVolumes]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[PersistentVolume]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "capacityBytes", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "295a505e-7167-4048-8133-7546e759a916", + "x": 0, + "y": 5, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Claimed By", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[PersistentVolume]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[pvClaims]}}", + "name": "pvClaims", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[PersistentVolumes]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[PersistentVolume]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "phase", + "requestedStorageBytes" + ], + "hiddenColumns": [ + "uid", + "pvName" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/Kubernetes/v1/defaultContent/Perspectives/pod.dash.json b/plugins/Kubernetes/v1/defaultContent/Perspectives/pod.dash.json new file mode 100644 index 00000000..39dab63f --- /dev/null +++ b/plugins/Kubernetes/v1/defaultContent/Perspectives/pod.dash.json @@ -0,0 +1,253 @@ +{ + "name": "Pod", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[Pod]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "4d32b0e5-acce-466a-889c-e3998261df55", + "x": 0, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pod Details", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Pod]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties", + "metadata": [ + { + "name": "memoryRequestBytes", + "shape": [ + "bytes", + { + "decimalPlaces": 2 + } + ] + }, + { + "name": "memoryLimitBytes", + "shape": [ + "bytes", + { + "decimalPlaces": 2 + } + ] + } + ] + }, + "scope": { + "scope": "{{scopes.[Pods]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Pod]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "b76a7e7c-8f6d-4947-be0c-7e01c04ec290", + "x": 2, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pod Health", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Pod]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Pods]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Pod]}}" + }, + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "labelColumn": "name", + "stateColumn": "containerHealth", + "sublabel": "phase", + "linkColumn": "none", + "columns": 1 + } + } + } + } + }, + { + "i": "3dc6204d-b17e-4b23-812c-30c612895d31", + "x": 0, + "y": 3, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "CPU Usage", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Pod]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[podMetrics]}}", + "name": "podMetrics", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "pod": { + "variable": "{{variables.[Pod]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Pods]}}" + } + } + }, + "scope": { + "scope": "{{scopes.[Pods]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Pod]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "cpuUsageMillicores", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "644ec5a1-7f6e-424f-9e19-245dd8f7c781", + "x": 2, + "y": 3, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Memory Usage", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Pod]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[podMetrics]}}", + "name": "podMetrics", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "pod": { + "variable": "{{variables.[Pod]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Pods]}}" + } + } + }, + "scope": { + "scope": "{{scopes.[Pods]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Pod]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "memoryUsageBytes", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "d3be7f66-5787-4ee9-b6d6-39fe0caf981d", + "x": 0, + "y": 6, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Restarts", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Pod]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Pods]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Pod]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "restartCount", + "comparisonColumn": "none" + } + } + } + } + } + ] + } +} diff --git a/plugins/Kubernetes/v1/defaultContent/Perspectives/service.dash.json b/plugins/Kubernetes/v1/defaultContent/Perspectives/service.dash.json new file mode 100644 index 00000000..e4613fc4 --- /dev/null +++ b/plugins/Kubernetes/v1/defaultContent/Perspectives/service.dash.json @@ -0,0 +1,144 @@ +{ + "name": "Service", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[Service]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "de317afc-1e27-4b02-912c-042bcd19902d", + "x": 0, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Service Details", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Service]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "371ae762-577e-485d-a8d8-5d9148b113d6", + "x": 2, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Service Type", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Service]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "serviceType", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "51398440-d925-4f19-9809-986209601a50", + "x": 0, + "y": 3, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pods", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[Service]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[podsByParent]}}", + "name": "podsByParent", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Services]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Service]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "phase", + "containerHealth" + ], + "hiddenColumns": [ + "uid", + "namespace", + "ownerName" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/Kubernetes/v1/defaultContent/Perspectives/statefulset.dash.json b/plugins/Kubernetes/v1/defaultContent/Perspectives/statefulset.dash.json new file mode 100644 index 00000000..7789dabb --- /dev/null +++ b/plugins/Kubernetes/v1/defaultContent/Perspectives/statefulset.dash.json @@ -0,0 +1,147 @@ +{ + "name": "StatefulSet", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[StatefulSet]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "91dc4194-540c-4b42-b2d7-c7d56df22979", + "x": 0, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "StatefulSet Details", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[StatefulSet]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[StatefulSets]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[StatefulSet]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "0cbc7596-6729-46ff-9493-687ad961a215", + "x": 2, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "StatefulSet Health", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[StatefulSet]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[StatefulSets]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[StatefulSet]}}" + }, + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "labelColumn": "name", + "stateColumn": "statefulSetHealth", + "sublabel": "namespace", + "linkColumn": "none", + "columns": 1 + } + } + } + } + }, + { + "i": "213627a4-70d8-4cb7-9b07-fe92c788d31b", + "x": 0, + "y": 3, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pods on This StatefulSet", + "description": "", + "timeframe": "none", + "variables": [ + "{{variables.[StatefulSet]}}" + ], + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[podsByParent]}}", + "name": "podsByParent", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[StatefulSets]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[StatefulSet]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "phase", + "containerHealth" + ], + "hiddenColumns": [ + "uid", + "namespace", + "ownerName" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/Kubernetes/v1/defaultContent/manifest.json b/plugins/Kubernetes/v1/defaultContent/manifest.json new file mode 100644 index 00000000..2bcd3317 --- /dev/null +++ b/plugins/Kubernetes/v1/defaultContent/manifest.json @@ -0,0 +1,8 @@ +{ + "items": [ + { + "name": "Perspectives", + "type": "folder" + } + ] +} diff --git a/plugins/Kubernetes/v1/defaultContent/scopes.json b/plugins/Kubernetes/v1/defaultContent/scopes.json new file mode 100644 index 00000000..993f143f --- /dev/null +++ b/plugins/Kubernetes/v1/defaultContent/scopes.json @@ -0,0 +1,155 @@ +[ + { + "name": "Namespaces", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Namespace" + ] + } + }, + "variable": { + "name": "Namespace", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Nodes", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Node" + ] + } + }, + "variable": { + "name": "Node", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Deployments", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Deployment" + ] + } + }, + "variable": { + "name": "Deployment", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Pods", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Pod" + ] + } + }, + "variable": { + "name": "Pod", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Services", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Service" + ] + } + }, + "variable": { + "name": "Service", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "DaemonSets", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "DaemonSet" + ] + } + }, + "variable": { + "name": "DaemonSet", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "StatefulSets", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "StatefulSet" + ] + } + }, + "variable": { + "name": "StatefulSet", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Ingresses", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Ingress" + ] + } + }, + "variable": { + "name": "Ingress", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "PersistentVolumes", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Persistent Volume" + ] + } + }, + "variable": { + "name": "PersistentVolume", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/Kubernetes/v1/docs/README.md b/plugins/Kubernetes/v1/docs/README.md new file mode 100644 index 00000000..e4d000fa --- /dev/null +++ b/plugins/Kubernetes/v1/docs/README.md @@ -0,0 +1,144 @@ +# Kubernetes monitoring in SquaredUp + +Monitor a [Kubernetes](https://kubernetes.io) cluster in SquaredUp — a live, auto-discovered service map of namespaces, nodes, workloads (deployments, replica sets, daemon sets, stateful sets, pods), networking (services, ingresses), storage (persistent volumes), and per-namespace resource guardrails (resource quotas, limit ranges), with cluster and resource health (no Prometheus dependency), live CPU/memory usage, and a live cluster events feed, via the [Kubernetes API](https://kubernetes.io/docs/reference/kubernetes-api/). + +> ⚠️ CPU/memory usage tiles require [`metrics-server`](https://github.com/kubernetes-sigs/metrics-server) to be running in your cluster — this is Kubernetes' standard, lightweight Metrics API and is **not** Prometheus. Some managed clusters provide it by default (e.g. GKE); others, including EKS, do not. Run `kubectl top nodes` to check — if it fails with "Metrics API not available", install `metrics-server` to enable the CPU/memory usage tiles. + +## Setup + +You will need a **ServiceAccount bearer token** with read-only, cluster-wide access to core workload, networking, storage, and infrastructure objects. + +1. Apply the following manifest to your cluster with `kubectl apply -f -`. The identity applying it needs `get`/`create`/`patch` on `ServiceAccount`, `ClusterRole`, and `ClusterRoleBinding` objects, and — per Kubernetes' RBAC self-escalation rules — must either already hold every permission listed in the `squaredup-reader` ClusterRole itself, or hold `escalate` on `clusterroles` and `bind` on this specific ClusterRole. `cluster-admin` satisfies this but isn't required; a role scoped to exactly these permissions works too: + + ```yaml + apiVersion: v1 + kind: ServiceAccount + metadata: + name: squaredup-reader + namespace: kube-system + --- + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: squaredup-reader + rules: + - apiGroups: [""] + resources: + [ + "nodes", + "namespaces", + "pods", + "services", + "events", + "persistentvolumes", + "persistentvolumeclaims", + "resourcequotas", + "limitranges", + ] + verbs: ["list", "get"] + - apiGroups: ["apps"] + resources: + ["deployments", "replicasets", "daemonsets", "statefulsets"] + verbs: ["list", "get"] + - apiGroups: ["networking.k8s.io"] + resources: ["ingresses"] + verbs: ["list", "get"] + - apiGroups: ["metrics.k8s.io"] + resources: ["nodes", "pods"] + verbs: ["list", "get"] + --- + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: squaredup-reader + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: squaredup-reader + subjects: + - kind: ServiceAccount + name: squaredup-reader + namespace: kube-system + ``` + +2. Generate a long-lived token for the ServiceAccount: + + ```shell + kubectl create token squaredup-reader -n kube-system --duration=8760h + ``` + + On older clusters where [`TokenRequest`](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-request-v1/) defaults to short-lived tokens, create a Secret-based token instead — see the [Kubernetes service account token docs](https://kubernetes.io/docs/concepts/security/service-accounts/#getting-a-token). + + **`--duration=8760h` is a request, not a guarantee** — the API server clamps it to its own configured maximum token lifetime, so the token may expire sooner. Plan to rotate it before then: re-run the command above to mint a new `TokenRequest` token and update the **Bearer Token** field, or, for a Secret-based token, delete the old Secret and create a new one. + + If the token is ever compromised, note that deleting the `ClusterRoleBinding` alone does **not** invalidate it — it only removes that binding's authorization, and the token remains valid for authentication (a different binding could still grant it access later). To actually invalidate the token: delete its Secret (Secret-based tokens), or delete and recreate the `squaredup-reader` ServiceAccount (`TokenRequest` tokens are bound to the ServiceAccount's identity, so recreating it invalidates previously issued tokens — allow for a short control-plane propagation delay). During containment, also remove every `ClusterRoleBinding`/`RoleBinding` granting access to this ServiceAccount, not just the one created above. + +3. Find your cluster's **API server URL**: + - **Cloud mode** (SquaredUp reaches your cluster directly over the internet) — for a managed cluster this is shown in your cloud provider's console (e.g. the EKS/AKS/GKE cluster endpoint), or run `kubectl cluster-info` to print it locally. + - **Relay agent mode** (recommended for firewalled/private clusters — see [Deployment modes](#deployment-modes) below) — if the relay agent runs as a pod inside the cluster it's monitoring, use the in-cluster address `https://kubernetes.default.svc` instead of an external URL. +4. Paste the API server URL into the **API Server URL** field and the token into the **Bearer Token** field below. + +### Deployment modes + +This plugin is **hybrid** — it can run in **cloud mode** (SquaredUp connects to your API server directly; simplest setup, requires your API server to be reachable from the internet) or **relay agent mode** (a small on-premises agent you run inside your network relays requests out; use this for private/firewalled clusters, including local clusters like `kind`/`minikube`/OrbStack). Most real-world clusters are not internet-reachable, so relay agent mode is the common case outside a public demo cluster. See [SquaredUp's relay agent documentation](https://docs.squaredup.com/features/connect-and-explore/relay-agents) for installing the agent itself (it can run as a container anywhere with network access to your API server, including as a Deployment inside the cluster it monitors). + +## Configuration fields + +| Field | What it is | Where to find it | Required | +| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -------- | +| **API Server URL** | The full URL of your cluster's Kubernetes API server, including scheme and port. | `kubectl cluster-info`, or your cloud provider's cluster details page. | Yes | +| **Bearer Token** | Authenticates every request as the `squaredup-reader` ServiceAccount via the `Authorization: Bearer` header. | Output of `kubectl create token squaredup-reader -n kube-system --duration=8760h` after applying the RBAC manifest above. | Yes | +| **Ignore certificate errors** | Skips TLS certificate verification. Enable only for clusters with a self-signed API server certificate (e.g. local `kind`/`minikube` clusters). | N/A | No | + +> ⚠️ **Ignoring certificate errors removes protection against man-in-the-middle attacks.** With verification disabled, anything that can intercept the connection to your API server URL can impersonate it and capture your Bearer Token. Only enable this for a self-signed cluster reachable over a network path you trust (e.g. a local cluster, or an internal network via relay agent mode) — never for an API server reachable over the public internet. + +On save, the plugin validates the token and URL by listing namespaces (`GET /api/v1/namespaces`) — this confirms both connectivity and that the token actually has read access to a cluster resource (a check that a non-resource endpoint like `/version` wouldn't catch); an invalid token, insufficient RBAC, unreachable URL, or TLS error (if **Ignore certificate errors** is needed but unchecked) fails setup with a connection error. + +## What this plugin monitors + +- **An auto-discovered service map** — namespaces, nodes, deployments, replica sets, daemon sets, stateful sets, pods, services, ingresses, and persistent volumes, wired together automatically (a pod's node, its owning controller, the services and ingresses routing to it, and its containing namespace). +- **Cluster and resource health, no Prometheus required** — node health (readiness plus memory/disk/PID pressure and network availability), workload availability (deployments, daemon sets, stateful sets), and pod/container health (crash loops, image pull failures, OOM kills) read directly from the Kubernetes API's own status conditions. This reflects node/workload/pod status, not control-plane component readiness (API server, scheduler, controller-manager, etcd) — those aren't exposed by the resource endpoints this plugin reads. +- **Live CPU/memory usage** — current node and pod resource consumption via Kubernetes' standard Metrics API (`metrics-server`), plus each pod's configured requests/limits and each node's allocatable/total capacity (including ephemeral storage), for at-a-glance capacity headroom. This is a live snapshot, not a Prometheus-style history. +- **Per-namespace resource guardrails** — ResourceQuota consumption against its hard limits (CPU, memory, pod count) and LimitRange defaults/min/max, so you can see when a namespace is approaching what it's allowed to consume. +- **A live cluster events feed** — Normal and Warning events (scheduling, image pulls, restarts, failures) as they happen. + +The out-of-the-box content ships 9 perspective dashboards, one each for **Node**, **Namespace**, **Deployment**, **DaemonSet**, **StatefulSet**, **Pod**, **Service**, **Ingress**, and **Persistent Volume** — each showing that object's own health and details plus tables of what's running inside/on/behind it. Resource quotas and limit ranges appear as scoped tables on the Namespace perspective. Persistent volume claims appear there too, and via a Persistent Volume's "Claimed By" drilldown. There's no separate cluster-wide overview dashboard — start from the Namespace perspective, or the object list/graph views, for a cluster-wide view. + +## Data streams + +- **Namespaces**, **Nodes**, **Deployments**, **ReplicaSets**, **DaemonSets**, **StatefulSets**, **Pods**, **Services**, **Ingresses**, **Persistent Volumes**, **Resource Quotas**, **Limit Ranges** — one cluster-wide stream per object type, current state. +- **Events** — all cluster events, cluster-wide, with type, reason, message, and the object they concern. +- **Node/Pod Metrics** — current CPU/memory usage per node and per pod via the Metrics API; cluster-wide by default, or narrowed to a single Node/Pod when a perspective dashboard binds one in. +- **Namespace Contents** — one namespace-scoped stream with a "Detail Type" selector (Deployments, DaemonSets, StatefulSets, ReplicaSets, Services, Ingresses, Persistent Volume Claims, Resource Quotas, Limit Ranges, or Pods), used to drive every table on the Namespace perspective without a separate stream per object kind. +- **Pods by Parent** — one stream scoped to Node, DaemonSet, StatefulSet, or Service alike, returning the pods belonging to whichever one is currently in view (the Service case is a label-selector approximation — see Known limitations). +- **ReplicaSets owned by this Deployment** — the remaining scoped drilldown stream behind the Deployment perspective's table. + +## What gets indexed + +| Object type | API source | Represents | +| --------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------- | +| **Namespace** | `GET /api/v1/namespaces` | A Kubernetes namespace. | +| **Node** | `GET /api/v1/nodes` | A cluster node. | +| **Deployment** | `GET /apis/apps/v1/deployments` | A Kubernetes deployment. | +| **ReplicaSet** | `GET /apis/apps/v1/replicasets` | A replica set (bridges deployments to the pods they own). | +| **DaemonSet** | `GET /apis/apps/v1/daemonsets` | A workload scheduled on every matching node. | +| **StatefulSet** | `GET /apis/apps/v1/statefulsets` | A workload with stable identity/storage per replica. | +| **Pod** | `GET /api/v1/pods` | A Kubernetes Pod, regardless of phase. | +| **Service** | `GET /api/v1/services` | A Kubernetes service. | +| **Ingress** | `GET /apis/networking.k8s.io/v1/ingresses` | An external HTTP(S) routing rule. | +| **Persistent Volume** | `GET /api/v1/persistentvolumes` | A cluster-scoped storage volume, optionally bound to a claim. | +| **Resource Quota** | `GET /api/v1/resourcequotas` | A namespace's hard limits on aggregate resource consumption (CPU, memory, pod count). | +| **Limit Range** | `GET /api/v1/limitranges` | A namespace's default/min/max resource constraints applied per container. | + +**Relationships:** Pod → Node (scheduled on); Pod → ReplicaSet (owned by) → Deployment (owned by); Pod → DaemonSet (owned by); Pod → StatefulSet (owned by); Service → Pod (routes to, label-selector approximation); Ingress → Service (routes to, primary-backend approximation); Namespace → Pod/Deployment/ReplicaSet/DaemonSet/StatefulSet/Service/Ingress/ResourceQuota/LimitRange (contains). + +## Known limitations + +- **CPU/memory usage requires `metrics-server`.** If it isn't installed in your cluster, the usage tiles will show no data (the rest of the plugin is unaffected) — see the note in Setup. +- **Label-selector matching is approximate.** Real Kubernetes Service→Pod matching requires every key/value pair in a service's selector to be present on the pod (a subset match); this plugin approximates it by matching a single well-known label key (`app`, falling back to `app.kubernetes.io/name`, then `k8s-app`). This covers the large majority of real-world services but can miss services selecting on other/multiple keys, or falsely match unrelated pods that happen to share the same resolved value in rare cases. +- **Ingress→Service matching is a single-backend approximation.** An Ingress can route different paths/hosts to different services; this plugin correlates on one "primary" backend (the default backend if set, otherwise the first rule's first path) rather than modeling every path individually. +- **Cluster-wide read RBAC required.** The unscoped, cluster-wide streams (Namespaces, Nodes, Deployments, ReplicaSets, DaemonSets, StatefulSets, Pods, Services, Ingresses, Persistent Volumes, Resource Quotas, Limit Ranges, Events, Node/Pod Metrics) all issue cluster-wide `list` calls — a ServiceAccount restricted to specific namespaces won't work for these. The namespaced drilldown streams (Namespace Contents, and the namespaced case of Pods by Parent and Persistent Volume Claims) issue per-namespace requests instead, so they only need `list`/`get` in the namespace(s) actually being viewed — but the RBAC manifest above still grants cluster-wide access for simplicity, since this plugin's dashboards otherwise assume cluster-wide visibility anyway. +- **No historical metrics.** Every data stream, including CPU/memory usage, is a current-state snapshot (`timeframes: false`) — there is no time-series history. Add a Prometheus-based plugin alongside this one if you need trends over time. +- **No mTLS / client-certificate authentication.** Only a ServiceAccount bearer token is supported, which is also the standard way external tools authenticate to a Kubernetes API server. +- **v1 object coverage.** Jobs/CronJobs are not yet imported — a future addition. +- **Read-only.** This plugin never creates, modifies, or deletes anything in your cluster. diff --git a/plugins/Kubernetes/v1/icon.svg b/plugins/Kubernetes/v1/icon.svg new file mode 100644 index 00000000..f16fb925 --- /dev/null +++ b/plugins/Kubernetes/v1/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugins/Kubernetes/v1/indexDefinitions/default.json b/plugins/Kubernetes/v1/indexDefinitions/default.json new file mode 100644 index 00000000..86cca8eb --- /dev/null +++ b/plugins/Kubernetes/v1/indexDefinitions/default.json @@ -0,0 +1,324 @@ +{ + "steps": [ + { + "name": "namespaces", + "dataStream": { + "name": "namespaces" + }, + "timeframe": "none", + "objectMapping": { + "id": "uid", + "name": "name", + "type": { + "value": "Namespace" + }, + "properties": [ + "uid", + "phase" + ] + }, + "optional": true + }, + { + "name": "nodes", + "dataStream": { + "name": "nodes" + }, + "timeframe": "none", + "objectMapping": { + "id": "uid", + "name": "name", + "type": { + "value": "Node" + }, + "properties": [ + "uid", + "kubeletVersion", + "osImage", + "unschedulable", + "readyStatus", + "memoryPressureStatus", + "diskPressureStatus", + "pidPressureStatus", + "networkUnavailableStatus", + "allocatableCpuMillicores", + "allocatableMemoryBytes", + "allocatableEphemeralStorageBytes", + "capacityCpuMillicores", + "capacityMemoryBytes", + "capacityEphemeralStorageBytes" + ] + }, + "dependsOn": [ + "namespaces" + ], + "optional": true + }, + { + "name": "deployments", + "dataStream": { + "name": "deployments" + }, + "timeframe": "none", + "objectMapping": { + "id": "uid", + "name": "name", + "type": { + "value": "Deployment" + }, + "properties": [ + "uid", + "namespace", + "desiredReplicas", + "availableReplicas", + "availableStatus" + ] + }, + "optional": true + }, + { + "name": "replicasets", + "dataStream": { + "name": "replicasets" + }, + "timeframe": "none", + "objectMapping": { + "id": "uid", + "name": "name", + "type": { + "value": "ReplicaSet" + }, + "properties": [ + "uid", + "namespace", + "ownerUid", + "desiredReplicas", + "readyReplicas" + ] + }, + "dependsOn": [ + "deployments" + ], + "optional": true + }, + { + "name": "pods", + "dataStream": { + "name": "pods" + }, + "timeframe": "none", + "objectMapping": { + "id": "uid", + "name": "name", + "type": { + "value": "Pod" + }, + "properties": [ + "uid", + "namespace", + "nodeName", + "ownerUid", + "appLabel", + "phase", + "containerHealth", + "restartCount", + "cpuRequestMillicores", + "memoryRequestBytes", + "cpuLimitMillicores", + "memoryLimitBytes" + ] + }, + "dependsOn": [ + "replicasets" + ], + "optional": true + }, + { + "name": "daemonsets", + "dataStream": { + "name": "daemonsets" + }, + "timeframe": "none", + "objectMapping": { + "id": "uid", + "name": "name", + "type": { + "value": "DaemonSet" + }, + "properties": [ + "uid", + "namespace", + "desiredNumberScheduled", + "numberReady", + "daemonSetHealth" + ] + }, + "dependsOn": [ + "nodes" + ], + "optional": true + }, + { + "name": "statefulsets", + "dataStream": { + "name": "statefulsets" + }, + "timeframe": "none", + "objectMapping": { + "id": "uid", + "name": "name", + "type": { + "value": "StatefulSet" + }, + "properties": [ + "uid", + "namespace", + "desiredReplicas", + "readyReplicas", + "statefulSetHealth" + ] + }, + "dependsOn": [ + "daemonsets" + ], + "optional": true + }, + { + "name": "ingresses", + "dataStream": { + "name": "ingresses" + }, + "timeframe": "none", + "objectMapping": { + "id": "uid", + "name": "name", + "type": { + "value": "Ingress" + }, + "properties": [ + "uid", + "namespace", + "ingressClassName", + "hosts", + "primaryBackendServiceName" + ] + }, + "dependsOn": [ + "services" + ], + "optional": true + }, + { + "name": "persistentvolumes", + "dataStream": { + "name": "persistentvolumes" + }, + "timeframe": "none", + "objectMapping": { + "id": "uid", + "name": "name", + "type": { + "value": "Persistent Volume" + }, + "properties": [ + "uid", + "phase", + "capacityBytes", + "claimName", + "claimNamespace" + ] + }, + "dependsOn": [ + "ingresses" + ], + "optional": true + }, + { + "name": "services", + "dataStream": { + "name": "services" + }, + "timeframe": "none", + "objectMapping": { + "id": "uid", + "name": "name", + "type": { + "value": "Service" + }, + "properties": [ + "uid", + "namespace", + "serviceType", + "selectorAppLabel" + ] + }, + "dependsOn": [ + "pods" + ], + "optional": true + }, + { + "name": "resourcequotas", + "dataStream": { + "name": "resourcequotas" + }, + "timeframe": "none", + "objectMapping": { + "id": "uid", + "name": "name", + "type": { + "value": "Resource Quota" + }, + "properties": [ + "uid", + "namespace", + "cpuRequestHardMillicores", + "cpuRequestUsedMillicores", + "memoryRequestHardBytes", + "memoryRequestUsedBytes", + "cpuLimitHardMillicores", + "cpuLimitUsedMillicores", + "memoryLimitHardBytes", + "memoryLimitUsedBytes", + "podsHard", + "podsUsed" + ] + }, + "dependsOn": [ + "statefulsets" + ], + "optional": true + }, + { + "name": "limitranges", + "dataStream": { + "name": "limitranges" + }, + "timeframe": "none", + "objectMapping": { + "id": "uid", + "name": "name", + "type": { + "value": "Limit Range" + }, + "properties": [ + "uid", + "namespace", + "limitType", + "defaultCpuMillicores", + "defaultMemoryBytes", + "defaultRequestCpuMillicores", + "defaultRequestMemoryBytes", + "maxCpuMillicores", + "maxMemoryBytes", + "minCpuMillicores", + "minMemoryBytes" + ] + }, + "dependsOn": [ + "resourcequotas" + ], + "optional": true + } + ] +} diff --git a/plugins/Kubernetes/v1/metadata.json b/plugins/Kubernetes/v1/metadata.json new file mode 100644 index 00000000..d4bae04d --- /dev/null +++ b/plugins/Kubernetes/v1/metadata.json @@ -0,0 +1,71 @@ +{ + "name": "kubernetes", + "displayName": "Kubernetes", + "version": "1.0.0", + "author": { + "name": "SquaredUp Labs", + "type": "labs" + }, + "description": "Monitor a Kubernetes cluster with an auto-discovered service map, control-plane health, and a live events feed.", + "category": "Infrastructure", + "type": "hybrid", + "schemaVersion": "2.1", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": [ + "kubernetes", + "k8s", + "containers", + "orchestration", + "infrastructure", + "cluster", + "gke", + "aks", + "eks", + "kind", + "minikube", + "docker", + "orbstack" + ], + "objectTypes": [ + "Namespace", + "Node", + "Deployment", + "ReplicaSet", + "DaemonSet", + "StatefulSet", + "Pod", + "Service", + "Ingress", + "Persistent Volume", + "Resource Quota", + "Limit Range" + ], + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/Kubernetes/v1/docs/README.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins", + "label": "Repository" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "baseUrl": "{{host}}", + "authMode": "none", + "headers": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "queryArgs": [] + } + } +} diff --git a/plugins/Kubernetes/v1/ui.json b/plugins/Kubernetes/v1/ui.json new file mode 100644 index 00000000..5681c8aa --- /dev/null +++ b/plugins/Kubernetes/v1/ui.json @@ -0,0 +1,28 @@ +[ + { + "type": "url", + "name": "host", + "label": "API Server URL", + "help": "Full URL to your Kubernetes API server, including port. Example: https://192.168.1.100:6443", + "placeholder": "https://your-cluster-api-server:6443", + "validation": { + "required": true + } + }, + { + "type": "password", + "name": "token", + "label": "Bearer Token", + "help": "ServiceAccount bearer token with read access to cluster objects. See the plugin's setup guide for the RBAC manifest to apply.", + "placeholder": "eyJhbGciOi...", + "validation": { + "required": true + } + }, + { + "type": "checkbox", + "name": "ignoreCertificateErrors", + "label": "Ignore certificate errors", + "help": "Enable when connecting to a cluster with a self-signed API server certificate (e.g. a local kind/minikube cluster)." + } +]