diff --git a/k8ssandra/renderer.tsx b/k8ssandra/renderer.tsx index 0259a2d..39c089f 100644 --- a/k8ssandra/renderer.tsx +++ b/k8ssandra/renderer.tsx @@ -3,9 +3,18 @@ import React from "react"; import { CassandraDatacenterDetails, CassandraDatacenterDetailsProps } from "./src/components/cassdc-details"; import { CassandraDatacenterPage } from "./src/components/cassdc-page"; import { CassandraDatacenter } from "./src/cassdc" +import { K8ssandraClusterDetails, K8ssandraClusterDetailsProps } from "./src/components/k8c-details"; +import { K8ssandraClusterPage } from "./src/components/k8c-page"; +import { K8ssandraCluster } from "./src/k8c" +export function K8ssandraClusterIcon(props: Renderer.Component.IconProps) { + return +} export function CassandraDatacenterIcon(props: Renderer.Component.IconProps) { - return + return +} +export function K8ssandraIcon(props: Renderer.Component.IconProps) { + return } export default class CassandraDatacenterExtension extends Renderer.LensExtension { @@ -14,6 +23,12 @@ export default class CassandraDatacenterExtension extends Renderer.LensExtension components: { Page: () => , } + }, + { + id: "k8ssandraclusters", + components: { + Page: () => , + } }] clusterPageMenus = [ @@ -21,13 +36,21 @@ export default class CassandraDatacenterExtension extends Renderer.LensExtension id: "k8ssandra", title: "K8ssandra", components: { - Icon: CassandraDatacenterIcon, + Icon: K8ssandraIcon, + } + }, + { + parentId: "k8ssandra", + target: { pageId: "k8ssandraclusters" }, + title: "Clusters", + components: { + Icon: K8ssandraClusterIcon, } }, { parentId: "k8ssandra", target: { pageId: "cassandradatacenters" }, - title: "Cassandra Datacenters", + title: "Datacenters", components: { Icon: CassandraDatacenterIcon, } @@ -40,5 +63,12 @@ export default class CassandraDatacenterExtension extends Renderer.LensExtension components: { Details: (props: CassandraDatacenterDetailsProps) => } + }, + { + kind: K8ssandraCluster.kind, + apiVersions: ["k8ssandra.io/v1alpha1"], + components: { + Details: (props: K8ssandraClusterDetailsProps) => + } }] } diff --git a/k8ssandra/src/cassdc.ts b/k8ssandra/src/cassdc.ts index 5cc37f9..0ff51d5 100644 --- a/k8ssandra/src/cassdc.ts +++ b/k8ssandra/src/cassdc.ts @@ -27,6 +27,21 @@ export class CassandraDatacenter extends Renderer.K8sApi.KubeObject { serverVersion: string; serverType: string; serverImage: string; + config: { + "cassandra-yaml": { + [key: string]: any; + }, + "jvm-options": { + [key: string]: string; + }, + "jvm-server-options": CassandraDatacenter["spec"]["config"]["jvm-options"], + "jvm11-server-options": { + "additional-jvm-opts": string[]; + }, + "cassandra-env-sh": { + "additional-jvm-opts": string[]; + }, + }; } status: { conditions: { diff --git a/k8ssandra/src/components/cassdc-details.tsx b/k8ssandra/src/components/cassdc-details.tsx index aa4bb07..78cea12 100644 --- a/k8ssandra/src/components/cassdc-details.tsx +++ b/k8ssandra/src/components/cassdc-details.tsx @@ -5,7 +5,11 @@ import { CassandraDatacenter } from "../cassdc"; const { Component: { Badge, - DrawerItem + DrawerItem, + Table, + TableHead, + TableCell, + TableRow, }, } = Renderer; export interface CassandraDatacenterDetailsProps extends Renderer.Component.KubeObjectDetailsProps{ @@ -25,6 +29,95 @@ export class CassandraDatacenterDetails extends React.Component } + const cassandraYamlHeader =
Cassandra Yaml
; + var cassandraYaml =
{cassandraYamlHeader}
; + if (cassdc.spec.config) { + if ("cassandra-yaml" in cassdc.spec.config) { + cassandraYaml =
+ {cassandraYamlHeader} +
+ + + Setting + Value + + {Object.keys(cassdc.spec.config["cassandra-yaml"]).map((key, index) => { + return ( + {key} + {JSON.stringify(cassdc.spec.config["cassandra-yaml"][key])} + ); + })} +
+
+
; + } + } + + const jvmOptionsHeader =
JVM Options
; + var jvmOptions =
{jvmOptionsHeader}
; + if (cassdc.spec.config) { + if ("jvm-options" in cassdc.spec.config) { + jvmOptions =
+ {jvmOptionsHeader} + {displayJvmOptions(cassdc.spec.config["jvm-options"])} +
; + } + } + + const jvmServerOptionsHeader =
JVM Server Options
; + var jvmOptions =
{jvmOptionsHeader}
; + if (cassdc.spec.config) { + if ("jvm-server-options" in cassdc.spec.config) { + jvmOptions =
+ {jvmServerOptionsHeader} + {displayJvmOptions(cassdc.spec.config["jvm-server-options"])} +
; + } + } + + const jvm11OptionsHeader =
JVM11 Server Options
; + var jvm11Options =
{jvm11OptionsHeader}
; + if (cassdc.spec.config) { + if ("jvm11-server-options" in cassdc.spec.config && "additional-jvm-opts" in cassdc.spec.config["jvm11-server-options"]) { + jvm11Options =
+ {jvm11OptionsHeader} +
+ + + Setting + + {cassdc.spec.config["jvm11-server-options"]["additional-jvm-opts"].map((setting, index) => { + return ( + {setting} + ); + })} +
+
+
; + } + } + + const cassandraEnvHeader =
cassandra-env.sh
; + var cassandraEnvOptions =
{cassandraEnvHeader}
; + if (cassdc.spec.config) { + if ("cassandra-env-sh" in cassdc.spec.config && "additional-jvm-opts" in cassdc.spec.config["cassandra-env-sh"]) { + cassandraEnvOptions =
+ {cassandraEnvHeader} +
+ + + Setting + + {cassdc.spec.config["cassandra-env-sh"]["additional-jvm-opts"].map((setting, index) => { + return ( + {setting} + ); + })} +
+
+
; + } + } return (
@@ -69,7 +162,28 @@ export class CassandraDatacenterDetails extends React.Component {nodeReplacements} + {cassandraYaml} + {jvmOptions} + {jvm11Options} + {cassandraEnvOptions}
) } } + +function displayJvmOptions(jvmOptions: CassandraDatacenter["spec"]["config"]["jvm-options"]) { + return
+ + + Setting + Value + + {Object.keys(jvmOptions).map((key, index) => { + return ( + {key} + {jvmOptions[key]} + ); + })} +
+
; +} \ No newline at end of file diff --git a/k8ssandra/src/components/k8c-details.tsx b/k8ssandra/src/components/k8c-details.tsx new file mode 100644 index 0000000..6ebdb30 --- /dev/null +++ b/k8ssandra/src/components/k8c-details.tsx @@ -0,0 +1,172 @@ +import { Renderer } from "@k8slens/extensions"; +import React from "react"; +import { K8ssandraCluster } from "../k8c"; + +const { + Component: { + Badge, + DrawerItem, + Table, + TableHead, + TableCell, + TableRow, + SubTitle, + }, +} = Renderer; +export interface K8ssandraClusterDetailsProps extends Renderer.Component.KubeObjectDetailsProps{ +} + +export class K8ssandraClusterDetails extends React.Component { + + render() { + const { object: k8c } = this.props; + if (!k8c) return null; + + var datacentersStatus = Object.keys(k8c.status.datacenters).map(dc => { + if (k8c.status.datacenters[dc].cassandra.conditions) { + return + {k8c.status.datacenters[dc].cassandra.conditions.map((condition, index) => { + const { type, reason, message, status } = condition; + const kind = type || reason; + if (!kind) return null; + if (status === "False") return null; + return ( + + ); + })} + + } else { + return null; + } + } + ); + + const cassandraYamlHeader = ; + const jvmOptionsHeader = ; + + var clusterCassandraYaml =
; + var clusterJvmOptions =
; + + if (k8c.spec.cassandra.config) { + if (k8c.spec.cassandra.config.cassandraYaml) { + clusterCassandraYaml =
+

+ {cassandraYamlHeader} + {displayCassandraYaml(k8c.spec.cassandra.config.cassandraYaml)} +
; + } + if (k8c.spec.cassandra.config.jvmOptions) { + clusterJvmOptions =
+

+ {jvmOptionsHeader} + {displayJvmOptions(k8c.spec.cassandra.config.jvmOptions)} +
; + } + } + + var storageConfig =
; + if (k8c.spec.cassandra.storageConfig) { + storageConfig = displayStorageConfig(k8c.spec.cassandra.storageConfig); + } + + var datacentersDetails = k8c.spec.cassandra.datacenters.map(dc => { + const dcHeader =
{dc.metadata.name}
; + if (dc.config) { + + var cassandraYaml =
; + if (dc.config.cassandraYaml) { + cassandraYaml =
+ {cassandraYamlHeader} + {displayCassandraYaml(k8c.spec.cassandra.config.cassandraYaml)} +
; + } + + var jvmOptions =
; + if (dc.config.jvmOptions) { + jvmOptions =
+ {jvmOptionsHeader} + {displayJvmOptions(dc.config.jvmOptions)} +
; + } + } + + var dcStorageConfig =
; + if (dc.storageConfig) { + dcStorageConfig = displayStorageConfig(dc.storageConfig); + } + + return
+ {dcHeader} + {dc.size} + {dc.stopped} + {dcStorageConfig}

+ {cassandraYaml} + {jvmOptions}
}); + + + return ( +
+ + {k8c.getAge(true, false)} ago ({k8c.metadata.creationTimestamp }) + + + {k8c.spec.cassandra.datacenters.length} + + + {k8c.spec.cassandra.serverVersion} + + {storageConfig} +
{datacentersStatus}
+
{clusterCassandraYaml}
+
{clusterJvmOptions}
+
{datacentersDetails}
+
+ ) + } +} + +function displayCassandraYaml(cassandraYaml: K8ssandraCluster["spec"]["cassandra"]["config"]["cassandraYaml"]) { + return
+ + + Setting + Value + + {Object.keys(cassandraYaml).map((key, index) => { + return ( + {key} + {JSON.stringify(cassandraYaml[key])} + ); + })} +
+
; +} + +function displayJvmOptions(jvmOptions: K8ssandraCluster["spec"]["cassandra"]["config"]["jvmOptions"]) { + return
+ + + Setting + Value + + {Object.keys(jvmOptions).map((key, index) => { + return ( + {key} + {jvmOptions[key]} + ); + })} +
+
; +} + +function displayStorageConfig(storageConfig: K8ssandraCluster["spec"]["cassandra"]["storageConfig"]) { + return
+ {storageConfig.cassandraDataVolumeClaimSpec.storageClassName} + {storageConfig.cassandraDataVolumeClaimSpec.resources.requests.storage} + {storageConfig.cassandraDataVolumeClaimSpec.accessModes.join(" - ")} +
; +} \ No newline at end of file diff --git a/k8ssandra/src/components/k8c-page.tsx b/k8ssandra/src/components/k8c-page.tsx new file mode 100644 index 0000000..853911f --- /dev/null +++ b/k8ssandra/src/components/k8c-page.tsx @@ -0,0 +1,157 @@ +import { Renderer } from "@k8slens/extensions"; +import React from "react"; +import { k8ssandraClusterStore } from "../k8c-store"; +import { K8ssandraCluster } from "../k8c" + +const { + Component: { + KubeObjectListLayout, + Badge + } +} = Renderer; + +enum sortBy { + name = "name", + namespace = "namespace" +} + +export class K8ssandraClusterPage extends React.Component<{ extension: Renderer.LensExtension }> { + + render() { + return ( + k8c.getName(), + [sortBy.namespace]: (k8c: K8ssandraCluster) => k8c.metadata.namespace, + }} + searchFilters={[ + (k8c: K8ssandraCluster) => k8c.getSearchFields() + ]} + renderHeaderTitle="Cassandra Datacenters" + renderTableHeader={[ + {title: "Name", className: "name", sortBy: sortBy.name}, + {title: "Namespace", className: "namespace", sortBy: sortBy.namespace}, + {title: "Version", className: "Version"}, + {title: "Datacenters", className: "Size"}, + {title: "Progress", className: "progress"}, + {title: "Reaper", className: "progress"}, + {title: "Stargate", className: "progress"}, + ]} + renderTableContents={(k8c: K8ssandraCluster) => [ + k8c.getName(), + k8c.metadata.namespace, + k8c.spec.cassandra.serverVersion, + k8c.spec.cassandra.datacenters.length, + renderProgress(k8c.status), + renderReaperProgress(k8c.status), + renderStargateProgress(k8c.status), + ]} + /> + ) + } +} + +function renderProgress(k8cStatus: K8ssandraCluster["status"]) { + var dcStatuses = new Map>(); + if (k8cStatus.datacenters) { + Object.entries(k8cStatus.datacenters).forEach((dcStatus, _) => { + console.log("Reading dcStatus for " + dcStatus[0]); + const progress = dcStatus[1].cassandra.cassandraOperatorProgress; + var className = "info"; + switch (progress) { + case "Ready": + className = "success"; + break; + case "Updating": + className = "warning"; + break; + } + let dcState = new Map([ + ["className", className], + ["progress", progress] + ]); + dcStatuses.set(dcStatus[0], dcState); + }); + }; + + var badges = Array.from(dcStatuses.keys()).map(dc => { + console.log("dcStatus for " + dc + " : " + dcStatuses.get(dc).get("progress") + "/" + dcStatuses.get(dc).get("className")); + return ( + + ); + }) + + return
{badges}
; +} + +function renderReaperProgress(k8cStatus: K8ssandraCluster["status"]) { + var reaperStatuses = new Map>(); + if (k8cStatus.datacenters) { + Object.entries(k8cStatus.datacenters).forEach((dcStatus, _) => { + if (dcStatus[1].reaper) { + const progress = dcStatus[1].reaper.progress; + var className = "info"; + switch (progress) { + case "Running": + className = "success"; + break; + } + let reaperState = new Map([ + ["className", className], + ["progress", progress] + ]); + reaperStatuses.set(dcStatus[0], reaperState); + } + }); + }; + + var badges = Array.from(reaperStatuses.keys()).map(dc => { + return ( + + ); + }) + + return
{badges}
; +} + +function renderStargateProgress(k8cStatus: K8ssandraCluster["status"]) { + var stargateStatuses = new Map>(); + if (k8cStatus.datacenters) { + Object.entries(k8cStatus.datacenters).forEach((dcStatus, _) => { + if (dcStatus[1].stargate) { + const progress = dcStatus[1].stargate.progress; + var className = "info"; + switch (progress) { + case "Running": + className = "success"; + break; + } + let stargateState = new Map([ + ["className", className], + ["progress", progress], + ["readyReplicasRatio", dcStatus[1].stargate.readyReplicasRatio] + ]); + stargateStatuses.set(dcStatus[0], stargateState); + } + }); + }; + + var badges = Array.from(stargateStatuses.keys()).map(dc => { + return ( + + ); + }) + + return
{badges}
; +} \ No newline at end of file diff --git a/k8ssandra/src/k8c-store.ts b/k8ssandra/src/k8c-store.ts new file mode 100644 index 0000000..c6a86a6 --- /dev/null +++ b/k8ssandra/src/k8c-store.ts @@ -0,0 +1,14 @@ +import { Renderer} from "@k8slens/extensions"; +import { K8ssandraCluster } from "./k8c"; + +export class K8ssandraClusterApi extends Renderer.K8sApi.KubeApi { +} + +export class K8ssandraClusterStore extends Renderer.K8sApi.KubeObjectStore { + api = new K8ssandraClusterApi({ + objectConstructor: K8ssandraCluster + }) +} + +export const k8ssandraClusterStore = new K8ssandraClusterStore(); +Renderer.K8sApi.apiManager.registerStore(k8ssandraClusterStore); diff --git a/k8ssandra/src/k8c.ts b/k8ssandra/src/k8c.ts new file mode 100644 index 0000000..8574a8b --- /dev/null +++ b/k8ssandra/src/k8c.ts @@ -0,0 +1,80 @@ +import { Renderer} from "@k8slens/extensions"; + +export class K8ssandraCluster extends Renderer.K8sApi.KubeObject { + static kind = "K8ssandraCluster" + static namespaced = true + static apiBase = "/apis/k8ssandra.io/v1alpha1/k8ssandraclusters" + + 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: { + cassandra: { + config: { + cassandraYaml: { + [key: string]: any; + }, + jvmOptions: { + [key: string]: string; + }, + }, + datacenters: { + config: K8ssandraCluster["spec"]["cassandra"]["config"]; + size: number; + metadata: { + name: string; + }, + stopped: boolean; + storageConfig: K8ssandraCluster["spec"]["cassandra"]["storageConfig"]; + }[]; + serverVersion: string; + storageConfig: { + cassandraDataVolumeClaimSpec: { + accessModes: string[]; + resources: { + requests: { + storage: string; + }; + }; + storageClassName: string; + }; + }; + } + } + status: { + datacenters: { + [key: string]: { + cassandra: { + conditions: { + lastTransitionTime: string; + message: string; + reason: string; + status: string; + type?: string; + }[]; + cassandraOperatorProgress: string; + }, + reaper: { + progress: string; + } + stargate: { + progress: string; + readyReplicasRatio: string; + } + } + } + } +}