mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge pull request #1673 from lensapp/release/v4.0-doc-update
[Release/v4.0] cherry-pick doc updates
This commit is contained in:
commit
b10f3fa4a1
BIN
docs/extensions/guides/images/certificates-crd-list.png
Normal file
BIN
docs/extensions/guides/images/certificates-crd-list.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 792 KiB |
@ -1,3 +1,268 @@
|
|||||||
---
|
# KubeObjectListLayout Sample
|
||||||
WIP
|
|
||||||
---
|
In this guide we will learn how to list Kubernetes CRD objects on the cluster dashboard. You can see the complete source code for this guide [here](https://github.com/lensapp/lens-extension-samples/tree/master/custom-resource-page).
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Next, we will go the implementation through in steps. To achieve our goal, we need to:
|
||||||
|
|
||||||
|
1. [Register ClustePage and ClusterPageMenu objects](#register-objects-for-clustepages-and-clusterpagemenus)
|
||||||
|
2. [List Certificate Objects on the Cluster Page](#list-certificate-objects-on-the-cluster-page)
|
||||||
|
3. [Customize Details Panel](#customize-details-panel)
|
||||||
|
|
||||||
|
## Register `clusterPage` and `clusterPageMenu` Objects
|
||||||
|
|
||||||
|
First thing we need to do with our extension is to register new menu item in the cluster menu and create a cluster page that is opened when clicking the menu item. We will do this in our extension class `CrdSampleExtension` that is derived `LensRendererExtension` class:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export default class CrdSampleExtension extends LensRendererExtension {
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To register menu item in the cluster menu we need to register `PageMenuRegistration` object. This object will register a menu item with "Certificates" text. It will also use `CertificateIcon` component to render an icon and navigate to cluster page that is having `certificates` page id.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function CertificateIcon(props: Component.IconProps) {
|
||||||
|
return <Component.Icon {...props} material="security" tooltip="Certificates"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CrdSampleExtension extends LensRendererExtension {
|
||||||
|
|
||||||
|
clusterPageMenus = [
|
||||||
|
{
|
||||||
|
target: { pageId: "certificates" },
|
||||||
|
title: "Certificates",
|
||||||
|
components: {
|
||||||
|
Icon: CertificateIcon,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then we need to register `PageRegistration` object with `certificates` id and define `CertificatePage` component to render certificates.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export default class CrdSampleExtension extends LensRendererExtension {
|
||||||
|
...
|
||||||
|
|
||||||
|
clusterPages = [{
|
||||||
|
id: "certificates",
|
||||||
|
components: {
|
||||||
|
Page: () => <CertificatePage extension={this} />,
|
||||||
|
MenuIcon: CertificateIcon,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## List Certificate Objects on the Cluster Page
|
||||||
|
|
||||||
|
In the previous step we defined `CertificatePage` component to render certificates. In this step we will actually implement that. `CertificatePage` is a React component that will render `Component.KubeObjectListLayout` component to list `Certificate` CRD objects.
|
||||||
|
|
||||||
|
### Get CRD objects
|
||||||
|
|
||||||
|
In order to list CRD objects, we need first fetch those from Kubernetes API. Lens Extensions API provides easy mechanism to do this. We just need to define `Certificate` class derived from `K8sApi.KubeObject`, `CertificatesApi`derived from `K8sApi.KubeApi` and `CertificatesStore` derived from `K8sApi.KubeObjectStore`.
|
||||||
|
|
||||||
|
`Certificate` class defines properties found in the CRD object:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class Certificate extends K8sApi.KubeObject {
|
||||||
|
static kind = "Certificate"
|
||||||
|
static namespaced = true
|
||||||
|
static apiBase = "/apis/cert-manager.io/v1alpha2/certificates"
|
||||||
|
|
||||||
|
kind: string
|
||||||
|
apiVersion: string
|
||||||
|
metadata: {
|
||||||
|
name: string;
|
||||||
|
namespace: string;
|
||||||
|
selfLink: string;
|
||||||
|
uid: string;
|
||||||
|
resourceVersion: string;
|
||||||
|
creationTimestamp: string;
|
||||||
|
labels: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
annotations: {
|
||||||
|
[key: string]: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
spec: {
|
||||||
|
dnsNames: string[];
|
||||||
|
issuerRef: {
|
||||||
|
group: string;
|
||||||
|
kind: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
secretName: string
|
||||||
|
}
|
||||||
|
status: {
|
||||||
|
conditions: {
|
||||||
|
lastTransitionTime: string;
|
||||||
|
message: string;
|
||||||
|
reason: string;
|
||||||
|
status: string;
|
||||||
|
type?: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With `CertificatesApi` class we are able to manage `Certificate` objects in Kubernetes API:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class CertificatesApi extends K8sApi.KubeApi<Certificate> {
|
||||||
|
}
|
||||||
|
export const certificatesApi = new CertificatesApi({
|
||||||
|
objectConstructor: Certificate
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`CertificateStore` defines storage for `Certificate` objects
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export class CertificatesStore extends K8sApi.KubeObjectStore<Certificate> {
|
||||||
|
api = certificatesApi
|
||||||
|
}
|
||||||
|
|
||||||
|
export const certificatesStore = new CertificatesStore();
|
||||||
|
```
|
||||||
|
|
||||||
|
And, finally, we register this store to Lens's API manager.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
K8sApi.apiManager.registerStore(certificatesStore);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Create CertificatePage component
|
||||||
|
|
||||||
|
Now we have created mechanism to manage `Certificate` objects in Kubernetes API. Then we need to fetch those and render them in the UI.
|
||||||
|
|
||||||
|
First we define `CertificatePage` class that extends `React.Component`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Component, LensRendererExtension } from "@k8slens/extensions";
|
||||||
|
import React from "react";
|
||||||
|
import { certificatesStore } from "../certificate-store";
|
||||||
|
import { Certificate } from "../certificate"
|
||||||
|
|
||||||
|
export class CertificatePage extends React.Component<{ extension: LensRendererExtension }> {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next we will implement `render` method that will display certificates in a list. To do that, we just need to add `Component.KubeObjectListLayout` component inside `Component.TabLayout` component in render method. To define which objects the list is showing, we need to pass `certificateStore` object to `Component.KubeObjectListLayout` in `store` property. `Component.KubeObjectListLayout` will fetch automacially items from the given store when component is mounted. Also, we can define needed sorting callbacks and search filters for the list:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
enum sortBy {
|
||||||
|
name = "name",
|
||||||
|
namespace = "namespace",
|
||||||
|
issuer = "issuer"
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CertificatePage extends React.Component<{ extension: LensRendererExtension }> {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Component.TabLayout>
|
||||||
|
<Component.KubeObjectListLayout
|
||||||
|
className="Certicates" store={certificatesStore}
|
||||||
|
sortingCallbacks={{
|
||||||
|
[sortBy.name]: (certificate: Certificate) => certificate.getName(),
|
||||||
|
[sortBy.namespace]: (certificate: Certificate) => certificate.metadata.namespace,
|
||||||
|
[sortBy.issuer]: (certificate: Certificate) => certificate.spec.issuerRef.name
|
||||||
|
}}
|
||||||
|
searchFilters={[
|
||||||
|
(certificate: Certificate) => certificate.getSearchFields()
|
||||||
|
]}
|
||||||
|
renderHeaderTitle="Certificates"
|
||||||
|
renderTableHeader={[
|
||||||
|
{ title: "Name", className: "name", sortBy: sortBy.name },
|
||||||
|
{ title: "Namespace", className: "namespace", sortBy: sortBy.namespace },
|
||||||
|
{ title: "Issuer", className: "issuer", sortBy: sortBy.namespace },
|
||||||
|
]}
|
||||||
|
renderTableContents={(certificate: Certificate) => [
|
||||||
|
certificate.getName(),
|
||||||
|
certificate.metadata.namespace,
|
||||||
|
certificate.spec.issuerRef.name
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Component.TabLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize Details panel
|
||||||
|
|
||||||
|
We have learned now, how to list CRD objects in a list view. Next, we will learn how to customize details panel that will be opened when the object is clicked in the list.
|
||||||
|
|
||||||
|
First, we need to register our custom component to render details for the specific Kubernetes custom resource, in our case `Certificate`. We will do this again in `CrdSampleExtension` class:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export default class CrdSampleExtension extends LensRendererExtension {
|
||||||
|
//...
|
||||||
|
|
||||||
|
kubeObjectDetailItems = [{
|
||||||
|
kind: Certificate.kind,
|
||||||
|
apiVersions: ["cert-manager.io/v1alpha2"],
|
||||||
|
components: {
|
||||||
|
Details: (props: CertificateDetailsProps) => <CertificateDetails {...props} />
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here we defined that `CertificateDetails` component will render the resource details. So, next we need to implement that component. Lens will inject `Certificate` object into our component so we just need to render some information out of it. We can use `Component.DrawerItem` component from Lens Extensions API to give the same look and feel as Lens is using elsewhere:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Component, K8sApi } from "@k8slens/extensions";
|
||||||
|
import React from "react";
|
||||||
|
import { Certificate } from "../certificate";
|
||||||
|
|
||||||
|
export interface CertificateDetailsProps extends Component.KubeObjectDetailsProps<Certificate>{
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CertificateDetails extends React.Component<CertificateDetailsProps> {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { object: certificate } = this.props;
|
||||||
|
if (!certificate) return null;
|
||||||
|
return (
|
||||||
|
<div className="Certificate">
|
||||||
|
<Component.DrawerItem name="Created">
|
||||||
|
{certificate.getAge(true, false)} ago ({certificate.metadata.creationTimestamp })
|
||||||
|
</Component.DrawerItem>
|
||||||
|
<Component.DrawerItem name="DNS Names">
|
||||||
|
{certificate.spec.dnsNames.join(",")}
|
||||||
|
</Component.DrawerItem>
|
||||||
|
<Component.DrawerItem name="Secret">
|
||||||
|
{certificate.spec.secretName}
|
||||||
|
</Component.DrawerItem>
|
||||||
|
<Component.DrawerItem name="Status" className="status" labelsOnly>
|
||||||
|
{certificate.status.conditions.map((condition, index) => {
|
||||||
|
const { type, reason, message, status } = condition;
|
||||||
|
const kind = type || reason;
|
||||||
|
if (!kind) return null;
|
||||||
|
return (
|
||||||
|
<Component.Badge
|
||||||
|
key={kind + index} label={kind}
|
||||||
|
className={"success "+kind.toLowerCase()}
|
||||||
|
tooltip={message}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Component.DrawerItem>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Like we can see above, it's very easy to add custom pages and fetch Kubernetes resources by using Extensions API. Please see the [complete source code](https://github.com/lensapp/lens-extension-samples/tree/master/custom-resource-page) to test it out.
|
||||||
@ -1 +1,46 @@
|
|||||||
TBD
|
# FAQ
|
||||||
|
|
||||||
|
### What operating systems does Lens support?
|
||||||
|
|
||||||
|
Lens supports MacOS, Windows and Linux operating systems. For Linux there are Snap and AppImage versions. For MacOS there are DMG and Homebrew options.
|
||||||
|
|
||||||
|
### Lens application is not opening, what might be wrong?
|
||||||
|
|
||||||
|
When Lens is started, it will start HTTP proxy server on the background and requires that operating system allows to start listening to some free port. You can see the port allocated for Lens from application logs. Lens expects also that `localhost` DNS points to `127.0.0.1` address.
|
||||||
|
|
||||||
|
### Why can't I add any clusters?
|
||||||
|
|
||||||
|
When adding new clusters, a valid Kubeconfig file is required. Please check that all contexts present in Kubeconfig file are valid.
|
||||||
|
|
||||||
|
### Why Cluster dashboard is not opening?
|
||||||
|
|
||||||
|
To see Cluster dashboard properly, Kubernetes cluster must be reachable either directly from your computer or via HTTP proxy. You can configure HTTP proxy in Cluster Settigns. Also, provided credentials in Kubeconfig must be valid. If Kubeconfig uses `exec` command, the binary must be available in global PATH or absolute path must be used. Lens application can't see PATH modifications made by any shell init scripts. There might be also some issues on the Snap version if the exec binary is installed also from Snap and requires additional symlinking, please see [#699](https://github.com/lensapp/lens/issues/699).
|
||||||
|
|
||||||
|
### Why I don't see anything on Cluster dashboard?
|
||||||
|
|
||||||
|
Users will see on Cluster dashboard only those resources that they are allowed to see (RBAC). Lens requires that user has access at least to one namespace. Lens tries first fetch namespaces from Kubernetes API. If user is not allowed to list namespaces, allowed namespaces can be configured in Cluster settings or in Kubeconfig.
|
||||||
|
|
||||||
|
### Why I don't see any metrics or some of the metrics are not working?
|
||||||
|
|
||||||
|
In order to display cluster metrics, Lens requires that Prometheus is running in the cluster. You can install Prometheus in Cluster settings if needed.
|
||||||
|
|
||||||
|
Lens tries to detect Prometheus installation automatically. If it fails to detect the installation properly, you can configure Prometheus service address in Cluster settings. If some of the metrics are not displayed correctly, you can see queries that Lens is using [here](https://github.com/lensapp/lens/tree/master/src/main/prometheus) and adapt your prometheus configuration to support those queries. Please refer [Prometheus documentation](https://prometheus.io/docs/prometheus/latest/configuration/configuration/) or your Prometheus installer documentation how to do this.
|
||||||
|
|
||||||
|
### Kubectl is not working in Lens terminal, what should I do?
|
||||||
|
|
||||||
|
Lens tries to download correct Kubectl version for the cluster and use that in Lens terminal. Some operating systems (namely Windows) might have restrictions set that prevent downloading and executing binaries from the default location that Lens is using. You can change the directory where Lens downloads the binaries in App Preferences. It's also possible to change the Download mirror to use Azure if default Google is not reachable from your network. If downloading Kubectl is not option for you, you can define path to pre-installed Kubectl on your machine and Lens will use that binary instead.
|
||||||
|
|
||||||
|
### How can I configure Helm repositories?
|
||||||
|
|
||||||
|
Lens comes with bundled Helm 3 binary and Lens will add by default `bitnami` repository if no other repositories are configured. You can add more repositories from Artifact HUB in App preferences. At this moment it is not possible to add private repositories. Those and other public repositories can be added manually via command line.
|
||||||
|
|
||||||
|
### Where can I find application logs?
|
||||||
|
|
||||||
|
Lens will store application logs to following locations depending on your operating system:
|
||||||
|
- MacOS: ~/Library/Logs/Lens/
|
||||||
|
- Windows: %USERPROFILE%\AppData\Roaming\Lens\logs\
|
||||||
|
- Linux: ~/.config/Lens/logs/
|
||||||
|
|
||||||
|
### How can I see more verbose logs?
|
||||||
|
|
||||||
|
You can start Lens application on debug mode from the command line to see more verbose logs. To start application on debug mode, please provide `DEBUG=true` environment variable and before starting the application, for example: `DEBUG=TRUE /Applications/Lens.app/Contents/MacOS/Lens`
|
||||||
@ -26,6 +26,11 @@ export interface WorkspaceState {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Workspace
|
||||||
|
*
|
||||||
|
* @beta
|
||||||
|
*/
|
||||||
export class Workspace implements WorkspaceModel, WorkspaceState {
|
export class Workspace implements WorkspaceModel, WorkspaceState {
|
||||||
/**
|
/**
|
||||||
* Unique id for workspace
|
* Unique id for workspace
|
||||||
@ -78,23 +83,39 @@ export class Workspace implements WorkspaceModel, WorkspaceState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is workspace managed by an extension
|
||||||
|
*/
|
||||||
get isManaged(): boolean {
|
get isManaged(): boolean {
|
||||||
return !!this.ownerRef;
|
return !!this.ownerRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get workspace state
|
||||||
|
*
|
||||||
|
*/
|
||||||
getState(): WorkspaceState {
|
getState(): WorkspaceState {
|
||||||
return toJS({
|
return toJS({
|
||||||
enabled: this.enabled
|
enabled: this.enabled
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push state
|
||||||
|
*
|
||||||
|
* @interal
|
||||||
|
* @param state workspace state
|
||||||
|
*/
|
||||||
pushState(state = this.getState()) {
|
pushState(state = this.getState()) {
|
||||||
logger.silly("[WORKSPACE] pushing state", {...state, id: this.id});
|
logger.silly("[WORKSPACE] pushing state", {...state, id: this.id});
|
||||||
broadcastMessage("workspace:state", this.id, toJS(state));
|
broadcastMessage("workspace:state", this.id, toJS(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
/**
|
||||||
setState(state: WorkspaceState) {
|
*
|
||||||
|
* @param state workspace state
|
||||||
|
*/
|
||||||
|
@action setState(state: WorkspaceState) {
|
||||||
Object.assign(this, state);
|
Object.assign(this, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
export { ExtensionStore } from "../extension-store";
|
export { ExtensionStore } from "../extension-store";
|
||||||
|
|
||||||
export { clusterStore, Cluster } from "../stores/cluster-store";
|
export { clusterStore, Cluster, ClusterStore } from "../stores/cluster-store";
|
||||||
export type { ClusterModel, ClusterId } from "../stores/cluster-store";
|
export type { ClusterModel, ClusterId } from "../stores/cluster-store";
|
||||||
|
|
||||||
export { workspaceStore, Workspace } from "../stores/workspace-store";
|
export { workspaceStore, Workspace, WorkspaceStore } from "../stores/workspace-store";
|
||||||
export type { WorkspaceId, WorkspaceModel } from "../stores/workspace-store";
|
export type { WorkspaceId, WorkspaceModel } from "../stores/workspace-store";
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,8 @@ export type { ClusterModel, ClusterId } from "../../common/cluster-store";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Store for all added clusters
|
* Store for all added clusters
|
||||||
|
*
|
||||||
|
* @beta
|
||||||
*/
|
*/
|
||||||
export class ClusterStore extends Singleton {
|
export class ClusterStore extends Singleton {
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,8 @@ export type { WorkspaceId, WorkspaceModel } from "../../common/workspace-store";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores all workspaces
|
* Stores all workspaces
|
||||||
|
*
|
||||||
|
* @beta
|
||||||
*/
|
*/
|
||||||
export class WorkspaceStore extends Singleton {
|
export class WorkspaceStore extends Singleton {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -49,10 +49,31 @@ export interface ClusterState {
|
|||||||
allowedResources: string[]
|
allowedResources: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cluster
|
||||||
|
*
|
||||||
|
* @beta
|
||||||
|
*/
|
||||||
export class Cluster implements ClusterModel, ClusterState {
|
export class Cluster implements ClusterModel, ClusterState {
|
||||||
|
/** Unique id for a cluster */
|
||||||
public id: ClusterId;
|
public id: ClusterId;
|
||||||
|
/**
|
||||||
|
* Kubectl
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
public kubeCtl: Kubectl;
|
public kubeCtl: Kubectl;
|
||||||
|
/**
|
||||||
|
* Context handler
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
public contextHandler: ContextHandler;
|
public contextHandler: ContextHandler;
|
||||||
|
/**
|
||||||
|
* Owner reference
|
||||||
|
*
|
||||||
|
* If extension sets this it needs to also mark cluster as enabled on activate (or when added to a store)
|
||||||
|
*/
|
||||||
public ownerRef: string;
|
public ownerRef: string;
|
||||||
protected kubeconfigManager: KubeconfigManager;
|
protected kubeconfigManager: KubeconfigManager;
|
||||||
protected eventDisposers: Function[] = [];
|
protected eventDisposers: Function[] = [];
|
||||||
@ -61,34 +82,147 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
whenInitialized = when(() => this.initialized);
|
whenInitialized = when(() => this.initialized);
|
||||||
whenReady = when(() => this.ready);
|
whenReady = when(() => this.ready);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is cluster object initialized
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
@observable initialized = false;
|
@observable initialized = false;
|
||||||
|
/**
|
||||||
|
* Kubeconfig context name
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
@observable contextName: string;
|
@observable contextName: string;
|
||||||
|
/**
|
||||||
|
* Workspace id
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
@observable workspace: WorkspaceId;
|
@observable workspace: WorkspaceId;
|
||||||
|
/**
|
||||||
|
* Path to kubeconfig
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
@observable kubeConfigPath: string;
|
@observable kubeConfigPath: string;
|
||||||
|
/**
|
||||||
|
* Kubernetes API server URL
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
@observable apiUrl: string; // cluster server url
|
@observable apiUrl: string; // cluster server url
|
||||||
|
/**
|
||||||
|
* Internal authentication proxy URL
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
@observable kubeProxyUrl: string; // lens-proxy to kube-api url
|
@observable kubeProxyUrl: string; // lens-proxy to kube-api url
|
||||||
|
/**
|
||||||
|
* Is cluster instance enabled (disabled clusters are currently hidden)
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
@observable enabled = false; // only enabled clusters are visible to users
|
@observable enabled = false; // only enabled clusters are visible to users
|
||||||
|
/**
|
||||||
|
* Is cluster online
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
@observable online = false; // describes if we can detect that cluster is online
|
@observable online = false; // describes if we can detect that cluster is online
|
||||||
|
/**
|
||||||
|
* Can user access cluster resources
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
@observable accessible = false; // if user is able to access cluster resources
|
@observable accessible = false; // if user is able to access cluster resources
|
||||||
|
/**
|
||||||
|
* Is cluster instance in usable state
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
@observable ready = false; // cluster is in usable state
|
@observable ready = false; // cluster is in usable state
|
||||||
|
/**
|
||||||
|
* Is cluster currently reconnecting
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
@observable reconnecting = false;
|
@observable reconnecting = false;
|
||||||
@observable disconnected = true; // false if user has selected to connect
|
/**
|
||||||
|
* Is cluster disconnected. False if user has selected to connect.
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
|
@observable disconnected = true;
|
||||||
|
/**
|
||||||
|
* Connection failure reason
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
@observable failureReason: string;
|
@observable failureReason: string;
|
||||||
|
/**
|
||||||
|
* Does user have admin like access
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
@observable isAdmin = false;
|
@observable isAdmin = false;
|
||||||
|
/**
|
||||||
|
* Preferences
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
@observable preferences: ClusterPreferences = {};
|
@observable preferences: ClusterPreferences = {};
|
||||||
|
/**
|
||||||
|
* Metadata
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
@observable metadata: ClusterMetadata = {};
|
@observable metadata: ClusterMetadata = {};
|
||||||
|
/**
|
||||||
|
* List of allowed namespaces
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
@observable allowedNamespaces: string[] = [];
|
@observable allowedNamespaces: string[] = [];
|
||||||
|
/**
|
||||||
|
* List of allowed resources
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
@observable allowedResources: string[] = [];
|
@observable allowedResources: string[] = [];
|
||||||
|
/**
|
||||||
|
* List of accessible namespaces
|
||||||
|
*
|
||||||
|
* @observable
|
||||||
|
*/
|
||||||
@observable accessibleNamespaces: string[] = [];
|
@observable accessibleNamespaces: string[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is cluster available
|
||||||
|
*
|
||||||
|
* @computed
|
||||||
|
*/
|
||||||
@computed get available() {
|
@computed get available() {
|
||||||
return this.accessible && !this.disconnected;
|
return this.accessible && !this.disconnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cluster name
|
||||||
|
*
|
||||||
|
* @computed
|
||||||
|
*/
|
||||||
@computed get name() {
|
@computed get name() {
|
||||||
return this.preferences.clusterName || this.contextName;
|
return this.preferences.clusterName || this.contextName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prometheus preferences
|
||||||
|
*
|
||||||
|
* @computed
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
@computed get prometheusPreferences(): ClusterPrometheusPreferences {
|
@computed get prometheusPreferences(): ClusterPrometheusPreferences {
|
||||||
const { prometheus, prometheusProvider } = this.preferences;
|
const { prometheus, prometheusProvider } = this.preferences;
|
||||||
|
|
||||||
@ -97,6 +231,9 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kubernetes version
|
||||||
|
*/
|
||||||
get version(): string {
|
get version(): string {
|
||||||
return String(this.metadata?.version) || "";
|
return String(this.metadata?.version) || "";
|
||||||
}
|
}
|
||||||
@ -110,17 +247,29 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is cluster managed by an extension
|
||||||
|
*/
|
||||||
get isManaged(): boolean {
|
get isManaged(): boolean {
|
||||||
return !!this.ownerRef;
|
return !!this.ownerRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
/**
|
||||||
updateModel(model: ClusterModel) {
|
* Update cluster data model
|
||||||
|
*
|
||||||
|
* @param model
|
||||||
|
*/
|
||||||
|
@action updateModel(model: ClusterModel) {
|
||||||
Object.assign(this, model);
|
Object.assign(this, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
/**
|
||||||
async init(port: number) {
|
* Initialize a cluster (can be done only in main process)
|
||||||
|
*
|
||||||
|
* @param port port where internal auth proxy is listening
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
@action async init(port: number) {
|
||||||
try {
|
try {
|
||||||
this.contextHandler = new ContextHandler(this);
|
this.contextHandler = new ContextHandler(this);
|
||||||
this.kubeconfigManager = await KubeconfigManager.create(this, this.contextHandler, port);
|
this.kubeconfigManager = await KubeconfigManager.create(this, this.contextHandler, port);
|
||||||
@ -139,6 +288,9 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
protected bindEvents() {
|
protected bindEvents() {
|
||||||
logger.info(`[CLUSTER]: bind events`, this.getMeta());
|
logger.info(`[CLUSTER]: bind events`, this.getMeta());
|
||||||
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s
|
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s
|
||||||
@ -156,14 +308,20 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* internal
|
||||||
|
*/
|
||||||
protected unbindEvents() {
|
protected unbindEvents() {
|
||||||
logger.info(`[CLUSTER]: unbind events`, this.getMeta());
|
logger.info(`[CLUSTER]: unbind events`, this.getMeta());
|
||||||
this.eventDisposers.forEach(dispose => dispose());
|
this.eventDisposers.forEach(dispose => dispose());
|
||||||
this.eventDisposers.length = 0;
|
this.eventDisposers.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
/**
|
||||||
async activate(force = false) {
|
* @param force force activation
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
@action async activate(force = false) {
|
||||||
if (this.activated && !force) {
|
if (this.activated && !force) {
|
||||||
return this.pushState();
|
return this.pushState();
|
||||||
}
|
}
|
||||||
@ -190,22 +348,29 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
return this.pushState();
|
return this.pushState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
protected async ensureKubectl() {
|
protected async ensureKubectl() {
|
||||||
this.kubeCtl = new Kubectl(this.version);
|
this.kubeCtl = new Kubectl(this.version);
|
||||||
|
|
||||||
return this.kubeCtl.ensureKubectl(); // download kubectl in background, so it's not blocking dashboard
|
return this.kubeCtl.ensureKubectl(); // download kubectl in background, so it's not blocking dashboard
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
/**
|
||||||
async reconnect() {
|
* @internal
|
||||||
|
*/
|
||||||
|
@action async reconnect() {
|
||||||
logger.info(`[CLUSTER]: reconnect`, this.getMeta());
|
logger.info(`[CLUSTER]: reconnect`, this.getMeta());
|
||||||
this.contextHandler?.stopServer();
|
this.contextHandler?.stopServer();
|
||||||
await this.contextHandler?.ensureServer();
|
await this.contextHandler?.ensureServer();
|
||||||
this.disconnected = false;
|
this.disconnected = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
/**
|
||||||
disconnect() {
|
* @internal
|
||||||
|
*/
|
||||||
|
@action disconnect() {
|
||||||
logger.info(`[CLUSTER]: disconnect`, this.getMeta());
|
logger.info(`[CLUSTER]: disconnect`, this.getMeta());
|
||||||
this.unbindEvents();
|
this.unbindEvents();
|
||||||
this.contextHandler?.stopServer();
|
this.contextHandler?.stopServer();
|
||||||
@ -217,8 +382,11 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
this.pushState();
|
this.pushState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
/**
|
||||||
async refresh(opts: ClusterRefreshOptions = {}) {
|
* @internal
|
||||||
|
* @param opts refresh options
|
||||||
|
*/
|
||||||
|
@action async refresh(opts: ClusterRefreshOptions = {}) {
|
||||||
logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
||||||
await this.whenInitialized;
|
await this.whenInitialized;
|
||||||
await this.refreshConnectionStatus();
|
await this.refreshConnectionStatus();
|
||||||
@ -235,8 +403,10 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
this.pushState();
|
this.pushState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
/**
|
||||||
async refreshMetadata() {
|
* @internal
|
||||||
|
*/
|
||||||
|
@action async refreshMetadata() {
|
||||||
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
|
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
|
||||||
const metadata = await detectorRegistry.detectForCluster(this);
|
const metadata = await detectorRegistry.detectForCluster(this);
|
||||||
const existingMetadata = this.metadata;
|
const existingMetadata = this.metadata;
|
||||||
@ -244,16 +414,20 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
this.metadata = Object.assign(existingMetadata, metadata);
|
this.metadata = Object.assign(existingMetadata, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
/**
|
||||||
async refreshConnectionStatus() {
|
* @internal
|
||||||
|
*/
|
||||||
|
@action async refreshConnectionStatus() {
|
||||||
const connectionStatus = await this.getConnectionStatus();
|
const connectionStatus = await this.getConnectionStatus();
|
||||||
|
|
||||||
this.online = connectionStatus > ClusterStatus.Offline;
|
this.online = connectionStatus > ClusterStatus.Offline;
|
||||||
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
|
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
/**
|
||||||
async refreshAllowedResources() {
|
* @internal
|
||||||
|
*/
|
||||||
|
@action async refreshAllowedResources() {
|
||||||
this.allowedNamespaces = await this.getAllowedNamespaces();
|
this.allowedNamespaces = await this.getAllowedNamespaces();
|
||||||
this.allowedResources = await this.getAllowedResources();
|
this.allowedResources = await this.getAllowedResources();
|
||||||
}
|
}
|
||||||
@ -262,10 +436,16 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
return loadConfig(this.kubeConfigPath);
|
return loadConfig(this.kubeConfigPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
getProxyKubeconfig(): KubeConfig {
|
getProxyKubeconfig(): KubeConfig {
|
||||||
return loadConfig(this.getProxyKubeconfigPath());
|
return loadConfig(this.getProxyKubeconfigPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
getProxyKubeconfigPath(): string {
|
getProxyKubeconfigPath(): string {
|
||||||
return this.kubeconfigManager.getPath();
|
return this.kubeconfigManager.getPath();
|
||||||
}
|
}
|
||||||
@ -279,6 +459,12 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
return request(this.kubeProxyUrl + path, options);
|
return request(this.kubeProxyUrl + path, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param prometheusPath path to prometheus service
|
||||||
|
* @param queryParams query parameters
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
getMetrics(prometheusPath: string, queryParams: IMetricsReqParams & { query: string }) {
|
getMetrics(prometheusPath: string, queryParams: IMetricsReqParams & { query: string }) {
|
||||||
const prometheusPrefix = this.preferences.prometheus?.prefix || "";
|
const prometheusPrefix = this.preferences.prometheus?.prefix || "";
|
||||||
const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`;
|
const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`;
|
||||||
@ -329,6 +515,10 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* @param resourceAttributes resource attributes
|
||||||
|
*/
|
||||||
async canI(resourceAttributes: V1ResourceAttributes): Promise<boolean> {
|
async canI(resourceAttributes: V1ResourceAttributes): Promise<boolean> {
|
||||||
const authApi = this.getProxyKubeconfig().makeApiClient(AuthorizationV1Api);
|
const authApi = this.getProxyKubeconfig().makeApiClient(AuthorizationV1Api);
|
||||||
|
|
||||||
@ -347,6 +537,9 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
async isClusterAdmin(): Promise<boolean> {
|
async isClusterAdmin(): Promise<boolean> {
|
||||||
return this.canI({
|
return this.canI({
|
||||||
namespace: "kube-system",
|
namespace: "kube-system",
|
||||||
@ -372,7 +565,9 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// serializable cluster-state used for sync btw main <-> renderer
|
/**
|
||||||
|
* Serializable cluster-state used for sync btw main <-> renderer
|
||||||
|
*/
|
||||||
getState(): ClusterState {
|
getState(): ClusterState {
|
||||||
const state: ClusterState = {
|
const state: ClusterState = {
|
||||||
initialized: this.initialized,
|
initialized: this.initialized,
|
||||||
@ -393,11 +588,18 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
/**
|
||||||
setState(state: ClusterState) {
|
* @internal
|
||||||
|
* @param state cluster state
|
||||||
|
*/
|
||||||
|
@action setState(state: ClusterState) {
|
||||||
Object.assign(this, state);
|
Object.assign(this, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* @param state cluster state
|
||||||
|
*/
|
||||||
pushState(state = this.getState()) {
|
pushState(state = this.getState()) {
|
||||||
logger.silly(`[CLUSTER]: push-state`, state);
|
logger.silly(`[CLUSTER]: push-state`, state);
|
||||||
broadcastMessage("cluster:state", this.id, state);
|
broadcastMessage("cluster:state", this.id, state);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user