src/panels/ClusterPropertiesPanel.ts (296 lines of code) (raw):

import { Uri } from "vscode"; import { failed, getErrorMessage } from "../commands/utils/errorable"; import { MessageHandler, MessageSink } from "../webview-contract/messaging"; import { BasePanel, PanelDataProvider } from "./BasePanel"; import { AgentPoolProfileInfo, ClusterInfo, InitialState, KubernetesVersionInfo, ToVsCodeMsgDef, ToWebViewMsgDef, } from "../webview-contract/webviewDefinitions/clusterProperties"; import { ContainerServiceClient, KubernetesVersion, KubernetesVersionListResult, ManagedCluster, ManagedClusterAgentPoolProfile, ManagedClusterUpgradeProfile, } from "@azure/arm-containerservice"; import { getKubernetesVersionInfo, getManagedCluster, getClusterUpgradeProfile } from "../commands/utils/clusters"; import { TelemetryDefinition } from "../webview-contract/webviewTypes"; import { getAksClient } from "../commands/utils/arm"; import { ReadyAzureSessionProvider } from "../auth/types"; import { aksCRUDDiagnostics } from "../commands/detectors/detectors"; import { IActionContext } from "@microsoft/vscode-azext-utils"; export class ClusterPropertiesPanel extends BasePanel<"clusterProperties"> { constructor(extensionUri: Uri) { super(extensionUri, "clusterProperties", { getPropertiesResponse: null, errorNotification: null, upgradeClusterVersionResponse: null, }); } } export class ClusterPropertiesDataProvider implements PanelDataProvider<"clusterProperties"> { private readonly client: ContainerServiceClient; constructor( private readonly sessionProvider: ReadyAzureSessionProvider, readonly subscriptionId: string, readonly resourceGroup: string, readonly clusterName: string, readonly target: unknown, ) { this.client = getAksClient(sessionProvider, subscriptionId); } getTitle(): string { return `Cluster Properties for ${this.clusterName}`; } getInitialState(): InitialState { return { clusterName: this.clusterName, }; } getTelemetryDefinition(): TelemetryDefinition<"clusterProperties"> { return { getPropertiesRequest: false, stopClusterRequest: true, startClusterRequest: true, abortAgentPoolOperation: true, abortClusterOperation: true, reconcileClusterRequest: true, refreshRequest: true, upgradeClusterVersionRequest: true, detectorCRUDRequest: true, }; } getMessageHandler(webview: MessageSink<ToWebViewMsgDef>): MessageHandler<ToVsCodeMsgDef> { return { getPropertiesRequest: () => this.handleGetPropertiesRequest(webview), stopClusterRequest: () => this.handleStopClusterRequest(webview), startClusterRequest: () => this.handleStartClusterRequest(webview), abortAgentPoolOperation: (poolName: string) => this.handleAbortAgentPoolOperation(webview, poolName), abortClusterOperation: () => this.handleAbortClusterOperation(webview), reconcileClusterRequest: () => this.handleReconcileClusterOperation(webview), // refreshRequest is just for telemetry, so it will use the same getPropertiesRequest handler. refreshRequest: () => this.handleGetPropertiesRequest(webview), upgradeClusterVersionRequest: (version: string) => this.handleUpgradeClusterVersion(webview, version), detectorCRUDRequest: () => this.handleDetectorCRUDRequest(this.target), }; } private async handleAbortAgentPoolOperation(webview: MessageSink<ToWebViewMsgDef>, poolName: string) { try { const poller = await this.client.agentPools.beginAbortLatestOperation( this.resourceGroup, this.clusterName, poolName, ); poller.onProgress((state) => { // Note: not handling 'canceled' here because this is a cancel operation. if (state.status === "failed") { const errorMessage = state.error ? getErrorMessage(state.error) : "Unknown error"; webview.postErrorNotification(errorMessage); } }); // Update the cluster properties now the operation has started. await this.readAndPostClusterProperties(webview); // Wait until operation completes. await poller.pollUntilDone(); } catch (ex) { const errorMessage = getErrorMessage(ex); webview.postErrorNotification(errorMessage); } } private async handleAbortClusterOperation(webview: MessageSink<ToWebViewMsgDef>) { try { const poller = await this.client.managedClusters.beginAbortLatestOperation( this.resourceGroup, this.clusterName, ); poller.onProgress((state) => { // Note: not handling 'canceled' here because this is a cancel operation. if (state.status === "failed") { const errorMessage = state.error ? getErrorMessage(state.error) : "Unknown error"; webview.postErrorNotification(errorMessage); } }); // Update the cluster properties now the operation has started. await this.readAndPostClusterProperties(webview); // Wait until operation completes. await poller.pollUntilDone(); } catch (ex) { const errorMessage = getErrorMessage(ex); webview.postErrorNotification(errorMessage); } } private async handleReconcileClusterOperation(webview: MessageSink<ToWebViewMsgDef>) { try { const getClusterInfo = await this.client.managedClusters.get(this.resourceGroup, this.clusterName); const poller = await this.client.managedClusters.beginCreateOrUpdate(this.resourceGroup, this.clusterName, { location: getClusterInfo.location, }); poller.onProgress((state) => { if (state.status === "canceled") { webview.postErrorNotification(`Reconcile Cluster operation on ${this.clusterName} was cancelled.`); } else if (state.status === "failed") { const errorMessage = state.error ? getErrorMessage(state.error) : "Unknown error"; webview.postErrorNotification(errorMessage); } }); // Update the cluster properties now the operation has started. await this.readAndPostClusterProperties(webview); // Wait until operation completes. await poller.pollUntilDone(); } catch (ex) { const errorMessage = getErrorMessage(ex); webview.postErrorNotification(errorMessage); } } private async handleGetPropertiesRequest(webview: MessageSink<ToWebViewMsgDef>) { await this.readAndPostClusterProperties(webview); } private async handleStopClusterRequest(webview: MessageSink<ToWebViewMsgDef>) { try { const poller = await this.client.managedClusters.beginStop(this.resourceGroup, this.clusterName); poller.onProgress((state) => { if (state.status === "canceled") { webview.postErrorNotification(`Stop Cluster operation on ${this.clusterName} was cancelled.`); } else if (state.status === "failed") { const errorMessage = state.error ? getErrorMessage(state.error) : "Unknown error"; webview.postErrorNotification(errorMessage); } }); // Update the cluster properties now the operation has started. await this.readAndPostClusterProperties(webview); // Wait until operation completes. await poller.pollUntilDone(); } catch (ex) { const errorMessage = getErrorMessage(ex); webview.postErrorNotification(errorMessage); } await this.readAndPostClusterProperties(webview); } private async handleStartClusterRequest(webview: MessageSink<ToWebViewMsgDef>) { try { const poller = await this.client.managedClusters.beginStart(this.resourceGroup, this.clusterName); poller.onProgress((state) => { if (state.status === "canceled") { webview.postErrorNotification(`Start Cluster operation on ${this.clusterName} was cancelled.`); } else if (state.status === "failed") { const errorMessage = state.error ? getErrorMessage(state.error) : "Unknown error"; webview.postErrorNotification(errorMessage); } }); // Update the cluster properties now the operation has started. await this.readAndPostClusterProperties(webview); // Wait until operation completes. await poller.pollUntilDone(); } catch (ex) { const errorMessage = getErrorMessage(ex); webview.postErrorNotification(errorMessage); } await this.readAndPostClusterProperties(webview); } private async handleUpgradeClusterVersion(webview: MessageSink<ToWebViewMsgDef>, version: string) { try { const cluster = await getManagedCluster( this.sessionProvider, this.subscriptionId, this.resourceGroup, this.clusterName, ); if (failed(cluster)) { webview.postErrorNotification(cluster.error); return; } // Create update parameters, preserving all other properties const updateParams: ManagedCluster = { ...cluster.result, kubernetesVersion: version, }; const poller = await this.client.managedClusters.beginCreateOrUpdate( this.resourceGroup, this.clusterName, updateParams, ); poller.onProgress((state) => { if (state.status === "canceled") { webview.postErrorNotification(`Upgrade operation on ${this.clusterName} was cancelled.`); webview.postUpgradeClusterVersionResponse(false); return; } else if (state.status === "failed") { const errorMessage = state.error ? getErrorMessage(state.error) : "Unknown error"; webview.postErrorNotification(errorMessage); webview.postUpgradeClusterVersionResponse(false); return; } }); // Update the cluster properties now that the operation has started await this.readAndPostClusterProperties(webview); // Wait until operation completes await poller.pollUntilDone(); webview.postUpgradeClusterVersionResponse(true); } catch (ex) { const errorMessage = getErrorMessage(ex); webview.postErrorNotification(errorMessage); webview.postUpgradeClusterVersionResponse(false); } // Refresh the cluster properties after operation completes await this.readAndPostClusterProperties(webview); } private handleDetectorCRUDRequest(commandTarget: unknown) { // This is a placeholder for the CRUD operation // Implement the CRUD logic here return aksCRUDDiagnostics({} as IActionContext, commandTarget); } private async readAndPostClusterProperties(webview: MessageSink<ToWebViewMsgDef>) { const cluster = await getManagedCluster( this.sessionProvider, this.subscriptionId, this.resourceGroup, this.clusterName, ); if (failed(cluster)) { webview.postErrorNotification(cluster.error); return; } const kubernetesVersion = await getKubernetesVersionInfo(this.client, cluster.result.location); if (failed(kubernetesVersion)) { webview.postErrorNotification(kubernetesVersion.error); return; } const upgradeProfile = await getClusterUpgradeProfile(this.client, this.resourceGroup, this.clusterName); if (failed(upgradeProfile)) { webview.postErrorNotification(upgradeProfile.error); return; } webview.postGetPropertiesResponse( asClusterInfo(cluster.result, kubernetesVersion.result, upgradeProfile.result), ); } } function asClusterInfo( cluster: ManagedCluster, kubernetesVersionList: KubernetesVersionListResult, upgradeProfile: ManagedClusterUpgradeProfile, ): ClusterInfo { return { provisioningState: cluster.provisioningState!, fqdn: cluster.fqdn!, kubernetesVersion: cluster.currentKubernetesVersion!, powerStateCode: cluster.powerState!.code!, agentPoolProfiles: (cluster.agentPoolProfiles || []).map(asPoolProfileInfo), supportedVersions: (kubernetesVersionList.values || []).map(asKubernetesVersionInfo), availableUpgradeVersions: processAndSortUpgradeVersions(upgradeProfile.controlPlaneProfile?.upgrades || []), }; } function processAndSortUpgradeVersions(upgrades: Array<{ kubernetesVersion?: string }>): string[] { return ( upgrades .map((upgrade) => upgrade.kubernetesVersion!) .filter((version): version is string => version !== undefined) // Sort versions in descending order (highest to lowest) .sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: "base" })) ); } function asPoolProfileInfo(pool: ManagedClusterAgentPoolProfile): AgentPoolProfileInfo { return { name: pool.name, nodeImageVersion: pool.nodeImageVersion!, powerStateCode: pool.powerState!.code!, osDiskSizeGB: pool.osDiskSizeGB!, provisioningState: pool.provisioningState!, vmSize: pool.vmSize!, count: pool.count!, osType: pool.osType!, }; } function asKubernetesVersionInfo(version: KubernetesVersion): KubernetesVersionInfo { return { version: version.version || "", patchVersions: version.patchVersions ? Object.keys(version.patchVersions) : [], supportPlan: version.capabilities ? Object.values(version.capabilities.supportPlan || {}) : [], isPreview: version.isPreview || false, }; }