diff --git a/CHANGELOG.md b/CHANGELOG.md index df29cef89..86fad12c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan ### Added - [#81](https://github.com/kobsio/kobs/pull/81): Add markdown plugin, which can be used to render a markdown formatted text in a dashboard panel. +- [#83](https://github.com/kobsio/kobs/pull/83): Extend Kubernetes resource with Teams, Applications and Dashboards via annotations. ### Fixed diff --git a/deploy/demo/bookinfo/details-application.yaml b/deploy/demo/bookinfo/details-application.yaml index 36c53d687..c7cc879f0 100644 --- a/deploy/demo/bookinfo/details-application.yaml +++ b/deploy/demo/bookinfo/details-application.yaml @@ -14,7 +14,7 @@ spec: - title: Application CR link: https://github.com/kobsio/kobs/blob/main/deploy/demo/bookinfo/details-application.yaml teams: - - name: squad-diablo + - name: team-diablo namespace: kobs preview: title: Incoming Success Rate diff --git a/deploy/demo/bookinfo/productpage-application.yaml b/deploy/demo/bookinfo/productpage-application.yaml index 4b27ba21e..37f8a30db 100644 --- a/deploy/demo/bookinfo/productpage-application.yaml +++ b/deploy/demo/bookinfo/productpage-application.yaml @@ -14,11 +14,11 @@ spec: - title: Application CR link: https://github.com/kobsio/kobs/blob/main/deploy/demo/bookinfo/productpage-application.yaml teams: - - name: squad-diablo + - name: team-diablo namespace: kobs - - name: squad-resident-evil + - name: team-resident-evil namespace: kobs - - name: squad-call-of-duty + - name: team-call-of-duty namespace: kobs dependencies: - name: details diff --git a/deploy/demo/bookinfo/productpage.yaml b/deploy/demo/bookinfo/productpage.yaml index 79fc6074c..da1c10476 100644 --- a/deploy/demo/bookinfo/productpage.yaml +++ b/deploy/demo/bookinfo/productpage.yaml @@ -32,6 +32,13 @@ metadata: labels: app: productpage version: v1 + annotations: + kobs.io/teams: | + [{"namespace": "kobs", "name": "team-diablo"}, + {"namespace": "kobs", "name": "team-resident-evil"}, + {"namespace": "kobs", "name": "team-call-of-duty"}] + kobs.io/applications: | + ["name": "productpage"}] spec: replicas: 1 selector: @@ -43,6 +50,16 @@ spec: labels: app: productpage version: v1 + annotations: + kobs.io/teams: | + [{"namespace": "kobs", "name": "team-diablo"}, + {"namespace": "kobs", "name": "team-resident-evil"}, + {"namespace": "kobs", "name": "team-call-of-duty"}] + kobs.io/applications: | + [{"name": "productpage"}] + kobs.io/dashboards: | + [{"namespace": "kobs", "name": "resource-usage", "title": "Resource Usage", "placeholders": {"namespace": "bookinfo", "pod": "$.metadata.name"}}, + {"namespace": "kobs", "name": "pod-logs", "title": "Logs", "placeholders": {"namespace": "bookinfo", "name": "$.metadata.name"}}] spec: serviceAccountName: bookinfo-productpage containers: diff --git a/deploy/demo/bookinfo/ratings-application.yaml b/deploy/demo/bookinfo/ratings-application.yaml index dc91a87e2..62d2ba6a9 100644 --- a/deploy/demo/bookinfo/ratings-application.yaml +++ b/deploy/demo/bookinfo/ratings-application.yaml @@ -14,7 +14,7 @@ spec: - title: Application CR link: https://github.com/kobsio/kobs/blob/main/deploy/demo/bookinfo/ratings-application.yaml teams: - - name: squad-call-of-duty + - name: team-call-of-duty namespace: kobs preview: title: Incoming Success Rate diff --git a/deploy/demo/bookinfo/reviews-application.yaml b/deploy/demo/bookinfo/reviews-application.yaml index ad8ec175c..85d54cd40 100644 --- a/deploy/demo/bookinfo/reviews-application.yaml +++ b/deploy/demo/bookinfo/reviews-application.yaml @@ -14,7 +14,7 @@ spec: - title: Application CR link: https://github.com/kobsio/kobs/blob/main/deploy/demo/bookinfo/reviews-application.yaml teams: - - name: squad-resident-evil + - name: team-resident-evil namespace: kobs dependencies: - name: ratings diff --git a/deploy/demo/kobs/base/dashboards/pod-logs.yaml b/deploy/demo/kobs/base/dashboards/pod-logs.yaml new file mode 100644 index 000000000..19f151cf5 --- /dev/null +++ b/deploy/demo/kobs/base/dashboards/pod-logs.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: kobs.io/v1beta1 +kind: Dashboard +metadata: + name: pod-logs + namespace: kobs +spec: + description: Istio Logs + placeholders: + - name: namespace + description: The Pod namespace + - name: name + description: The Pod name + rows: + - size: -1 + panels: + - title: Pod Logs + colSpan: 12 + plugin: + name: elasticsearch + options: + query: "kubernetes.namespace: {{ .namespace }} AND kubernetes.pod.name: {{ .name }}" + fields: + - "kubernetes.container.name" + - "message" + showChart: true diff --git a/deploy/demo/kobs/base/kustomization.yaml b/deploy/demo/kobs/base/kustomization.yaml index 3804f74a2..12c3be89d 100644 --- a/deploy/demo/kobs/base/kustomization.yaml +++ b/deploy/demo/kobs/base/kustomization.yaml @@ -5,6 +5,7 @@ resources: - ../../../kustomize/kobs - dashboards/istio-http.yaml - dashboards/istio-logs.yaml + - dashboards/pod-logs.yaml - dashboards/resource-usage.yaml - dashboards/resources.yaml - dashboards/traces.yaml diff --git a/docs/resources/assets/resources-annotations.png b/docs/resources/assets/resources-annotations.png new file mode 100644 index 000000000..699568dcc Binary files /dev/null and b/docs/resources/assets/resources-annotations.png differ diff --git a/docs/resources/resources.md b/docs/resources/resources.md index 8b4079c2e..3530cf0b2 100644 --- a/docs/resources/resources.md +++ b/docs/resources/resources.md @@ -21,3 +21,93 @@ If you want to view the Yaml representation of the resource you can select the c Next to the yaml representation, you find a seconde tab events, which shows all events, which are related to the selected object. The events are retrieved with a field selector and the name of the resource: `fieldSelector=involvedObject.name=`. ![Events](assets/resources-events.png) + +## Annotations + +You can extend your resources with additional information for kobs, by using annotations. This allows you to specify teams, applications and dashboards for your Kubernetes objects like Pods, Deployments, etc. + +| Annotations | Format | Description | +| ----------- | ------ | ----------- | +| `kobs.io/teams` | `[{"cluster": "", "namespace": "", "name": ""}, {...}]` | Specify a list of teams. You have to provide the name of the team and an optional cluster / namespace. If the cluster / namespace is not specified, the cluster / namespace of the resource will be used. | +| `kobs.io/applications` | `[{"cluster": "", "namespace": "", "name": ""}, {...}]` | Specify a list of applications. You have to provide the name of the application and an optional cluster / namespace. If the cluster / namespace is not specified, the cluster / namespace of the resource will be used. | +| `kobs.io/dashboards` | `[{"cluster": "", "namespace": "", "name": "", "title": "", "placeholders": {"placeholder1": "", "placeholder2": ""}}, {...}]` | | Specify a list of dashboards. You have to provide the name of the dashboard and an optional cluster / namespace. If the cluster / namespace is not specified, the cluster / namespace of the resource will be used. You can also set the values for placeholders. | + +### Teams + +Specify a list of teams within the `kobs.io/teams` annotation. The list contains an array of teams, where each team is identified by a cluster, namespace and name. If the cluster or namespace isn't set the cluster / namespace of the Kubernetes resource will be used. + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: productpage-v1 + namespace: bookinfo + labels: + app: productpage + version: v1 + annotations: + kobs.io/teams: | + [{"namespace": "kobs", "name": "team-diablo"}, + {"namespace": "kobs", "name": "team-resident-evil"}, + {"namespace": "kobs", "name": "team-call-of-duty"}] +``` + +### Applications + +Specify a list of applications within the `kobs.io/applications` annotation. The list contains an array of applications, where each application is identified by a cluster, namespace and name. If the cluster or namespace isn't set the cluster / namespace of the Kubernetes resource will be used. + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: productpage-v1 + namespace: bookinfo + labels: + app: productpage + version: v1 + annotations: + kobs.io/applications: | + [{"name": "productpage"}] +``` + +### Dashboards + +Specify a list of dashboards within the `kobs.io/dashboards` annotation. The list contains multiple dashboards, as they can be set for [applications](applications.md#dashboard) and [teams](teams.md#dashboard). + +To set the value of a placeholder, you can use a [JSONPath](https://goessner.net/articles/JsonPath/). The JSONPath is run against the resource manifest, so that for example `$.metadata.name` will use the name of the resource as value for a placeholder. + +!!! note + We are using the [jsonpath-plus](https://www.npmjs.com/package/jsonpath-plus) to extract the content from the Kubernetes objects. A list of examples can be found within the documentation of the module. + +The following example adds the `kobs.io/teams`, `kobs.io/applications` and `kobs.io/dashboards` annotation to each Pod of the `productpage-v1` Deployment. The corresponding Pods will then have a dashboard which can be used to view the resource usage of this Pod and the logs for the Pods from Elasticsearch. + +```yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: productpage-v1 + namespace: bookinfo +spec: + selector: + matchLabels: + app: productpage + version: v1 + template: + metadata: + labels: + app: productpage + version: v1 + annotations: + kobs.io/teams: | + [{"namespace": "kobs", "name": "team-diablo"}, + {"namespace": "kobs", "name": "team-resident-evil"}, + {"namespace": "kobs", "name": "team-call-of-duty"}] + kobs.io/applications: | + [{"name": "productpage"}] + kobs.io/dashboards: | + [{"namespace": "kobs", "name": "resource-usage", "title": "Resource Usage", "placeholders": {"namespace": "bookinfo", "pod": "$.metadata.name"}}, + {"namespace": "kobs", "name": "pod-logs", "title": "Logs", "placeholders": {"namespace": "bookinfo", "name": "$.metadata.name"}}] +``` + +![Annotations](assets/resources-annotations.png) diff --git a/plugins/dashboards/src/components/dashboards/Dashboard.tsx b/plugins/dashboards/src/components/dashboards/Dashboard.tsx index cb14de8cb..5c3a7e263 100644 --- a/plugins/dashboards/src/components/dashboards/Dashboard.tsx +++ b/plugins/dashboards/src/components/dashboards/Dashboard.tsx @@ -107,8 +107,8 @@ const Dashboard: React.FunctionComponent = ({ const json = await response.json(); if (response.status >= 200 && response.status < 300) { - if (json && Array.isArray(json) && json.length > 1) { - if (json && json.length > 0 && tmpVariables[i].plugin.options.allowAll) { + if (json && Array.isArray(json) && json.length > 0) { + if (json && json.length > 1 && tmpVariables[i].plugin.options.allowAll) { json.unshift(json.join('|')); } diff --git a/plugins/resources/package.json b/plugins/resources/package.json index 5e772f922..083c6e962 100644 --- a/plugins/resources/package.json +++ b/plugins/resources/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@kobsio/plugin-core": "*", + "@kobsio/plugin-dashboards": "*", "@kubernetes/client-node": "^0.15.0", "@patternfly/react-core": "^4.128.2", "@patternfly/react-icons": "^4.10.11", @@ -19,6 +20,7 @@ "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "@types/react-router-dom": "^5.1.7", + "jsonpath-plus": "^6.0.1", "react": "^17.0.2", "react-dom": "^17.0.2", "react-query": "^3.17.2", diff --git a/plugins/resources/src/components/panel/details/Dashboards.tsx b/plugins/resources/src/components/panel/details/Dashboards.tsx new file mode 100644 index 000000000..71e0c1f69 --- /dev/null +++ b/plugins/resources/src/components/panel/details/Dashboards.tsx @@ -0,0 +1,81 @@ +import { Alert, AlertVariant } from '@patternfly/react-core'; +import { IRow } from '@patternfly/react-table'; +import { JSONPath } from 'jsonpath-plus'; +import React from 'react'; + +import { DashboardsWrapper, IDashboardReference } from '@kobsio/plugin-dashboards'; + +// getDashboards parses the kobs.io/dashboards annotation of a Kubernetes resources and returns all provided dashboards. +// Before we are returning the dashboards we are checking all the provided placeholder and if one of the placeholders +// uses an JSONPath we are replacing it with the correct value. +const getDashboards = (resource: IRow): IDashboardReference[] | undefined => { + try { + if ( + resource.props && + resource.props.metadata && + resource.props.metadata.annotations && + resource.props.metadata.annotations['kobs.io/dashboards'] + ) { + const dashboards: IDashboardReference[] = JSON.parse( + resource.props.metadata.annotations['kobs.io/dashboards'], + resource.props, + ); + + for (let i = 0; i < dashboards.length; i++) { + if (dashboards[i].placeholders) { + for (const key of Object.keys(dashboards[i].placeholders || {})) { + if ( + dashboards[i].placeholders?.hasOwnProperty(key) && + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + dashboards[i].placeholders![key].trim().startsWith('$.') + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + dashboards[i].placeholders![key] = JSONPath({ + json: resource.props, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + path: dashboards[i].placeholders![key].trim(), + wrap: false, + }); + } + } + } + } + + return dashboards; + } + } catch (err) { + return undefined; + } +}; + +interface IDashboardsProps { + resource: IRow; +} + +const Dashboards: React.FunctionComponent = ({ resource }: IDashboardsProps) => { + const dashboards = getDashboards(resource); + + if (!dashboards) { + return ( + +

+ You can add dashboards to your Kubernetes resources by adding a kobs.io/dashboards annotation. +

+
+ ); + } + + return ( + + ); +}; + +export default Dashboards; diff --git a/plugins/resources/src/components/panel/details/Details.tsx b/plugins/resources/src/components/panel/details/Details.tsx index 98c755a51..781b2e11d 100644 --- a/plugins/resources/src/components/panel/details/Details.tsx +++ b/plugins/resources/src/components/panel/details/Details.tsx @@ -14,7 +14,9 @@ import { IRow } from '@patternfly/react-table'; import yaml from 'js-yaml'; import { Editor, Title } from '@kobsio/plugin-core'; +import Dashboards from './Dashboards'; import Events from './Events'; +import Links from './Links'; import Logs from './Logs'; import Overview from './Overview'; import Pods from './Pods'; @@ -66,6 +68,8 @@ const Details: React.FunctionComponent = ({ resource, close }: ID + + setActiveTab(tabIndex.toString())} @@ -121,6 +125,12 @@ const Details: React.FunctionComponent = ({ resource, close }: ID ) : null} + + Dashboards}> +
+ +
+
diff --git a/plugins/resources/src/components/panel/details/Links.tsx b/plugins/resources/src/components/panel/details/Links.tsx new file mode 100644 index 000000000..9c0149828 --- /dev/null +++ b/plugins/resources/src/components/panel/details/Links.tsx @@ -0,0 +1,108 @@ +import { + Button, + ButtonVariant, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, +} from '@patternfly/react-core'; +import { TopologyIcon, UsersIcon } from '@patternfly/react-icons'; +import { IRow } from '@patternfly/react-table'; +import { Link } from 'react-router-dom'; +import React from 'react'; + +import { IPluginDefaults } from '@kobsio/plugin-core'; + +// getTeams parses the kobs.io/teams annotation of a Kubernetes resources and returns all provided teams. +const getTeams = (resource: IRow): IPluginDefaults[] | undefined => { + try { + if ( + resource.props && + resource.props.metadata && + resource.props.metadata.annotations && + resource.props.metadata.annotations['kobs.io/teams'] + ) { + return JSON.parse(resource.props.metadata.annotations['kobs.io/teams'], resource.props); + } + } catch (err) { + return undefined; + } +}; + +// getApplications parses the kobs.io/teams annotation of a Kubernetes resources and returns all provided teams. +const getApplications = (resource: IRow): IPluginDefaults[] | undefined => { + try { + if ( + resource.props && + resource.props.metadata && + resource.props.metadata.annotations && + resource.props.metadata.annotations['kobs.io/applications'] + ) { + return JSON.parse(resource.props.metadata.annotations['kobs.io/applications'], resource.props); + } + } catch (err) { + return undefined; + } +}; + +interface ILinksProps { + resource: IRow; +} + +const Links: React.FunctionComponent = ({ resource }: ILinksProps) => { + const teams = getTeams(resource); + const applications = getApplications(resource); + + if ((applications && applications.length > 0) || (teams && teams.length > 0)) { + return ( +
+ + {applications && applications.length > 0 ? ( + + Applications + + {applications.map((application, index) => ( + + +
+ + ))} +
+
+ ) : null} + {teams && teams.length > 0 ? ( + + Teams + + {teams.map((team, index) => ( + + +
+ + ))} +
+
+ ) : null} +
+
+ ); + } + + return null; +}; + +export default Links;