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 7 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 that go beyond the built-in extensibility functionality. This feature is ideal for creating unique and specialized displays that are not covered by the built-in components.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Busola's custom extension feature allows you to design fully custom user interfaces that go beyond the built-in extensibility functionality. This feature is ideal for creating unique and specialized displays that are not covered by the built-in components.
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

First, to enable the custom extension feature you need to set the corresponding feature flag in your busola config, which is disabled by default.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
First, to enable the custom extension feature you need to set the corresponding feature flag in your busola config, which is disabled by default.
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## Creating custom extensions
## 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.

The best way to get familiar with this mechanism is to have a look at our [example](./../../examples/custom-extension/README.md), where everything is explained in detail.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The best way to get familiar with this mechanism is to have a look at our [example](./../../examples/custom-extension/README.md), where everything is explained in detail.
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. An example for a custom extension can be found [here](../examples/custom-extension/README.md).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- **EXTENSIBILITY_CUSTOM_COMPONENTS** - is used to indicate whether entirely custom extensions can be added to Busola. An example for a custom extension can be found [here](../examples/custom-extension/README.md).
- **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
47 changes: 47 additions & 0 deletions examples/custom-extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Set Up Your Custom Busola Extension

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This example contains a basic custom extension, that queries all deployments of a selected namespace of your cluster, and additionally retrieves the current weather data for Munich, Germany from an external weather API.
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 Static HTML Content
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### 1. Adjust Static HTML Content
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### 2. Configure Dynamic Components
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- **Accessing Kubernetes Resources**: Use the `fetchWrapper` function to interact with cluster resources through the Kubernetes API.
- **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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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


grego952 marked this conversation as resolved.
Show resolved Hide resolved
---
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
---


### 3. Define Extension Metadata
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### 3. Define Extension Metadata
3. Define extension metadata.


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

#### ⚠️ Important:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#### ⚠️ Important:
> [! 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.


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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
---
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
---


### 4. Deploy Your Extension
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### 4. Deploy Your Extension
4. Deploy your extension.


Run `./deploy.sh` to create a ConfigMap and deploy it to your cluster
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To run this command, I need to have exported Kubeconfig to my cluster, please add this information, and also I'll consider changing the name of the sh file to some deploy-custom-extensions or something more specific.


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
---
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
---


### 5. Test Your Changes Locally
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
### 5. Test Your Changes Locally
5. Test your changes locally.


Run `npm start` to start the development server.
4 changes: 4 additions & 0 deletions examples/custom-extension/deploy.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