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:
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:
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:
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 ExampleClusterFeatureSettingsProps {
cluster: Common.Catalog.KubernetesCluster;
}
@observer
export class ExampleClusterFeatureSettings extends React.Component<ExampleClusterFeatureSettingsProps> {
constructor(props: ExampleClusterFeatureSettingsProps) {
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 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:
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}) => (
<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.
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.