diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bed6d986..a7917bec7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan - [#22](https://github.com/kobsio/kobs/pull/22): Add Helm chart. - [#23](https://github.com/kobsio/kobs/pull/23): Add Kustomize files and demo. - [#24](https://github.com/kobsio/kobs/pull/24): Add documentation. +- [#26](https://github.com/kobsio/kobs/pull/26): Add support for all Kubernetes resources. ### Fixed diff --git a/app/public/img/plugins/applications.png b/app/public/img/plugins/applications.png new file mode 100644 index 000000000..b8328741b Binary files /dev/null and b/app/public/img/plugins/applications.png differ diff --git a/app/public/img/plugins/kobs.png b/app/public/img/plugins/kobs.png deleted file mode 100644 index 3a6ff484c..000000000 Binary files a/app/public/img/plugins/kobs.png and /dev/null differ diff --git a/app/public/img/plugins/plugins.png b/app/public/img/plugins/plugins.png index 137c36f3c..b1fd35685 100644 Binary files a/app/public/img/plugins/plugins.png and b/app/public/img/plugins/plugins.png differ diff --git a/app/public/img/plugins/kubernetes.png b/app/public/img/plugins/resources.png similarity index 100% rename from app/public/img/plugins/kubernetes.png rename to app/public/img/plugins/resources.png diff --git a/app/src/components/Home.tsx b/app/src/components/Home.tsx index b8b6c8337..827dd7b18 100644 --- a/app/src/components/Home.tsx +++ b/app/src/components/Home.tsx @@ -20,16 +20,11 @@ const Home: React.FunctionComponent = () => { title="Applications" body={applicationsDescription} link="/applications" - icon="/img/plugins/kobs.png" + icon="/img/plugins/applications.png" /> - + {pluginsContext.plugins.length === 0 ? ( diff --git a/app/src/components/LinkWrapper.tsx b/app/src/components/LinkWrapper.tsx index b263deaa3..9e9e7866d 100644 --- a/app/src/components/LinkWrapper.tsx +++ b/app/src/components/LinkWrapper.tsx @@ -10,12 +10,12 @@ interface ILinkWrapperProps { // which should navigate the user to another location. This is to prefer over an onClick handler, so that the user can // decide if he wants the link in a new tab or not. const LinkWrapper: React.FunctionComponent = ({ children, link }: ILinkWrapperProps) => { + if (link.startsWith('http')) { + return
window.open(link, '_blank')}>{children}
; + } + return ( - + {children} ); diff --git a/app/src/plugins/elasticsearch/ElasticsearchPreviewChart.tsx b/app/src/plugins/elasticsearch/ElasticsearchPreviewChart.tsx index 428decb4b..685154431 100644 --- a/app/src/plugins/elasticsearch/ElasticsearchPreviewChart.tsx +++ b/app/src/plugins/elasticsearch/ElasticsearchPreviewChart.tsx @@ -78,7 +78,7 @@ const ElasticsearchPreviewChart: React.FunctionComponent
{data.hits} Hits
{query.name ? ( -
{query.name}
+
{query.name}
) : null}
diff --git a/app/src/plugins/jaeger/JaegerPageCompareTrace.tsx b/app/src/plugins/jaeger/JaegerPageCompareTrace.tsx index 4e7404a98..e4cece618 100644 --- a/app/src/plugins/jaeger/JaegerPageCompareTrace.tsx +++ b/app/src/plugins/jaeger/JaegerPageCompareTrace.tsx @@ -93,7 +93,7 @@ const JaegerPageCompareTrace: React.FunctionComponent - + <Title className="pf-u-text-nowrap pf-u-text-truncate" headingLevel="h6" size="xl"> {data.trace.processes[data.trace.spans[0].processID].serviceName}: {data.trace.spans[0].operationName}{' '} <span className="pf-u-pl-sm pf-u-font-size-sm pf-u-color-400">{data.trace.traceID}</span> diff --git a/app/src/utils/helpers.ts b/app/src/utils/helpers.ts index f6d1461cc..ab6afdd4e 100644 --- a/app/src/utils/helpers.ts +++ b/app/src/utils/helpers.ts @@ -1,3 +1,5 @@ +import { V1LabelSelector } from '@kubernetes/client-node'; + // timeDifference calculates the difference of two given timestamps and returns a human readable string for the // difference. It is used to get the same style for the age of resources like it is displayed by kubectl. export const timeDifference = (current: number, previous: number): string => { @@ -28,3 +30,25 @@ export const formatTime = (timestamp: number): string => { '0' + d.getHours() ).slice(-2)}:${('0' + d.getMinutes()).slice(-2)}:${('0' + d.getSeconds()).slice(-2)}`; }; + +// getLabelSelector returns the given label selector as string, so that it can be used within the React UI. +export const getLabelSelector = (labelSelector: V1LabelSelector | undefined): string => { + if (!labelSelector) { + return ''; + } + + if (labelSelector.matchLabels) { + return Object.keys(labelSelector.matchLabels) + .map( + (key) => + `${key}=${ + labelSelector.matchLabels && labelSelector.matchLabels.hasOwnProperty(key) + ? labelSelector.matchLabels[key] + : '' + }`, + ) + .join(', '); + } + + return ''; +}; diff --git a/app/src/utils/resources.tsx b/app/src/utils/resources.tsx index 94b79576e..d675008e3 100644 --- a/app/src/utils/resources.tsx +++ b/app/src/utils/resources.tsx @@ -1,12 +1,31 @@ import { Bullseye, EmptyState, EmptyStateBody, EmptyStateIcon, EmptyStateVariant, Title } from '@patternfly/react-core'; import { + CoreV1EventList, + V1ClusterRoleBindingList, + V1ClusterRoleList, + V1ConfigMapList, V1DaemonSetList, V1DeploymentList, + V1EndpointsList, + V1IngressList, V1JobList, + V1NetworkPolicyList, + V1NodeList, + V1PersistentVolumeClaimList, + V1PersistentVolumeList, V1PodList, V1ReplicaSetList, + V1RoleBindingList, + V1RoleList, + V1SecretList, + V1ServiceAccountList, + V1ServiceList, V1StatefulSetList, + V1StorageClassList, V1beta1CronJobList, + V1beta1PodDisruptionBudgetList, + V1beta1PodSecurityPolicyList, + V2beta1HorizontalPodAutoscalerList, } from '@kubernetes/client-node'; import { IRow } from '@patternfly/react-table'; import { JSONPath } from 'jsonpath-plus'; @@ -14,11 +33,20 @@ import React from 'react'; import { SearchIcon } from '@patternfly/react-icons'; import { CRD, Resources as ProtoResources } from 'proto/clusters_grpc_web_pb'; -import { timeDifference } from 'utils/helpers'; +import { getLabelSelector, timeDifference } from 'utils/helpers'; // TScope is the scope of a resource, which can be namespaced or cluster. export type TScope = 'Namespaced' | 'Cluster'; +export interface IResourceGroups { + [key: string]: IResourceGroup; +} + +export interface IResourceGroup { + name: string; + resources: IResources; +} + // The IResources is a list of resources, which is supported by kobs. export interface IResources { [key: string]: IResource; @@ -43,6 +71,7 @@ export interface IResource { // result from the gRPC API call to the rows function. The returned rows are mostly the same as they are also retunred // by kubectl. const resources: IResources = { + // eslint-disable-next-line sort-keys cronjobs: { columns: ['Name', 'Namespace', 'Cluster', 'Schedule', 'Suspend', 'Active', 'Last Schedule', 'Age'], description: 'A CronJob creates Jobs on a repeating schedule.', @@ -390,6 +419,809 @@ const resources: IResources = { scope: 'Namespaced', title: 'Stateful Sets', }, + // eslint-disable-next-line sort-keys + endpoints: { + columns: ['Name', 'Namespace', 'Cluster', 'Endpoints', 'Age'], + description: '', + isCRD: false, + path: '/api/v1', + resource: 'endpoints', + rows: (results: ProtoResources[]): IRow[] => { + const rows: IRow[] = []; + + for (const result of results) { + const enpointList: V1EndpointsList = JSON.parse(result.getResourcelist()); + for (const endpoint of enpointList.items) { + const age = + endpoint.metadata && endpoint.metadata.creationTimestamp + ? timeDifference(new Date().getTime(), new Date(endpoint.metadata.creationTimestamp.toString()).getTime()) + : '-'; + const ep: string[] = []; + if (endpoint.subsets) { + for (const subset of endpoint.subsets) { + const ips = subset.addresses?.map((address) => address.ip); + if (ips) { + ep.push(...ips); + } + } + } + + rows.push({ + cells: [endpoint.metadata?.name, result.getNamespace(), result.getCluster(), ep.join(', '), age], + props: endpoint, + }); + } + } + + return rows; + }, + scope: 'Namespaced', + title: 'Endpoints', + }, + horizontalpodautoscalers: { + columns: ['Name', 'Namespace', 'Cluster', 'Reference', 'Min. Pods', 'Max. Pods', 'Replicas', 'Age'], + description: '', + isCRD: false, + path: '/apis/autoscaling/v2beta1', + resource: 'horizontalpodautoscalers', + rows: (results: ProtoResources[]): IRow[] => { + const rows: IRow[] = []; + + for (const result of results) { + const hpaList: V2beta1HorizontalPodAutoscalerList = JSON.parse(result.getResourcelist()); + for (const hpa of hpaList.items) { + const reference = + hpa.spec && hpa.spec.scaleTargetRef + ? `${hpa.spec.scaleTargetRef.kind}/${hpa.spec.scaleTargetRef.name}` + : ''; + const minPods = hpa.spec && hpa.spec.minReplicas ? hpa.spec.minReplicas : ''; + const maxPods = hpa.spec && hpa.spec.maxReplicas ? hpa.spec.maxReplicas : ''; + const replicas = + hpa.status && hpa.status.currentReplicas + ? `${hpa.status.currentReplicas}${hpa.status.desiredReplicas ? `/${hpa.status.desiredReplicas}` : ''}` + : ''; + const age = + hpa.metadata && hpa.metadata.creationTimestamp + ? timeDifference(new Date().getTime(), new Date(hpa.metadata.creationTimestamp.toString()).getTime()) + : '-'; + + rows.push({ + cells: [ + hpa.metadata?.name, + result.getNamespace(), + result.getCluster(), + reference, + minPods, + maxPods, + replicas, + age, + ], + props: hpa, + }); + } + } + + return rows; + }, + scope: 'Namespaced', + title: 'Horizontal Pod Autoscalers', + }, + ingresses: { + columns: ['Name', 'Namespace', 'Cluster', 'Hosts', 'Adress', 'Age'], + description: '', + isCRD: false, + path: '/apis/extensions/v1beta1', + resource: 'ingresses', + rows: (results: ProtoResources[]): IRow[] => { + const rows: IRow[] = []; + + for (const result of results) { + const ingressList: V1IngressList = JSON.parse(result.getResourcelist()); + for (const ingress of ingressList.items) { + const hosts = ingress.spec?.rules?.map((rule) => rule.host); + const address = + ingress.status && + ingress.status.loadBalancer && + ingress.status.loadBalancer.ingress && + ingress.status.loadBalancer.ingress.length > 0 && + ingress.status.loadBalancer.ingress[0].ip + ? ingress.status.loadBalancer.ingress[0].ip + : ''; + const age = + ingress.metadata && ingress.metadata.creationTimestamp + ? timeDifference(new Date().getTime(), new Date(ingress.metadata.creationTimestamp.toString()).getTime()) + : '-'; + + rows.push({ + cells: [ + ingress.metadata?.name, + result.getNamespace(), + result.getCluster(), + hosts ? hosts.join(', ') : '', + address, + age, + ], + props: ingress, + }); + } + } + + return rows; + }, + scope: 'Namespaced', + title: 'Ingresses', + }, + networkpolicies: { + columns: ['Name', 'Namespace', 'Cluster', 'Pod Selector', 'Age'], + description: '', + isCRD: false, + path: '/apis/networking.k8s.io/v1', + resource: 'networkpolicies', + rows: (results: ProtoResources[]): IRow[] => { + const rows: IRow[] = []; + + for (const result of results) { + const networkPolicyList: V1NetworkPolicyList = JSON.parse(result.getResourcelist()); + for (const networkPolicy of networkPolicyList.items) { + const podSelector = getLabelSelector(networkPolicy.spec?.podSelector); + const age = + networkPolicy.metadata && networkPolicy.metadata.creationTimestamp + ? timeDifference( + new Date().getTime(), + new Date(networkPolicy.metadata.creationTimestamp.toString()).getTime(), + ) + : '-'; + + rows.push({ + cells: [networkPolicy.metadata?.name, result.getNamespace(), result.getCluster(), podSelector, age], + props: networkPolicy, + }); + } + } + + return rows; + }, + scope: 'Namespaced', + title: 'Network Policies', + }, + services: { + columns: ['Name', 'Namespace', 'Cluster', 'Type', 'Cluster IP', 'External IP', 'Port(s)', 'Age'], + description: '', + isCRD: false, + path: '/api/v1', + resource: 'services', + rows: (results: ProtoResources[]): IRow[] => { + const rows: IRow[] = []; + + for (const result of results) { + const serviceList: V1ServiceList = JSON.parse(result.getResourcelist()); + for (const service of serviceList.items) { + const type = service.spec ? service.spec.type : ''; + const clusterIP = service.spec && service.spec.clusterIP ? service.spec.clusterIP : ''; + const externalIPs = + service.status && service.status.loadBalancer && service.status.loadBalancer.ingress + ? service.status.loadBalancer.ingress.map((ingress) => (ingress.ip ? ingress.ip : '')).join(', ') + : ''; + const ports = + service.spec && service.spec.ports + ? service.spec.ports + .map( + (port) => + `${port.port}${port.protocol ? `/${port.protocol}` : ''} (${port.name}${ + port.appProtocol ? `/${port.appProtocol}` : '' + })`, + ) + .join(', ') + : ''; + const age = + service.metadata && service.metadata.creationTimestamp + ? timeDifference(new Date().getTime(), new Date(service.metadata.creationTimestamp.toString()).getTime()) + : '-'; + + rows.push({ + cells: [ + service.metadata?.name, + result.getNamespace(), + result.getCluster(), + type, + clusterIP, + externalIPs, + ports, + age, + ], + props: service, + }); + } + } + + return rows; + }, + scope: 'Namespaced', + title: 'Services', + }, + // eslint-disable-next-line sort-keys + configmaps: { + columns: ['Name', 'Namespace', 'Cluster', 'Data', 'Age'], + description: '', + isCRD: false, + path: '/api/v1', + resource: 'configmaps', + rows: (results: ProtoResources[]): IRow[] => { + const rows: IRow[] = []; + + for (const result of results) { + const configMapList: V1ConfigMapList = JSON.parse(result.getResourcelist()); + for (const configMap of configMapList.items) { + const age = + configMap.metadata && configMap.metadata.creationTimestamp + ? timeDifference( + new Date().getTime(), + new Date(configMap.metadata.creationTimestamp.toString()).getTime(), + ) + : '-'; + + rows.push({ + cells: [ + configMap.metadata?.name, + result.getNamespace(), + result.getCluster(), + configMap.data ? Object.keys(configMap.data).length : 0, + age, + ], + props: configMap, + }); + } + } + + return rows; + }, + scope: 'Namespaced', + title: 'Config Maps', + }, + persistentvolumeclaims: { + columns: ['Name', 'Namespace', 'Cluster', 'Status', 'Volume', 'Capacity', 'Access Modes', 'Storage Class', 'Age'], + description: '', + isCRD: false, + path: '/api/v1', + resource: 'persistentvolumeclaims', + rows: (results: ProtoResources[]): IRow[] => { + const rows: IRow[] = []; + + for (const result of results) { + const pvcList: V1PersistentVolumeClaimList = JSON.parse(result.getResourcelist()); + for (const pvc of pvcList.items) { + const status = pvc.status && pvc.status.phase ? pvc.status.phase : ''; + const volume = pvc.spec && pvc.spec.volumeName ? pvc.spec.volumeName : ''; + const capacity = + pvc.status && pvc.status.capacity && pvc.status.capacity.storage ? pvc.status.capacity.storage : ''; + const accessMode = pvc.spec && pvc.spec.accessModes ? pvc.spec.accessModes.join(', ') : ''; + const storageClass = pvc.spec && pvc.spec.storageClassName ? pvc.spec.storageClassName : ''; + const age = + pvc.metadata && pvc.metadata.creationTimestamp + ? timeDifference(new Date().getTime(), new Date(pvc.metadata.creationTimestamp.toString()).getTime()) + : '-'; + + rows.push({ + cells: [ + pvc.metadata?.name, + result.getNamespace(), + result.getCluster(), + status, + volume, + capacity, + accessMode, + storageClass, + age, + ], + props: pvc, + }); + } + } + + return rows; + }, + scope: 'Namespaced', + title: 'Persistent Volume Claims', + }, + persistentvolumes: { + columns: [ + 'Name', + 'Cluster', + 'Capacity', + 'Access Modes', + 'Reclaim Policy', + 'Status', + 'Claim', + 'Storage Class', + 'Reason', + 'Age', + ], + description: '', + isCRD: false, + path: '/api/v1', + resource: 'persistentvolumes', + rows: (results: ProtoResources[]): IRow[] => { + const rows: IRow[] = []; + + for (const result of results) { + const persistentVolumeList: V1PersistentVolumeList = JSON.parse(result.getResourcelist()); + for (const persistentVolume of persistentVolumeList.items) { + const capacity = + persistentVolume.spec && persistentVolume.spec.capacity ? persistentVolume.spec.capacity : ''; + const accessMode = + persistentVolume.spec && persistentVolume.spec.accessModes + ? persistentVolume.spec.accessModes.join(', ') + : ''; + const reclaimPolicy = + persistentVolume.spec && persistentVolume.spec.persistentVolumeReclaimPolicy + ? persistentVolume.spec.persistentVolumeReclaimPolicy + : ''; + const status = persistentVolume.status && persistentVolume.status.phase ? persistentVolume.status.phase : ''; + const claim = + persistentVolume.spec && persistentVolume.spec.claimRef + ? `${persistentVolume.spec.claimRef.namespace}/${persistentVolume.spec.claimRef.name}` + : ''; + const storageClass = + persistentVolume.spec && persistentVolume.spec.storageClassName + ? persistentVolume.spec.storageClassName + : ''; + const reason = + persistentVolume.status && persistentVolume.status.reason ? persistentVolume.status.reason : ''; + const age = + persistentVolume.metadata && persistentVolume.metadata.creationTimestamp + ? timeDifference( + new Date().getTime(), + new Date(persistentVolume.metadata.creationTimestamp.toString()).getTime(), + ) + : '-'; + + rows.push({ + cells: [ + persistentVolume.metadata?.name, + result.getCluster(), + capacity, + accessMode, + reclaimPolicy, + status, + claim, + storageClass, + reason, + age, + ], + props: persistentVolume, + }); + } + } + + return rows; + }, + scope: 'Cluster', + title: 'Persistent Volumes', + }, + poddisruptionbudgets: { + columns: ['Name', 'Namespace', 'Cluster', 'Min. Available', 'Max. Unavailable', 'Allowed Disruptions', 'Age'], + description: '', + isCRD: false, + path: '/apis/policy/v1beta1', + resource: 'poddisruptionbudgets', + rows: (results: ProtoResources[]): IRow[] => { + const rows: IRow[] = []; + + for (const result of results) { + const pdbList: V1beta1PodDisruptionBudgetList = JSON.parse(result.getResourcelist()); + for (const pdb of pdbList.items) { + const minAvailable = pdb.spec && pdb.spec.minAvailable ? pdb.spec.minAvailable : ''; + const maxUnavailable = pdb.spec && pdb.spec.maxUnavailable ? pdb.spec.maxUnavailable : ''; + const allowedDisruptions = pdb.status && pdb.status.disruptionsAllowed ? pdb.status.disruptionsAllowed : ''; + const age = + pdb.metadata && pdb.metadata.creationTimestamp + ? timeDifference(new Date().getTime(), new Date(pdb.metadata.creationTimestamp.toString()).getTime()) + : '-'; + + rows.push({ + cells: [ + pdb.metadata?.name, + result.getNamespace(), + result.getCluster(), + minAvailable, + maxUnavailable, + allowedDisruptions, + age, + ], + props: pdb, + }); + } + } + + return rows; + }, + scope: 'Namespaced', + title: 'Pod Disruption Budgets', + }, + secrets: { + columns: ['Name', 'Namespace', 'Cluster', 'Type', 'Data', 'Age'], + description: '', + isCRD: false, + path: '/api/v1', + resource: 'secrets', + rows: (results: ProtoResources[]): IRow[] => { + const rows: IRow[] = []; + + for (const result of results) { + const secretList: V1SecretList = JSON.parse(result.getResourcelist()); + for (const secret of secretList.items) { + const type = secret.type ? secret.type : ''; + const data = secret.data ? Object.keys(secret.data).length : ''; + const age = + secret.metadata && secret.metadata.creationTimestamp + ? timeDifference(new Date().getTime(), new Date(secret.metadata.creationTimestamp.toString()).getTime()) + : '-'; + + rows.push({ + cells: [secret.metadata?.name, result.getNamespace(), result.getCluster(), type, data, age], + props: secret, + }); + } + } + + return rows; + }, + scope: 'Namespaced', + title: 'Secrets', + }, + serviceaccounts: { + columns: ['Name', 'Namespace', 'Cluster', 'Secrets', 'Age'], + description: '', + isCRD: false, + path: '/api/v1', + resource: 'serviceaccounts', + rows: (results: ProtoResources[]): IRow[] => { + const rows: IRow[] = []; + + for (const result of results) { + const serviceAccountList: V1ServiceAccountList = JSON.parse(result.getResourcelist()); + for (const serviceAccount of serviceAccountList.items) { + const secrets = serviceAccount.secrets ? serviceAccount.secrets.length : ''; + const age = + serviceAccount.metadata && serviceAccount.metadata.creationTimestamp + ? timeDifference( + new Date().getTime(), + new Date(serviceAccount.metadata.creationTimestamp.toString()).getTime(), + ) + : '-'; + + rows.push({ + cells: [serviceAccount.metadata?.name, result.getNamespace(), result.getCluster(), secrets, age], + props: serviceAccount, + }); + } + } + + return rows; + }, + scope: 'Namespaced', + title: 'Service Accounts', + }, + storageclasses: { + columns: [ + 'Name', + 'Cluster', + 'Provisioner', + 'Reclaim Policy', + 'Volume Binding Mode', + 'Allow Volume Expansion', + 'Age', + ], + description: '', + isCRD: false, + path: '/apis/storage.k8s.io/v1', + resource: 'storageclasses', + rows: (results: ProtoResources[]): IRow[] => { + const rows: IRow[] = []; + + for (const result of results) { + const storageClassList: V1StorageClassList = JSON.parse(result.getResourcelist()); + for (const storageClass of storageClassList.items) { + const provisioner = storageClass.provisioner; + const reclaimPolicy = storageClass.reclaimPolicy ? storageClass.reclaimPolicy : ''; + const volumeBindingMode = storageClass.volumeBindingMode ? storageClass.volumeBindingMode : ''; + const allowVolumeExpansion = storageClass.allowVolumeExpansion ? 'true' : 'false'; + const age = + storageClass.metadata && storageClass.metadata.creationTimestamp + ? timeDifference( + new Date().getTime(), + new Date(storageClass.metadata.creationTimestamp.toString()).getTime(), + ) + : '-'; + + rows.push({ + cells: [ + storageClass.metadata?.name, + result.getCluster(), + provisioner, + reclaimPolicy, + volumeBindingMode, + allowVolumeExpansion, + age, + ], + props: storageClass, + }); + } + } + + return rows; + }, + scope: 'Cluster', + title: 'Storage Classes', + }, + // eslint-disable-next-line sort-keys + clusterrolebindings: { + columns: ['Name', 'Cluster', 'Age'], + description: '', + isCRD: false, + path: '/apis/rbac.authorization.k8s.io/v1', + resource: 'clusterrolebindings', + rows: (results: ProtoResources[]): IRow[] => { + const rows: IRow[] = []; + + for (const result of results) { + const clusterRoleBindingsList: V1ClusterRoleBindingList = JSON.parse(result.getResourcelist()); + for (const clusterRoleBindings of clusterRoleBindingsList.items) { + const age = + clusterRoleBindings.metadata && clusterRoleBindings.metadata.creationTimestamp + ? timeDifference( + new Date().getTime(), + new Date(clusterRoleBindings.metadata.creationTimestamp.toString()).getTime(), + ) + : '-'; + + rows.push({ + cells: [clusterRoleBindings.metadata?.name, result.getCluster(), age], + props: clusterRoleBindings, + }); + } + } + + return rows; + }, + scope: 'Cluster', + title: 'Cluster Role Bindings', + }, + clusterroles: { + columns: ['Name', 'Cluster', 'Age'], + description: '', + isCRD: false, + path: '/apis/rbac.authorization.k8s.io/v1', + resource: 'clusterroles', + rows: (results: ProtoResources[]): IRow[] => { + const rows: IRow[] = []; + + for (const result of results) { + const clusterRoleList: V1ClusterRoleList = JSON.parse(result.getResourcelist()); + for (const clusterRole of clusterRoleList.items) { + const age = + clusterRole.metadata && clusterRole.metadata.creationTimestamp + ? timeDifference( + new Date().getTime(), + new Date(clusterRole.metadata.creationTimestamp.toString()).getTime(), + ) + : '-'; + + rows.push({ + cells: [clusterRole.metadata?.name, result.getCluster(), age], + props: clusterRole, + }); + } + } + + return rows; + }, + scope: 'Cluster', + title: 'Cluster Roles', + }, + rolebindings: { + columns: ['Name', 'Namespace', 'Cluster', 'Age'], + description: '', + isCRD: false, + path: '/apis/rbac.authorization.k8s.io/v1', + resource: 'rolebindings', + rows: (results: ProtoResources[]): IRow[] => { + const rows: IRow[] = []; + + for (const result of results) { + const roleBindingList: V1RoleBindingList = JSON.parse(result.getResourcelist()); + for (const roleBinding of roleBindingList.items) { + const age = + roleBinding.metadata && roleBinding.metadata.creationTimestamp + ? timeDifference( + new Date().getTime(), + new Date(roleBinding.metadata.creationTimestamp.toString()).getTime(), + ) + : '-'; + + rows.push({ + cells: [roleBinding.metadata?.name, result.getNamespace(), result.getCluster(), age], + props: roleBinding, + }); + } + } + + return rows; + }, + scope: 'Namespaced', + title: 'Role Bindings', + }, + roles: { + columns: ['Name', 'Namespace', 'Cluster', 'Age'], + description: '', + isCRD: false, + path: '/apis/rbac.authorization.k8s.io/v1', + resource: 'roles', + rows: (results: ProtoResources[]): IRow[] => { + const rows: IRow[] = []; + + for (const result of results) { + const roleList: V1RoleList = JSON.parse(result.getResourcelist()); + for (const role of roleList.items) { + const age = + role.metadata && role.metadata.creationTimestamp + ? timeDifference(new Date().getTime(), new Date(role.metadata.creationTimestamp.toString()).getTime()) + : '-'; + + rows.push({ + cells: [role.metadata?.name, result.getNamespace(), result.getCluster(), age], + props: role, + }); + } + } + + return rows; + }, + scope: 'Namespaced', + title: 'Roles', + }, + // eslint-disable-next-line sort-keys + events: { + columns: ['Name', 'Namespace', 'Cluster', 'Last Seen', 'Type', 'Reason', 'Object', 'Message'], + description: '', + isCRD: false, + path: '/api/v1', + resource: 'events', + rows: (results: ProtoResources[]): IRow[] => { + const rows: IRow[] = []; + + for (const result of results) { + const eventList: CoreV1EventList = JSON.parse(result.getResourcelist()); + for (const event of eventList.items) { + rows.push({ + cells: [ + event.metadata?.name, + result.getNamespace(), + result.getCluster(), + event.lastTimestamp + ? timeDifference(new Date().getTime(), new Date(event.lastTimestamp.toString()).getTime()) + : '-', + event.type, + event.reason, + `${event.involvedObject.kind}/${event.involvedObject.name}`, + event.message, + ], + props: event, + }); + } + } + + return rows; + }, + scope: 'Namespaced', + title: 'Events', + }, + nodes: { + columns: ['Name', 'Cluster', 'Status', 'Version', 'Age'], + description: '', + isCRD: false, + path: '/api/v1', + resource: 'nodes', + rows: (results: ProtoResources[]): IRow[] => { + const rows: IRow[] = []; + + for (const result of results) { + const nodeList: V1NodeList = JSON.parse(result.getResourcelist()); + for (const node of nodeList.items) { + const status: string[] = []; + if (node.status && node.status.conditions) { + for (const condition of node.status.conditions) { + if (condition.status === 'True') { + status.push(condition.type); + } + } + } + + const version = + node.status && node.status.nodeInfo && node.status.nodeInfo.kubeletVersion + ? node.status.nodeInfo.kubeletVersion + : ''; + const age = + node.metadata && node.metadata.creationTimestamp + ? timeDifference(new Date().getTime(), new Date(node.metadata.creationTimestamp.toString()).getTime()) + : '-'; + + rows.push({ + cells: [node.metadata?.name, result.getCluster(), status.join(', '), version, age], + props: node, + }); + } + } + + return rows; + }, + scope: 'Cluster', + title: 'Nodes', + }, + podsecuritypolicies: { + columns: [ + 'Name', + 'Cluster', + 'Privileged', + 'Capabilities', + 'SELinux', + 'Run As User', + 'FS Group', + 'Supplemental Groups', + 'Read Only Root FS', + 'Volumes', + 'Age', + ], + description: '', + isCRD: false, + path: '/apis/policy/v1beta1', + resource: 'podsecuritypolicies', + rows: (results: ProtoResources[]): IRow[] => { + const rows: IRow[] = []; + + for (const result of results) { + const pspList: V1beta1PodSecurityPolicyList = JSON.parse(result.getResourcelist()); + for (const psp of pspList.items) { + const privileged = psp.spec && psp.spec.privileged ? 'true' : 'false'; + const capabilities = psp.spec && psp.spec.allowedCapabilities ? psp.spec.allowedCapabilities.join(', ') : ''; + const seLinux = psp.spec && psp.spec.seLinux && psp.spec.seLinux.rule ? psp.spec.seLinux.rule : ''; + const runAsUser = psp.spec && psp.spec.runAsUser && psp.spec.runAsUser.rule ? psp.spec.runAsUser.rule : ''; + const fsGroup = psp.spec && psp.spec.fsGroup && psp.spec.fsGroup.rule ? psp.spec.fsGroup.rule : ''; + const supplementalGroups = + psp.spec && psp.spec.supplementalGroups && psp.spec.supplementalGroups.rule + ? psp.spec.supplementalGroups.rule + : ''; + const readOnlyRootFS = psp.spec && psp.spec.readOnlyRootFilesystem ? 'true' : 'false'; + const volumes = psp.spec && psp.spec.volumes ? psp.spec.volumes.join(', ') : ''; + const age = + psp.metadata && psp.metadata.creationTimestamp + ? timeDifference(new Date().getTime(), new Date(psp.metadata.creationTimestamp.toString()).getTime()) + : '-'; + + rows.push({ + cells: [ + psp.metadata?.name, + result.getCluster(), + privileged, + capabilities, + seLinux, + runAsUser, + fsGroup, + supplementalGroups, + readOnlyRootFS, + volumes, + age, + ], + props: psp, + }); + } + } + + return rows; + }, + scope: 'Cluster', + title: 'Pod Security Policies', + }, }; // customResourceDefinition merges the standard Kubernetes resources with a list of Custom Resource Definitions. The diff --git a/docs/resources/applications.md b/docs/resources/applications.md index a4713c322..9fe0b89dd 100644 --- a/docs/resources/applications.md +++ b/docs/resources/applications.md @@ -80,6 +80,11 @@ spec: | kinds | []string | A list of the resource types, which should be loaded by the specified selector. | Yes | | selector | string | A label selector, which is used to load the correct resources (e.g. `app=reviews`). | Yes | +!!! note + The following strings can be used as kinds: *cronjobs*, *daemonsets*, *deployments*, *jobs*, *pods*, *replicasets*, *statefulsets*, *endpoints*, *horizontalpodautoscalers*, *ingresses*, *networkpolicies*, *services*, *configmaps*, *persistentvolumeclaims*, *persistentvolumes*, *poddisruptionbudgets*, *secrets*, *serviceaccounts*, *storageclasses*, *clusterrolebindings*, *clusterroles*, *rolebindings*, *roles*, *events*, *nodes*, *podsecuritypolicies*. + + A Custom Resource can be specified in the following form `./` (e.g. `vaultsecrets.ricoberger.de/v1alpha1`). + ## Example The following Application CR is used in the [demo](../installation/demo.md) to display the resources, metrics, logs and traces for the reviews service of the Bookinfo Application.