Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
app/src/proto/* linguist-vendored
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan
- [#6](https://github.com/kobsio/kobs/pull/6): Add Prometheus as datasource for Application metrics.
- [#8](https://github.com/kobsio/kobs/pull/8): Add new page to directly query a configured Prometheus datasource.
- [#10](https://github.com/kobsio/kobs/pull/10): Add Elasticsearch as datasource for Application logs.
- [#12](https://github.com/kobsio/kobs/pull/12): :warning: *Breaking change:* :warning: Add plugin system and readd Prometheus and Elasticsearch as plugins.

### Fixed

Expand Down
145 changes: 145 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- [Server](#server)
- [Envoy](#envoy)
- [Run kobs](#run-kobs)
- [Add a new Plugin](#add-a-new-plugin)

Every contribution to kobs is welcome, whether it is reporting a bug, submitting a fix, proposing new features or becoming a maintainer. To make contributing to kubenav as easy as possible you will find more details for the development flow in this documentation.

Expand Down Expand Up @@ -151,3 +152,147 @@ docker run -it --rm --name envoy -p 15222:15222 -v $(pwd)/deploy/docker/envoy/en
```

When you want to run kobs inside your Kubernetes cluster, please checkout the Documentation at [kobs.io](https://kobs.io).

## Add a new Plugin

To add a new plugin to kobs, you have to create a `proto/<PLUGIN-NAME>.proto`. Our [Makefile](./Makefile) will the handle the code generation for your plugin.

```protobuf
syntax = "proto3";
package plugins.<PLUGIN-NAME>;

option go_package = "github.com/kobsio/kobs/pkg/api/plugins/<PLUGIN-NAME>/proto";
```

To add your plugin to the Application CRD, add a corresponding field to the `Plugin` message format in the `proto/plugins.proto` file:

```protobuf
syntax = "proto3";
package plugins;

option go_package = "github.com/kobsio/kobs/pkg/api/plugins/plugins/proto";

import "<PLUGIN-NAME>.proto";

message Plugin {
<PLUGIN-NAME>.Spec <PLUGIN-NAME> = 1;
}
```

Besides the protocol buffers definition your also have to create a `pkg/api/plugins/<PLUGIN-NAME>/<PLUGIN-NAME>.go` file, which implements your definition and handles the registration of your plugin. To register your plugin you have to modify the `Register` function in the `pkg/api/plugins/plugins/plugins.go` file:

```go
package plugins

import (
"github.com/kobsio/kobs/pkg/api/plugins/<PLUGIN-NAME>"
)

func Register(cfg *config.Config, grpcServer *grpc.Server) error {
<PLUGIN-NAME>Instances, err := <PLUGIN-NAME>.Register(cfg.<PLUGIN-NAME>, grpcServer)
if err != nil {
log.WithError(err).WithFields(logrus.Fields{"plugin": "<PLUGIN-NAME>"}).Errorf("Failed to register <PLUGIN-NAME> plugin.")
return err
}

plugins = append(plugins, <PLUGIN-NAME>Instances...)
}
```

The configuration for your plugin must be added to the `Config` struct in the `pkg/config/config.go` file:

```go
package config

import (
"github.com/kobsio/kobs/pkg/api/plugins/<PLUGIN-NAME>"
)

type Config struct {
<PLUGIN-NAME> []<PLUGIN-NAME>.Config `yaml:"<PLUGIN-NAME>"`
}
```

Now your plugin is registered at the gRPC server and can be configured via a `config.yaml` file. In the next step you can implement the Reac UI components for your plugin. Your plugin must provide the following two components as entry point: `app/src/plugins/<PLUGIN-NAME>/<PLUGIN-NAME>Page.tsx` and `app/src/plugins/<PLUGIN-NAME>/<PLUGIN-NAME>Plugin.tsx`:

```tsx
import {
PageSection,
PageSectionVariants,
Title,
} from '@patternfly/react-core';
import React from 'react';

import { IPluginPageProps } from 'utils/plugins';

const <PLUGIN-NAME>Page: React.FunctionComponent<IPluginPageProps> = ({ name, description }: IPluginPageProps) => {
return (
<React.Fragment>
<PageSection variant={PageSectionVariants.light}>
<Title headingLevel="h6" size="xl">
{name}
</Title>
<p>{description}</p>
</PageSection>
</React.Fragment>
);
};

export default <PLUGIN-NAME>Page;
```

```tsx
import React from 'react';
import ListIcon from '@patternfly/react-icons/dist/js/icons/list-icon';

import { IPluginProps } from 'utils/plugins';
import PluginDataMissing from 'components/plugins/PluginDataMissing';

const <PLUGIN-NAME>Plugin: React.FunctionComponent<IPluginProps> = ({
isInDrawer,
name,
description,
plugin,
showDetails,
}: IPluginProps) => {
if (!plugin.<PLUGIN-NAME>) {
return (
<PluginDataMissing
title="<PLUGIN-NAME> properties are missing"
description="The <PLUGIN-NAME> properties are missing in your CR for this application. Visit the documentation to learn more on how to use the <PLUGIN-NAME> plugin in an Application CR."
documentation="https://kobs.io"
icon={ListIcon}
/>
);
}

return (
<React.Fragment>
</React.Fragment>
);
};

export default <PLUGIN-NAME>Plugin;
```

In the last step you have to register these two React components in the `app/src/utils/plugins.tsx` file:

```tsx
import React from 'react';

import <PLUGIN-NAME>Page from 'plugins/<PLUGIN-NAME>/<PLUGIN-NAME>Page';
import <PLUGIN-NAME>Plugin from 'plugins/<PLUGIN-NAME>/<PLUGIN-NAME>Plugin';

export const plugins: IPlugins = {
<PLUGIN-NAME>: {
page: <PLUGIN-NAME>Page,
plugin: <PLUGIN-NAME>Plugin,
},
};
```

Thats it, now you can generate the Go and TypeScript code from your `.proto` file and the new Application CRD with the following command:

```sh
make generate
```
9 changes: 4 additions & 5 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@
],
"extends": [
"react-app",
"prettier",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"prettier/@typescript-eslint",
"plugin:prettier/recommended"
],
"rules": {
Expand Down Expand Up @@ -140,13 +139,13 @@
]
},
"dependencies": {
"@kubernetes/client-node": "^0.13.2",
"@kubernetes/client-node": "^0.14.0",
"@patternfly/patternfly": "^4.80.3",
"@patternfly/react-charts": "^6.14.2",
"@patternfly/react-core": "^4.90.2",
"@patternfly/react-table": "^4.20.15",
"@types/google-protobuf": "^3.7.4",
"@types/node": "^12.0.0",
"@types/node": "^14.14.35",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-router-dom": "^5.1.7",
Expand All @@ -167,7 +166,7 @@
"@typescript-eslint/parser": "^4.15.1",
"babel-eslint": "^10.1.0",
"eslint": "^7.20.0",
"eslint-config-prettier": "^7.2.0",
"eslint-config-prettier": "^8.1.0",
"eslint-config-react-app": "^6.0.0",
"eslint-plugin-flowtype": "^5.2.0",
"eslint-plugin-import": "^2.22.1",
Expand Down
Binary file added app/public/img/plugins/kobs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/public/img/plugins/kubernetes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/public/img/plugins/plugins.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 15 additions & 10 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import Application from 'components/applications/Application';
import Applications from 'components/applications/Applications';
import { ClustersContextProvider } from 'context/ClustersContext';
import Home from 'components/Home';
import Plugins from 'components/plugins/PluginPage';
import { PluginsContextProvider } from 'context/PluginsContext';
import Resources from 'components/resources/Resources';

import 'app.css';
Expand All @@ -23,16 +25,19 @@ const App: React.FunctionComponent = () => {

return (
<ClustersContextProvider>
<Router>
<Page header={Header}>
<Switch>
<Route exact={true} path="/" component={Home} />
<Route exact={true} path="/applications" component={Applications} />
<Route exact={true} path="/applications/:cluster/:namespace/:name" component={Application} />
<Route exact={true} path="/resources" component={Resources} />
</Switch>
</Page>
</Router>
<PluginsContextProvider>
<Router>
<Page header={Header}>
<Switch>
<Route exact={true} path="/" component={Home} />
<Route exact={true} path="/applications" component={Applications} />
<Route exact={true} path="/applications/:cluster/:namespace/:name" component={Application} />
<Route exact={true} path="/resources" component={Resources} />
<Route exact={true} path="/plugins/:name" component={Plugins} />
</Switch>
</Page>
</Router>
</PluginsContextProvider>
</ClustersContextProvider>
);
};
Expand Down
1 change: 1 addition & 0 deletions app/src/components/Editor.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useRef } from 'react';
import AceEditor from 'react-ace';

import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/mode-yaml';
import 'ace-builds/src-noconflict/theme-nord_dark';

Expand Down
41 changes: 38 additions & 3 deletions app/src/components/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Gallery, GalleryItem, PageSection, PageSectionVariants } from '@patternfly/react-core';
import React from 'react';
import React, { useContext } from 'react';

import { IPluginsContext, PluginsContext } from 'context/PluginsContext';
import { applicationsDescription, resourcesDescription } from 'utils/constants';
import HomeItem from 'components/HomeItem';

Expand All @@ -9,15 +10,49 @@ import HomeItem from 'components/HomeItem';
// The items for the gallery should always use the HomeItem component, this will render a card, which are selectable. By
// a click on the item the user is navigated to the corresponding page.
const Home: React.FunctionComponent = () => {
const pluginsContext = useContext<IPluginsContext>(PluginsContext);

return (
<PageSection variant={PageSectionVariants.default}>
<Gallery hasGutter={true}>
<GalleryItem>
<HomeItem title="Applications" body={applicationsDescription} link="/applications" />
<HomeItem
title="Applications"
body={applicationsDescription}
link="/applications"
icon="/img/plugins/kobs.png"
/>
</GalleryItem>
<GalleryItem>
<HomeItem title="Resources" body={resourcesDescription} link="/resources" />
<HomeItem
title="Resources"
body={resourcesDescription}
link="/resources"
icon="/img/plugins/kubernetes.png"
/>
</GalleryItem>

{pluginsContext.plugins.length === 0 ? (
<GalleryItem>
<HomeItem
title="Plugins"
body="Plugins can be used to extend the kobs. Click on this card to find out more about plugin."
link="https://kobs.io"
icon="/img/plugins/plugins.png"
/>
</GalleryItem>
) : (
pluginsContext.plugins.map((plugin, index) => (
<GalleryItem key={index}>
<HomeItem
title={plugin.name}
body={plugin.description}
link={`/plugins/${plugin.name}`}
icon={`/img/plugins/${plugin.type}.png`}
/>
</GalleryItem>
))
)}
</Gallery>
</PageSection>
);
Expand Down
22 changes: 15 additions & 7 deletions app/src/components/HomeItem.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
import { Card, CardBody, CardTitle } from '@patternfly/react-core';
import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core';
import React from 'react';
import { useHistory } from 'react-router-dom';

// IHomeItemProps is the interface for an item on the home page. Each item contains a title, body and a link.
// IHomeItemProps is the interface for an item on the home page. Each item contains a title, body, link and icon.
interface IHomeItemProps {
body: string;
link: string;
title: string;
icon: string;
}

// HomeItem is used to render an item in the home page. It requires a title, body and a link. When the card is clicked,
// the user is redirected to the provided link.
const HomeItem: React.FunctionComponent<IHomeItemProps> = ({ body, link, title }: IHomeItemProps) => {
// HomeItem is used to render an item in the home page. It requires a title, body, link and icon. When the card is
// clicked, the user is redirected to the provided link.
const HomeItem: React.FunctionComponent<IHomeItemProps> = ({ body, link, title, icon }: IHomeItemProps) => {
const history = useHistory();

const handleClick = (): void => {
history.push(link);
if (link.startsWith('http')) {
window.open(link, '_blank');
} else {
history.push(link);
}
};

return (
<Card style={{ cursor: 'pointer' }} isHoverable={true} onClick={handleClick}>
<CardTitle>{title}</CardTitle>
<CardHeader>
<img src={icon} alt={title} width="27px" style={{ marginRight: '5px' }} />
<CardTitle>{title}</CardTitle>
</CardHeader>
<CardBody>{body}</CardBody>
</Card>
);
Expand Down
Loading