Skip to content

Commit

Permalink
feat: custom busola extensions (#3523)
Browse files Browse the repository at this point in the history
* feat: add proxyhandler to backend

* feat: adjust busola code to handle custom extensions

* feat: added example custom extension

* feat: rename folder

* feat: add docs

* feat: update docs

* feat: update docs

* feat: add more specific types

* feat: adjust to review comments

* adjust to comments
  • Loading branch information
chriskari authored Dec 12, 2024
1 parent 38a1415 commit 463d5a9
Show file tree
Hide file tree
Showing 19 changed files with 486 additions and 6 deletions.
10 changes: 6 additions & 4 deletions backend/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,12 @@ function extractHeadersData(req) {
const clientCAHeader = 'x-client-certificate-data';
const clientKeyDataHeader = 'x-client-key-data';
const authorizationHeader = 'x-k8s-authorization';

const targetApiServer = handleDockerDesktopSubsitution(
new URL(req.headers[urlHeader]),
);
let targetApiServer;
if (req.headers[urlHeader]) {
targetApiServer = handleDockerDesktopSubsitution(
new URL(req.headers[urlHeader]),
);
}
const ca = decodeHeaderToBuffer(req.headers[caHeader]) || certs;
const cert = decodeHeaderToBuffer(req.headers[clientCAHeader]);
const key = decodeHeaderToBuffer(req.headers[clientKeyDataHeader]);
Expand Down
7 changes: 6 additions & 1 deletion backend/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { makeHandleRequest, serveStaticApp, serveMonaco } from './common';
import { proxyHandler } from './proxy.js';
import { handleTracking } from './tracking.js';
import jsyaml from 'js-yaml';
//import { requestLogger } from './utils/other'; //uncomment this to log the outgoing traffic
Expand Down Expand Up @@ -53,6 +54,8 @@ if (process.env.NODE_ENV === 'development') {
app.use(cors({ origin: '*' }));
}

app.get('/proxy', proxyHandler);

let server = null;

if (
Expand Down Expand Up @@ -81,13 +84,15 @@ const isDocker = process.env.IS_DOCKER === 'true';
const handleRequest = makeHandleRequest();

if (isDocker) {
// Running in dev mode
// yup, order matters here
serveMonaco(app);
app.use('/backend', handleRequest);
serveStaticApp(app, '/', '/core-ui');
} else {
// Running in prod mode
handleTracking(app);
app.use(handleRequest);
app.use('/backend', handleRequest);
}

process.on('SIGINT', function() {
Expand Down
45 changes: 45 additions & 0 deletions backend/proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { request as httpsRequest } from 'https';
import { request as httpRequest } from 'http';
import { URL } from 'url';

async function proxyHandler(req, res) {
const targetUrl = req.query.url;
if (!targetUrl) {
return res.status(400).send('Target URL is required as a query parameter');
}

try {
const parsedUrl = new URL(targetUrl);
const isHttps = parsedUrl.protocol === 'https:';
const libRequest = isHttps ? httpsRequest : httpRequest;

const options = {
hostname: parsedUrl.hostname,
port: parsedUrl.port || (isHttps ? 443 : 80),
path: parsedUrl.pathname + parsedUrl.search,
method: req.method,
headers: { ...req.headers, host: parsedUrl.host },
};

const proxyReq = libRequest(options, proxyRes => {
// Forward status and headers from the target response
res.writeHead(proxyRes.statusCode, proxyRes.headers);
// Pipe the response data from the target back to the client
proxyRes.pipe(res);
});

proxyReq.on('error', () => {
res.status(500).send('An error occurred while making the proxy request.');
});

if (Buffer.isBuffer(req.body)) {
proxyReq.end(req.body); // If the body is already buffered, use it directly.
} else {
req.pipe(proxyReq); // Otherwise, pipe the request for streamed or chunked data.
}
} catch (error) {
res.status(500).send('An error occurred while processing the request.');
}
}

export { proxyHandler };
2 changes: 2 additions & 0 deletions docs/extensibility/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

With Busola's extensibility feature, you can create a dedicated user interface (UI) page for your CustomResourceDefinition (CRD). It enables you to add navigation nodes, on cluster or namespace level, and to configure your [UI display](./30-details-summary.md), for example, a resource list page, and details pages. You can also [create and edit forms](./40-form-fields.md). To create a UI component, you need a ConfigMap.

You can also leverage Busola's [custom extension feature](./custom-extensions.md) to design entirely custom user interfaces tailored to your specific needs.

## Create a ConfigMap for Your UI

To create a ConfigMap with your CRD's UI configuration, you can either use the Extensions feature or do it manually.
Expand Down
24 changes: 24 additions & 0 deletions docs/extensibility/custom-extensions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Custom Extensions

Busola's custom extension feature allows you to design fully custom user interfaces beyond the built-in extensibility functionality. This feature is ideal for creating unique and specialized displays not covered by the built-in components.

## Getting Started

To enable the custom extension feature, you must set the corresponding feature flag in your Busola config, which is disabled by default.

```yaml
EXTENSIBILITY_CUSTOM_COMPONENTS:
isEnabled: true
```
## Creating Custom Extensions
Creating a custom extension is as straightforward as setting up a ConfigMap with the following sections:
- `data.general`: Contains configuration details
- `data.customHtml`: Defines static HTML content
- `data.customScript`: Adds dynamic behavior to your extension.

Once your ConfigMap is ready, add it to your cluster, and Busola will load and display your custom UI.

See this [example](./../../examples/custom-extension/README.md), to learn more.
9 changes: 9 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ EXTENSIBILITY:
isEnabled: true
```
- **EXTENSIBILITY_CUSTOM_COMPONENTS** - is used to indicate whether entirely custom extensions can be added to Busola. See [this example](../examples/custom-extension/README.md).
Default settings:
```yaml
EXTENSIBILITY_CUSTOM_COMPONENTS:
isEnabled: false
```
- **EXTERNAL_NODES** - a list of links to external websites. `category`: a category name, `icon`: an optional icon, `scope`: either `namespace` or `cluster` (defaults to `cluster`), `children`: a list of pairs (label and link).

Default settings:
Expand Down
44 changes: 44 additions & 0 deletions examples/custom-extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Set Up Your Custom Busola Extension

This example contains a basic custom extension that queries all deployments of a selected namespace of your cluster. Additionally, it retrieves the current weather data for Munich, Germany, from an external weather API.

To set up and deploy your own custom Busola extension, follow these steps.

1. Adjust the static HTML content.

Edit the `ui.html` file to define the static HTML content for your custom extension.

2. Configure dynamic components.

Set up dynamic or behavioral components by modifying the custom element defined in the `script.js` file.

- **Accessing Kubernetes resources**: Use the `fetchWrapper` function to interact with cluster resources through the Kubernetes API.

- **Making external API requests**: Use the `proxyFetch` function to handle requests to external APIs that are subject to CORS regulations.

3. Define extension metadata

Update the `general.yaml` file to define metadata for your custom extension.

> [! WARNING]
> Ensure that the `general.customElement` property matches the name of the custom element defined in `script.js`. The script is loaded only once, and this property is used to determine whether the custom element is already defined.
4. Deploy your extension

Before running the deployment command, ensure that your `kubeconfig` is correctly exported and points to the desired cluster. You can check the current context by running:

```bash
kubectl config current-context
```

Run `./deploy-custom-extension.sh` to create a ConfigMap and deploy it to your cluster

Alternatively, you can use the following command:

```bash
kubectl kustomize . | kubectl apply -n kyma-system -f -
```

### 5. Test your changes locally

Run `npm start` to start the development server.
4 changes: 4 additions & 0 deletions examples/custom-extension/deploy-custom-extension.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash

kubectl kustomize . > ./custom-ui.yaml
kubectl apply -f ./custom-ui.yaml -n kyma-system
10 changes: 10 additions & 0 deletions examples/custom-extension/general.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
resource:
kind: Secret
version: v1
urlPath: custom-busola-extension-example
category: Kyma
name: Custom busola extension example
scope: cluster
customElement: my-custom-element
description: >-
Custom busola extension example
11 changes: 11 additions & 0 deletions examples/custom-extension/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
configMapGenerator:
- name: custom-ui
files:
- customHtml=ui.html
- customScript=script.js
- general=general.yaml
options:
disableNameSuffixHash: true
labels:
busola.io/extension: 'resource'
busola.io/extension-version: '0.5'
Loading

0 comments on commit 463d5a9

Please sign in to comment.