Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: custom busola extensions #3523

Merged
merged 10 commits into from
Dec 12, 2024
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
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.

grego952 marked this conversation as resolved.
Show resolved Hide resolved
- **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.

grego952 marked this conversation as resolved.
Show resolved Hide resolved
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 -
```

grego952 marked this conversation as resolved.
Show resolved Hide resolved
### 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
Loading