Skip to content

Commit

Permalink
[app] Add Integrations for Kubernetes Resources
Browse files Browse the repository at this point in the history
Add a new integrations feature for kobs. In this first iteration we
implemented the suggested integrations feature from #255 for Kubernetes
Resources.

This integration allows an administrator of kobs to add a set of default
dashboards to Kubernetes Resources. This way we can add for example a
resource usage dashboard to all Pods, without that each Pod must have
the "kobs.io/dashboards" annotation.

The configured dashboards in the integrations and the dashboards from
the "kobs.io/dashboards" annotation are always merged and not
overwritte.
  • Loading branch information
ricoberger committed Jul 22, 2022
1 parent 4490c9f commit 13b86b8
Show file tree
Hide file tree
Showing 20 changed files with 190 additions and 41 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan
- [#394](https://github.com/kobsio/kobs/pull/#394): [app] Allow users and teams to customize their notification settings.
- [#398](https://github.com/kobsio/kobs/pull/#398): [github] Add GitHub plugin to access issues, pull requests, teams and members of an organization.
- [#399](https://github.com/kobsio/kobs/pull/#399): [github] Add new `usernotifications` panel and allow users to use the plugin within the Notifications.
- [#401](https://github.com/kobsio/kobs/pull/#401): [app] Add integrations for Kubernetes Resource, which allows administrators to define a set of default dashboards, which are added to each resource.

### Fixed

Expand Down
27 changes: 21 additions & 6 deletions docs/getting-started/configuration/hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@ satellites:

# The api configuration is optional.
api:
# It is possible to customize the navigation sidebar of kobs. More details can be found on the "Navigation" page in the configuration section of the docs (https://kobs.io/main/getting-started/configuration/navigation/).
navigation:

# It is possible to show notifications within kobs from a configured plugin. More details can be found on the "Notifications" page in the configuration section of the docs (https://kobs.io/main/getting-started/configuration/notifications/).
notifications:

# The resources configuration section can be used to add integrations for Kubernetes Resources. Currently it is possible to add a set of default dashboards for each Kubernetes Resource via the integrations.
resources:
integrations:
dashboards:
# In the following example we are adding a dashboard "resource-usage" from the "kobs" namespace to each Pod.
# The configuration uses the same syntax as it is used in the "kobs.io/dashboards" annotation for resources. See https://kobs.io/main/resources/kubernetes-resources/#dashboards for more information.
# - resource: pods
# dashboard:
# name: resource-usage
# namespace: kobs
# title: Resource Usage
# placeholders:
# namespace: "<% $.metadata.namespace %>"
# pod: "<% $.metadata.name %>"

# The users configuration section can be used to show a list of default dashboards on the users profile page, when the user has not configured his profile page.
users:
defaultDashboards:
Expand All @@ -78,12 +99,6 @@ api:
# plugin:
# type: app
# name: userapplications

# It is possible to customize the navigation sidebar of kobs. More details can be found on the "Navigation" page in the configuration section of the docs (https://kobs.io/main/getting-started/configuration/navigation/).
navigation:

# It is possible to show notifications within kobs from a configured plugin. More details can be found on the "Notifications" page in the configuration section of the docs (https://kobs.io/main/getting-started/configuration/notifications/).
notifications:
```

You can also use environment variables within the configuration file. To use an environment variable you can place the following placeholder in the config file: `${NAME_OF_THE_ENVIRONMENT_VARIABLE}`. When kobs reads the file the placeholder will be replaced, with the value of the environment variable. This allows you to provide confidential data via an environment variable, instead of putting them into the file.
14 changes: 13 additions & 1 deletion pkg/hub/api/api.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
package api

import (
"github.com/kobsio/kobs/pkg/hub/api/applications"
"github.com/kobsio/kobs/pkg/hub/api/clusters"
"github.com/kobsio/kobs/pkg/hub/api/dashboards"
"github.com/kobsio/kobs/pkg/hub/api/navigation"
"github.com/kobsio/kobs/pkg/hub/api/notifications"
"github.com/kobsio/kobs/pkg/hub/api/plugins"
"github.com/kobsio/kobs/pkg/hub/api/resources"
"github.com/kobsio/kobs/pkg/hub/api/teams"
"github.com/kobsio/kobs/pkg/hub/api/users"
)

type Config struct {
Users users.Config `json:"users"`
Applications applications.Config `json:"applications"`
Clusters clusters.Config `json:"clusters"`
Dashboards dashboards.Config `json:"dashboards"`
Navigation navigation.Config `json:"navigation"`
Notifications notifications.Config `json:"notifications"`
Plugins plugins.Config `json:"plugins"`
Resources resources.Config `json:"resources"`
Teams teams.Config `json:"teams"`
Users users.Config `json:"users"`
}
4 changes: 3 additions & 1 deletion pkg/hub/api/applications/applications.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"go.uber.org/zap"
)

type Config struct{}

type Router struct {
*chi.Mux
storeClient store.Client
Expand Down Expand Up @@ -295,7 +297,7 @@ func (router *Router) getApplicationsByTeam(w http.ResponseWriter, r *http.Reque
render.JSON(w, r, data)
}

func Mount(storeClient store.Client) chi.Router {
func Mount(config Config, storeClient store.Client) chi.Router {
router := Router{
chi.NewRouter(),
storeClient,
Expand Down
2 changes: 1 addition & 1 deletion pkg/hub/api/applications/applications_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,6 @@ func TestGetApplicationsByTeam(t *testing.T) {
}

func TestMount(t *testing.T) {
router := Mount(nil)
router := Mount(Config{}, nil)
require.NotNil(t, router)
}
4 changes: 3 additions & 1 deletion pkg/hub/api/clusters/clusters.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"go.uber.org/zap"
)

type Config struct{}

type Router struct {
*chi.Mux
storeClient store.Client
Expand Down Expand Up @@ -72,7 +74,7 @@ func (router *Router) getResources(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, shared.GetResources(crds))
}

func Mount(storeClient store.Client) chi.Router {
func Mount(config Config, storeClient store.Client) chi.Router {
router := Router{
chi.NewRouter(),
storeClient,
Expand Down
2 changes: 1 addition & 1 deletion pkg/hub/api/clusters/clusters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,6 @@ func TestGetResources(t *testing.T) {
}

func TestMount(t *testing.T) {
router := Mount(nil)
router := Mount(Config{}, nil)
require.NotNil(t, router)
}
4 changes: 3 additions & 1 deletion pkg/hub/api/dashboards/dashboards.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"go.uber.org/zap"
)

type Config struct{}

type Router struct {
*chi.Mux
storeClient store.Client
Expand Down Expand Up @@ -86,7 +88,7 @@ func (router *Router) getDashboard(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, dashboard)
}

func Mount(storeClient store.Client) chi.Router {
func Mount(config Config, storeClient store.Client) chi.Router {
router := Router{
chi.NewRouter(),
storeClient,
Expand Down
2 changes: 1 addition & 1 deletion pkg/hub/api/dashboards/dashboards_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,6 @@ func TestGetDashboard(t *testing.T) {
}

func TestMount(t *testing.T) {
router := Mount(nil)
router := Mount(Config{}, nil)
require.NotNil(t, router)
}
4 changes: 3 additions & 1 deletion pkg/hub/api/plugins/plugins.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
"go.uber.org/zap"
)

type Config struct{}

// Router implements the router for the resources plugin, which can be registered in the router for our rest api.
type Router struct {
*chi.Mux
Expand Down Expand Up @@ -87,7 +89,7 @@ func (router *Router) proxyPlugins(w http.ResponseWriter, r *http.Request) {

// Mount returns a chi.Router which can be used to interact with the plugins of the satellites. It returns all the
// loaded plugins from the satellites and proxies the plugin requests to the satellites.
func Mount(satellitesClient satellites.Client, storeClient store.Client) chi.Router {
func Mount(config Config, satellitesClient satellites.Client, storeClient store.Client) chi.Router {
router := Router{
chi.NewRouter(),
satellitesClient,
Expand Down
42 changes: 40 additions & 2 deletions pkg/hub/api/resources/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/kobsio/kobs/pkg/hub/satellites"
"github.com/kobsio/kobs/pkg/hub/store"
"github.com/kobsio/kobs/pkg/hub/store/shared"
dashboardv1 "github.com/kobsio/kobs/pkg/kube/apis/dashboard/v1"
"github.com/kobsio/kobs/pkg/log"
"github.com/kobsio/kobs/pkg/middleware/errresponse"

Expand All @@ -18,10 +19,29 @@ import (
"go.uber.org/zap"
)

// Config is the configuration for the resources API. At the moment it allows users to configure integration, which can
// be used to set a list of default dashboards which should be added to a resource. This is the same as the
// "kobs.io/dashboards" annotation, but allows to set a list of dashboards for all resources of a certain type which are
// matching the specified labels.
type Config struct {
Integrations Integrations `json:"integrations"`
}

type Integrations struct {
Dashboards []Dashboard `json:"dashboards"`
}

type Dashboard struct {
Resource string `json:"resource"`
Labels map[string]string `json:"labels"`
Dashboard dashboardv1.Reference `json:"dashboard"`
}

type ResourceResponse struct {
Resource shared.Resource `json:"resource"`
ResourceLists []ResourceList `json:"resourceLists"`
Errors []string `json:"errors"`
Integrations Integrations `json:"integrations"`
}

type ResourceList struct {
Expand All @@ -30,10 +50,25 @@ type ResourceList struct {
List map[string]any `json:"list"`
}

// Router implements the resources API. It implement a chi.Mux and contains the satellites and store client and the
// configured integration to serve our needs.
type Router struct {
*chi.Mux
satellitesClient satellites.Client
storeClient store.Client
integrations Integrations
}

func (router *Router) getDashboardsFromIntegrationsByID(id string) []Dashboard {
var dashboards []Dashboard

for _, dashboard := range router.integrations.Dashboards {
if dashboard.Resource == id {
dashboards = append(dashboards, dashboard)
}
}

return dashboards
}

func (router *Router) getResources(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -89,6 +124,8 @@ func (router *Router) getResources(w http.ResponseWriter, r *http.Request) {
resource = shared.CRDToResource(*crd)
}

dashboards := router.getDashboardsFromIntegrationsByID(resourceID)

var resourceLists []ResourceList
var errors []string

Expand Down Expand Up @@ -146,7 +183,7 @@ func (router *Router) getResources(w http.ResponseWriter, r *http.Request) {

wgNamespaces.Wait()
muResources.Lock()
resourceResponses = append(resourceResponses, ResourceResponse{Resource: resource, ResourceLists: resourceLists, Errors: errors})
resourceResponses = append(resourceResponses, ResourceResponse{Resource: resource, ResourceLists: resourceLists, Errors: errors, Integrations: Integrations{Dashboards: dashboards}})
muResources.Unlock()
}(resourceID)
}
Expand Down Expand Up @@ -177,11 +214,12 @@ func (router *Router) proxyResources(w http.ResponseWriter, r *http.Request) {
satellite.Proxy(w, r)
}

func Mount(satellitesClient satellites.Client, storeClient store.Client) chi.Router {
func Mount(config Config, satellitesClient satellites.Client, storeClient store.Client) chi.Router {
router := Router{
chi.NewRouter(),
satellitesClient,
storeClient,
config.Integrations,
}

router.Get("/_", router.getResources)
Expand Down
4 changes: 3 additions & 1 deletion pkg/hub/api/teams/teams.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"go.uber.org/zap"
)

type Config struct{}

type Router struct {
*chi.Mux
storeClient store.Client
Expand Down Expand Up @@ -88,7 +90,7 @@ func (router *Router) getTeam(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, team)
}

func Mount(storeClient store.Client) chi.Router {
func Mount(config Config, storeClient store.Client) chi.Router {
router := Router{
chi.NewRouter(),
storeClient,
Expand Down
2 changes: 1 addition & 1 deletion pkg/hub/api/teams/teams_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,6 @@ func TestGetTeam(t *testing.T) {
}

func TestMount(t *testing.T) {
router := Mount(nil)
router := Mount(Config{}, nil)
require.NotNil(t, router)
}
12 changes: 6 additions & 6 deletions pkg/hub/hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,13 @@ func New(config api.Config, debugUsername, debugPassword, hubAddress string, aut
r.Mount("/auth", userauth.Mount(authLogoutRedirect))
r.Mount("/navigation", navigation.Mount(config.Navigation))
r.Mount("/notifications", notifications.Mount(config.Notifications, storeClient))
r.Mount("/clusters", clusters.Mount(storeClient))
r.Mount("/applications", applications.Mount(storeClient))
r.Mount("/teams", teams.Mount(storeClient))
r.Mount("/clusters", clusters.Mount(config.Clusters, storeClient))
r.Mount("/applications", applications.Mount(config.Applications, storeClient))
r.Mount("/teams", teams.Mount(config.Teams, storeClient))
r.Mount("/users", users.Mount(config.Users, storeClient))
r.Mount("/dashboards", dashboards.Mount(storeClient))
r.Mount("/resources", resources.Mount(satellitesClient, storeClient))
r.Mount("/plugins", plugins.Mount(satellitesClient, storeClient))
r.Mount("/dashboards", dashboards.Mount(config.Dashboards, storeClient))
r.Mount("/resources", resources.Mount(config.Resources, satellitesClient, storeClient))
r.Mount("/plugins", plugins.Mount(config.Plugins, satellitesClient, storeClient))
})

return &server{
Expand Down
4 changes: 2 additions & 2 deletions plugins/app/src/components/dashboards/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ const Dashboard: React.FunctionComponent<IDashboardProps> = ({
style={
row.size !== undefined && row.size === -1
? undefined
: { height: rowHeight(row.size, panel.rowSpan), overflow: 'auto' }
: { height: rowHeight(row.size, panel.rowSpan) }
}
>
<PluginPanel
Expand All @@ -171,7 +171,7 @@ const Dashboard: React.FunctionComponent<IDashboardProps> = ({
style={
row.size !== undefined && row.size === -1
? undefined
: { height: rowHeight(row.size, panel.rowSpan), overflow: 'auto' }
: { height: rowHeight(row.size, panel.rowSpan) }
}
></div>
)}
Expand Down
10 changes: 8 additions & 2 deletions plugins/app/src/components/resources/ResourcesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { QueryObserverResult, useQuery } from 'react-query';
import React, { useEffect, useState } from 'react';
import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';

import { IOptions, IResourceResponse } from './utils/interfaces';
import { IIntegrations, IOptions, IResourceResponse } from './utils/interfaces';
import Details from './details/Details';
import { IResource } from '../../resources/clusters';
import { IResourceRow } from './utils/tabledata';
Expand Down Expand Up @@ -141,12 +141,18 @@ const ResourcesPanel: React.FunctionComponent<IResourcesPanelProps> = ({
selectedRow={state.selectedRow}
selectRow={
setDetails
? (rowIndex: number, resource: IResource, resourceData: IResourceRow): void => {
? (
rowIndex: number,
resource: IResource,
resourceData: IResourceRow,
integrations: IIntegrations,
): void => {
setState({ ...state, selectedRow: rowIndex });
setDetails(
<Details
resource={resource}
resourceData={resourceData}
integrations={integrations}
close={(): void => {
setState({ ...state, selectedRow: -1 });
setDetails(undefined);
Expand Down
10 changes: 7 additions & 3 deletions plugins/app/src/components/resources/ResourcesPanelTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TableComposable, TableVariant, Tbody, Td, Th, Thead, Tr } from '@patter
import React from 'react';
import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';

import { IColumn, IResourceResponse } from './utils/interfaces';
import { IColumn, IIntegrations, IResourceResponse } from './utils/interfaces';
import { IResourceRow, customResourceDefinitionTableData, resourcesTableData } from './utils/tabledata';
import { IResource } from '../../resources/clusters';

Expand All @@ -12,7 +12,7 @@ interface IResourcesPanelTableProps {
columns?: IColumn[];
filter?: string;
selectedRow: number;
selectRow?: (rowIndex: number, resource: IResource, resourceData: IResourceRow) => void;
selectRow?: (rowIndex: number, resource: IResource, resourceData: IResourceRow, integrations: IIntegrations) => void;
}

const ResourcesPanelTable: React.FunctionComponent<IResourcesPanelTableProps> = ({
Expand Down Expand Up @@ -81,7 +81,11 @@ const ResourcesPanelTable: React.FunctionComponent<IResourcesPanelTableProps> =
key={rowIndex}
isHoverable={selectRow ? true : false}
isRowSelected={selectedRow === rowIndex}
onClick={(): void => (selectRow ? selectRow(rowIndex, resourceResponse.resource, row) : undefined)}
onClick={(): void =>
selectRow
? selectRow(rowIndex, resourceResponse.resource, row, resourceResponse.integrations)
: undefined
}
>
{row.cells.map((cell, cellIndex) => (
<Td key={cellIndex}>{cell}</Td>
Expand Down
Loading

0 comments on commit 13b86b8

Please sign in to comment.