mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
238 lines
9.3 KiB
Markdown
238 lines
9.3 KiB
Markdown
# Resource Stack (Cluster Feature)
|
||
|
||
A cluster feature is a set of Kubernetes resources that can be applied to and managed within the active cluster.
|
||
The `Renderer.K8sApi.ResourceStack` class provides the functionality to input and apply kubernetes resources to a cluster.
|
||
It is up to the extension developer to manage the life cycle of the resource stack.
|
||
It could be applied automatically to a cluster by the extension, or the end-user could be required to install it.
|
||
|
||
The code examples in this section show how to create a resource stack, and define a cluster feature that is configurable from the cluster **Settings** page.
|
||
|
||
!!! info
|
||
To access the cluster **Settings** page, right-click the relevant cluster in the left side menu and click **Settings**.
|
||
|
||
The resource stack in this example consists of a single kubernetes resource:
|
||
|
||
```yaml
|
||
apiVersion: v1
|
||
kind: Pod
|
||
metadata:
|
||
name: example-pod
|
||
spec:
|
||
containers:
|
||
- name: example-pod
|
||
image: nginx
|
||
```
|
||
|
||
It is simply a pod named `example-pod`, running nginx. Assume this content is in the file `../resources/example-pod.yml`.
|
||
|
||
The following code sample shows how to use the `Renderer.K8sApi.ResourceStack` to manage installing and uninstalling this resource stack:
|
||
|
||
```typescript
|
||
import { Renderer, Common } from "@k8slens/extensions";
|
||
import * as path from "path";
|
||
|
||
const {
|
||
K8sApi: {
|
||
ResourceStack,
|
||
forCluster,
|
||
Pod,
|
||
}
|
||
} = Renderer;
|
||
|
||
type ResourceStack = Renderer.K8sApi.ResourceStack;
|
||
type Pod = Renderer.K8sApi.Pod;
|
||
type KubernetesCluster = Common.Catalog.KubernetesCluster;
|
||
|
||
export class ExampleClusterFeature {
|
||
protected stack: ResourceStack;
|
||
|
||
constructor(protected cluster: KubernetesCluster) {
|
||
this.stack = new ResourceStack(cluster, "example-resource-stack");
|
||
}
|
||
|
||
get resourceFolder() {
|
||
return path.join(__dirname, "../resources/");
|
||
}
|
||
|
||
install(): Promise<string> {
|
||
console.log("installing example-pod");
|
||
return this.stack.kubectlApplyFolder(this.resourceFolder);
|
||
}
|
||
|
||
async isInstalled(): Promise<boolean> {
|
||
try {
|
||
const podApi = forCluster(this.cluster, Pod);
|
||
const examplePod = await podApi.get({name: "example-pod", namespace: "default"});
|
||
|
||
if (examplePod?.kind) {
|
||
console.log("found example-pod");
|
||
return true;
|
||
}
|
||
} catch(e) {
|
||
console.log("Error getting example-pod:", e);
|
||
}
|
||
console.log("didn't find example-pod");
|
||
|
||
return false;
|
||
}
|
||
|
||
async uninstall(): Promise<string> {
|
||
console.log("uninstalling example-pod");
|
||
return this.stack.kubectlDeleteFolder(this.resourceFolder);
|
||
}
|
||
}
|
||
```
|
||
|
||
The `ExampleClusterFeature` class constructor takes a `Common.Catalog.KubernetesCluster` argument.
|
||
This is the cluster that the resource stack will be applied to, and the constructor instantiates a `Renderer.K8sApi.ResourceStack` as such.
|
||
`ExampleClusterFeature` implements an `install()` method which simply invokes the `kubectlApplyFolder()` method of the `Renderer.K8sApi.ResourceStack` class.
|
||
`kubectlApplyFolder()` applies to the cluster all kubernetes resources found in the folder passed to it, in this case `../resources`.
|
||
Similarly, `ExampleClusterFeature` implements an `uninstall()` method which simply invokes the `kubectlDeleteFolder()` method of the `Renderer.K8sApi.ResourceStack` class.
|
||
`kubectlDeleteFolder()` tries to delete from the cluster all kubernetes resources found in the folder passed to it, again in this case `../resources`.
|
||
|
||
`ExampleClusterFeature` also implements an `isInstalled()` method, which demonstrates how you can utilize the kubernetes api to inspect the resource stack status.
|
||
`isInstalled()` simply tries to find a pod named `example-pod`, as a way to determine if the pod is already installed.
|
||
This method can be useful in creating a context-sensitive UI for installing/uninstalling the feature, as demonstrated in the next sample code.
|
||
|
||
To allow the end-user to control the life cycle of this cluster feature the following code sample shows how to implement a user interface based on React and custom Lens UI components:
|
||
|
||
```typescript
|
||
import React from "react";
|
||
import { Common, Renderer } from "@k8slens/extensions";
|
||
import { observer } from "mobx-react";
|
||
import { computed, observable, makeObservable } from "mobx";
|
||
import { ExampleClusterFeature } from "./example-cluster-feature";
|
||
|
||
const {
|
||
Component: {
|
||
SubTitle, Button,
|
||
}
|
||
} = Renderer;
|
||
|
||
interface Props {
|
||
cluster: Common.Catalog.KubernetesCluster;
|
||
}
|
||
|
||
@observer
|
||
export class ExampleClusterFeatureSettings extends React.Component<Props> {
|
||
constructor(props: Props) {
|
||
super(props);
|
||
makeObservable(this);
|
||
}
|
||
|
||
@observable installed = false;
|
||
@observable inProgress = false;
|
||
|
||
feature: ExampleClusterFeature;
|
||
|
||
async componentDidMount() {
|
||
this.feature = new ExampleClusterFeature(this.props.cluster);
|
||
|
||
await this.updateFeatureState();
|
||
}
|
||
|
||
async updateFeatureState() {
|
||
this.installed = await this.feature.isInstalled();
|
||
}
|
||
|
||
async save() {
|
||
this.inProgress = true;
|
||
|
||
try {
|
||
if (this.installed) {
|
||
await this.feature.uninstall();
|
||
} else {
|
||
await this.feature.install();
|
||
}
|
||
} finally {
|
||
this.inProgress = false;
|
||
|
||
await this.updateFeatureState();
|
||
}
|
||
}
|
||
|
||
@computed get buttonLabel() {
|
||
if (this.inProgress && this.installed) return "Uninstalling ...";
|
||
if (this.inProgress) return "Applying ...";
|
||
|
||
if (this.installed) {
|
||
return "Uninstall";
|
||
}
|
||
|
||
return "Apply";
|
||
}
|
||
|
||
render() {
|
||
return (
|
||
<>
|
||
<section>
|
||
<SubTitle title="Example Cluster Feature using a Resource Stack" />
|
||
<Button
|
||
label={this.buttonLabel}
|
||
waiting={this.inProgress}
|
||
onClick={() => this.save()}
|
||
primary />
|
||
</section>
|
||
</>
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
The `ExampleClusterFeatureSettings` class extends `React.Component` and simply renders a subtitle and a button.
|
||
`ExampleClusterFeatureSettings` takes the cluster as a prop and when the React component has mounted the `ExampleClusterFeature` is instantiated using this cluster (in `componentDidMount()`).
|
||
The rest of the logic concerns the button appearance and action, based on the `ExampleClusterFeatureSettings` fields `installed` and `inProgress`.
|
||
The `installed` value is of course determined using the aforementioned `ExampleClusterFeature` method `isInstalled()`.
|
||
The `inProgress` value is true while waiting for the feature to be installed (or uninstalled).
|
||
|
||
Note that the button is a `Renderer.Component.Button` element and this example takes advantage of its `waiting` prop to show a "waiting" animation while the install (or uninstall) is in progress.
|
||
Using elements from `Renderer.Component` is encouraged, to take advantage of their built-in properties, and to ensure a common look and feel.
|
||
|
||
Also note that [MobX 6](https://mobx.js.org/README.html) is used for state management, ensuring that the UI is rerendered when state has changed.
|
||
The `ExampleClusterFeatureSettings` class is marked as an `@observer`, and its constructor must call `makeObservable()`.
|
||
As well, the `installed` and `inProgress` fields are marked as `@observable`, ensuring that the button gets rerendered any time these fields change.
|
||
|
||
Finally, `ExampleClusterFeatureSettings` needs to be connected to the extension, and would typically appear on the cluster **Settings** page via a `Renderer.LensExtension.entitySettings` entry.
|
||
The `ExampleExtension` would look like this:
|
||
|
||
```typescript
|
||
import { Common, Renderer } from "@k8slens/extensions";
|
||
import { ExampleClusterFeatureSettings } from "./src/example-cluster-feature-settings"
|
||
import React from "react"
|
||
|
||
export default class ExampleExtension extends Renderer.LensExtension {
|
||
entitySettings = [
|
||
{
|
||
apiVersions: ["entity.k8slens.dev/v1alpha1"],
|
||
kind: "KubernetesCluster",
|
||
title: "Example Cluster Feature",
|
||
priority: 5,
|
||
components: {
|
||
View: ({ entity = null }: { entity: Common.Catalog.KubernetesCluster}) => {
|
||
return (
|
||
<ExampleClusterFeatureSettings cluster={entity} />
|
||
);
|
||
}
|
||
}
|
||
}
|
||
];
|
||
|
||
}
|
||
```
|
||
|
||
An entity setting is added to the `entitySettings` array field of the `Renderer.LensExtension` class.
|
||
Because Lens's catalog can contain different kinds of entities, the kind must be identified.
|
||
For more details about the catalog see the [Catalog Guide](catalog.md).
|
||
Clusters are a built-in kind, so the `apiVersions` and `kind` fields should be set as above.
|
||
The `title` is shown as a navigation item on the cluster **Settings** page and the `components.View` is displayed when the navigation item is clicked on.
|
||
The `components.View` definition above shows how the `ExampleClusterFeatureSettings` element is included, and how its `cluster` prop is set.
|
||
`priority` determines the order of the entity settings, the higher the number the higher in the navigation panel the setting is placed. The default value is 50.
|
||
|
||
The final result looks like this:
|
||
|
||

|
||
|
||
`ExampleClusterFeature` and `ExampleClusterFeatureSettings` demonstrate a cluster feature for a simple resource stack.
|
||
In practice a resource stack can include many resources, and require more sophisticated life cycle management (upgrades, partial installations, etc.)
|
||
Using `Renderer.K8sApi.ResourceStack` and `entitySettings` it is possible to implement solutions for more complex cluster features.
|
||
The **Lens Metrics** setting (on the cluster **Settings** page) is a good example of an advanced solution. |