diff --git a/src/panels/CreateClusterPanel.ts b/src/panels/CreateClusterPanel.ts index 37cf29d5..e1f10600 100644 --- a/src/panels/CreateClusterPanel.ts +++ b/src/panels/CreateClusterPanel.ts @@ -2,24 +2,23 @@ import { ContainerServiceClient, KubernetesVersion } from "@azure/arm-containers import { ResourceGroup as ARMResourceGroup, ResourceManagementClient } from "@azure/arm-resources"; import { RestError } from "@azure/storage-blob"; import { Uri, window } from "vscode"; +import { getEnvironment } from "../auth/azureAuth"; +import { ReadyAzureSessionProvider } from "../auth/types"; +import { getAksClient, getResourceManagementClient } from "../commands/utils/arm"; +import { getPortalResourceUrl } from "../commands/utils/env"; import { failed, getErrorMessage } from "../commands/utils/errorable"; +import { getResourceGroups } from "../commands/utils/resourceGroups"; import { MessageHandler, MessageSink } from "../webview-contract/messaging"; import { InitialState, - Preset, ProgressEventType, ToVsCodeMsgDef, ToWebViewMsgDef, ResourceGroup as WebviewResourceGroup, } from "../webview-contract/webviewDefinitions/createCluster"; -import { BasePanel, PanelDataProvider } from "./BasePanel"; -import { ClusterDeploymentBuilder, ClusterSpec } from "./utilities/ClusterSpecCreationBuilder"; -import { getPortalResourceUrl } from "../commands/utils/env"; import { TelemetryDefinition } from "../webview-contract/webviewTypes"; -import { getResourceGroups } from "../commands/utils/resourceGroups"; -import { getAksClient, getResourceManagementClient } from "../commands/utils/arm"; -import { getEnvironment } from "../auth/azureAuth"; -import { ReadyAzureSessionProvider } from "../auth/types"; +import { BasePanel, PanelDataProvider } from "./BasePanel"; +import { ClusterDeploymentBuilder, ClusterSpec, Preset } from "./utilities/ClusterSpecCreationBuilder"; export class CreateClusterPanel extends BasePanel<"createCluster"> { constructor(extensionUri: Uri) { diff --git a/src/panels/KaitoPanel.ts b/src/panels/KaitoPanel.ts index 593c6e1c..3a0a4a30 100644 --- a/src/panels/KaitoPanel.ts +++ b/src/panels/KaitoPanel.ts @@ -1,13 +1,16 @@ +import { FeatureClient } from "@azure/arm-features"; +import { ResourceManagementClient } from "@azure/arm-resources"; +import { RestError } from "@azure/storage-blob"; import * as vscode from "vscode"; import { ReadyAzureSessionProvider } from "../auth/types"; +import { getFeatureClient, getResourceManagementClient } from "../commands/utils/arm"; +import { getErrorMessage } from "../commands/utils/errorable"; +import { longRunning } from "../commands/utils/host"; import { MessageHandler, MessageSink } from "../webview-contract/messaging"; import { InitialState, ToVsCodeMsgDef, ToWebViewMsgDef } from "../webview-contract/webviewDefinitions/kaito"; import { TelemetryDefinition } from "../webview-contract/webviewTypes"; import { BasePanel, PanelDataProvider } from "./BasePanel"; -import { getFeatureClient, getResourceManagementClient } from "../commands/utils/arm"; -import { FeatureClient } from "@azure/arm-features"; -import { longRunning } from "../commands/utils/host"; -import { ResourceManagementClient } from "@azure/arm-resources"; +import { ClusterDeploymentBuilder, ClusterSpec, Preset } from "./utilities/ClusterSpecCreationBuilder"; export class KaitoPanel extends BasePanel<"kaito"> { constructor(extensionUri: vscode.Uri) { @@ -107,13 +110,55 @@ export class KaitoPanelDataProvider implements PanelDataProvider<"kaito"> { }); } private async handleKaitoInstallation(webview: MessageSink) { - // register feature - const featureRegister = await longRunning(`Register KAITO Feature.`, () => - this.featureClient.features.register("Microsoft.ContainerService", "AIToolchainOperatorPreview"), + // const featureRegister = await longRunning(`Register KAITO Feature.`, () => + // this.featureClient.features.register("Microsoft.ContainerService", "AIToolchainOperatorPreview"), + // ); + + // if (featureRegister.properties?.state !== "Registered") { + // webview.postKaitoInstallProgressUpdate({ + // operationDescription: "Installing Kaito", + // event: 3, + // errorMessage: "Failed to register feature", + // models: [], + // }); + // return; + // } + + // // Install kaito enablement + // // Get current json + // const currentJson = await longRunning(`Get current json.`, () => { + // return this.resourceManagementClient.resources.getById(this.armId, "2023-08-01"); + // }); + // console.log(currentJson); + + // // Update json + // if (currentJson.properties) { + // currentJson.properties.aiToolchainOperatorProfile = { enabled: true }; + // } + + // const updateJson = await longRunning(`Update json.`, () => { + // return this.resourceManagementClient.resources.beginCreateOrUpdateByIdAndWait( + // this.armId, + // "2023-08-01", + // currentJson, + // ); + // }); + // console.log(updateJson); + const subscriptionFeatureRegistrationType = { + properties: {}, + }; + const options = { + subscriptionFeatureRegistrationType, + }; + + const featureRegistrationPoller = await this.featureClient.subscriptionFeatureRegistrations.createOrUpdate( + "Microsoft.ContainerService", + "AIToolchainOperatorPreview", + options, ); - if (featureRegister.properties?.state !== "Registered") { + if (featureRegistrationPoller.properties?.state !== "Registered") { webview.postKaitoInstallProgressUpdate({ operationDescription: "Installing Kaito", event: 3, @@ -123,58 +168,103 @@ export class KaitoPanelDataProvider implements PanelDataProvider<"kaito"> { return; } - // Install kaito enablement // Get current json const currentJson = await longRunning(`Get current json.`, () => { return this.resourceManagementClient.resources.getById(this.armId, "2023-08-01"); }); console.log(currentJson); - // Update json - if (currentJson.properties) { - currentJson.properties.aiToolchainOperatorProfile = { enabled: true }; - } - - const updateJson = await longRunning(`Update json.`, () => { - return this.resourceManagementClient.resources.beginCreateOrUpdateByIdAndWait(this.armId, "2023-08-01", currentJson); - }); - console.log(updateJson); - - // const kaitoEnablement = await longRunning(`Enable KAITO Feature.`, () => - // this.resourceManagementClient.deployments.beginCreateOrUpdate( - // this.resourceGroupName, - // "Microsoft.ContainerService", - // "", - // "providers/Microsoft.ContainerService/enableKaito", - // "2021-11-01-preview", - // {}, - // ), - // ); + const clusterSpec: ClusterSpec = { + location: "eastus2euap", //TODO get location from cluster + name: this.clusterName, + resourceGroupName: this.resourceGroupName, + subscriptionId: this.subscriptionId, + kubernetesVersion: "1.28", // TODO k8s version from cluster + }; - // install kaito - webview.postKaitoInstallProgressUpdate({ - operationDescription: "Installing Kaito", - event: 1, - errorMessage: null, - models: [], - }); + const deploymentName = `${this.clusterName}-${Math.random().toString(36).substring(5)}`; - // simulate kaito installation and success - setTimeout(() => { + const deploymentSpec = new ClusterDeploymentBuilder() + .buildCommonParametersForKaito(clusterSpec) + .buildTemplate(Preset.KaitoAddon) + .getDeployment(); + try { + const poller = await this.resourceManagementClient.deployments.beginCreateOrUpdate( + this.resourceGroupName, + deploymentName, + deploymentSpec, + ); + // kaito installation in progress webview.postKaitoInstallProgressUpdate({ - operationDescription: "Kaito installed", - event: 4, - errorMessage: null, - models: [ - { - family: "family", - modelName: "modelName", - minimumGpu: 1, - kaitoVersion: "v1.0", - modelSource: "modelSource", - }, - ], + operationDescription: "Installing Kaito", + event: 1, + errorMessage: undefined, + models: [], + }); + poller.onProgress((state) => { + if (state.status === "succeeded") { + webview.postKaitoInstallProgressUpdate({ + operationDescription: "Installing Kaito succeeded", + event: 4, + errorMessage: undefined, + models: [], + }); + } else if (state.status === "failed") { + webview.postKaitoInstallProgressUpdate({ + operationDescription: "Installing Kaito failed", + event: 3, + errorMessage: state.error?.message, + models: [], + }); + } }); - }, 5000); + } catch (ex) { + const errorMessage = isInvalidTemplateDeploymentError(ex) + ? getInvalidTemplateErrorMessage(ex) + : getErrorMessage(ex); + vscode.window.showErrorMessage(`Error installing Kaito addon for ${this.clusterName}: ${errorMessage}`); + webview.postKaitoInstallProgressUpdate({ + operationDescription: "Installing Kaito failed", + event: 3, + errorMessage: ex instanceof Error ? ex.message : String(ex), + models: [], + }); + } + } +} + +function getInvalidTemplateErrorMessage(ex: InvalidTemplateDeploymentRestError): string { + const innerDetails = ex.details.error?.details || []; + if (innerDetails.length > 0) { + const details = innerDetails.map((d) => `${d.code}: ${d.message}`).join("\n"); + return `Invalid template:\n${details}`; + } + + const innerError = ex.details.error?.message || ""; + if (innerError) { + return `Invalid template:\n${innerError}`; } + + return `Invalid template: ${getErrorMessage(ex)}`; +} + +type InvalidTemplateDeploymentRestError = RestError & { + details: { + error?: { + code: "InvalidTemplateDeployment"; + message?: string; + details?: { + code?: string; + message?: string; + }[]; + }; + }; +}; + +function isInvalidTemplateDeploymentError(ex: unknown): ex is InvalidTemplateDeploymentRestError { + return isRestError(ex) && ex.code === "InvalidTemplateDeployment"; +} + +function isRestError(ex: unknown): ex is RestError { + return typeof ex === "object" && ex !== null && ex.constructor.name === "RestError"; } diff --git a/src/panels/templates/DevTestCreateCluster.json b/src/panels/templates/DevTestCreateCluster.json index a9d38bf5..a2bbbf24 100644 --- a/src/panels/templates/DevTestCreateCluster.json +++ b/src/panels/templates/DevTestCreateCluster.json @@ -776,6 +776,9 @@ "networkPolicy": "[parameters('networkPolicy')]", "serviceCidr": "[parameters('serviceCidr')]", "dnsServiceIP": "[parameters('dnsServiceIP')]" + }, + "AiToolchainOperatorProfile": { + "enabled": true } }, "tags": "[parameters('clusterTags')]" diff --git a/src/panels/templates/UpdateClusterKaitoAddon.json b/src/panels/templates/UpdateClusterKaitoAddon.json new file mode 100644 index 00000000..a7c01efa --- /dev/null +++ b/src/panels/templates/UpdateClusterKaitoAddon.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "subscriptionId": { + "type": "string", + "metadata": { + "description": "Subscription ID" + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Location for the resources" + } + }, + "resourceName":{ + "type": "string", + "metadata": { + "description": "Name of the resource" + } + }, + + "resourceGroupName":{ + "type": "string", + "metadata": { + "description": "Name of the resource group" + } + }, + + "apiVersion":{ + "type": "string", + "metadata": { + "description": "API version" + } + }, + "kubernetesVersion": { + "type": "string", + "metadata": { + "description": "Kubernetes version" + } + } + }, + "resources": [ + { + "type": "Microsoft.ContainerService/managedClusters", + "apiVersion": "[parameters('apiVersion')]", + "name": "[parameters('resourceName')]", + "properties": { + "aiToolchainOperatorProfile": { + "enabled": true + }, + "oidcIssuerProfile": { + "enabled": true + } + } + } + ] + } \ No newline at end of file diff --git a/src/panels/utilities/ClusterSpecCreationBuilder.ts b/src/panels/utilities/ClusterSpecCreationBuilder.ts index 7cedb0e9..50a4ac59 100644 --- a/src/panels/utilities/ClusterSpecCreationBuilder.ts +++ b/src/panels/utilities/ClusterSpecCreationBuilder.ts @@ -1,6 +1,6 @@ import { Deployment } from "@azure/arm-resources"; -import { Preset } from "../../webview-contract/webviewDefinitions/createCluster"; import devTestTemplate from "../templates/DevTestCreateCluster.json"; +import kaitoTemplate from "../templates/UpdateClusterKaitoAddon.json"; export type ClusterSpec = { location: string; @@ -8,7 +8,7 @@ export type ClusterSpec = { resourceGroupName: string; subscriptionId: string; kubernetesVersion: string; - username: string; + username?: string; }; type TemplateContent = Record; @@ -16,8 +16,14 @@ type TemplateContent = Record; const deploymentApiVersion = "2023-08-01"; const presetTemplates: Record = { dev: devTestTemplate, + kaitoAddon: kaitoTemplate, }; +export enum Preset { + Dev = "dev", + KaitoAddon = "kaitoAddon", +} + export class ClusterDeploymentBuilder { private deployment: Deployment = { properties: { @@ -27,6 +33,32 @@ export class ClusterDeploymentBuilder { }, }; + public buildCommonParametersForKaito(clusterSpec: ClusterSpec): ClusterDeploymentBuilder { + this.deployment.properties.parameters = { + ...this.deployment.properties.parameters, + location: { + value: clusterSpec.location, + }, + resourceName: { + value: clusterSpec.name, + }, + apiVersion: { + value: deploymentApiVersion, + }, + subscriptionId: { + value: clusterSpec.subscriptionId, + }, + resourceGroupName: { + value: clusterSpec.resourceGroupName, + }, + kubernetesVersion: { + value: clusterSpec.kubernetesVersion, + }, + }; + + return this; + } + public buildCommonParameters(clusterSpec: ClusterSpec): ClusterDeploymentBuilder { this.deployment.properties.parameters = { ...this.deployment.properties.parameters, diff --git a/src/webview-contract/webviewDefinitions/createCluster.ts b/src/webview-contract/webviewDefinitions/createCluster.ts index 73c471a7..33e70302 100644 --- a/src/webview-contract/webviewDefinitions/createCluster.ts +++ b/src/webview-contract/webviewDefinitions/createCluster.ts @@ -1,3 +1,4 @@ +import { Preset } from "../../panels/utilities/ClusterSpecCreationBuilder"; import { WebviewDefinition } from "../webviewTypes"; export interface InitialState { @@ -29,9 +30,6 @@ export interface CreateClusterParams { preset: Preset; } -// NOTE: This is intented to be a union of Preset strings, but for now we only have one. -export type Preset = "dev"; - export type ToVsCodeMsgDef = { getLocationsRequest: void; getResourceGroupsRequest: void; diff --git a/src/webview-contract/webviewDefinitions/kaito.ts b/src/webview-contract/webviewDefinitions/kaito.ts index ec59a968..ae19924e 100644 --- a/src/webview-contract/webviewDefinitions/kaito.ts +++ b/src/webview-contract/webviewDefinitions/kaito.ts @@ -34,7 +34,7 @@ export type ToWebViewMsgDef = { kaitoInstallProgressUpdate: { operationDescription: string; event: ProgressEventType; - errorMessage: string | null; + errorMessage: string | undefined; models: ModelDetails[]; }; // to webview during kaito installation getLLMModelsResponse: { diff --git a/webview-ui/src/CreateCluster/CreateClusterInput.tsx b/webview-ui/src/CreateCluster/CreateClusterInput.tsx index 2b0c8a48..f8a1a8b8 100644 --- a/webview-ui/src/CreateCluster/CreateClusterInput.tsx +++ b/webview-ui/src/CreateCluster/CreateClusterInput.tsx @@ -2,20 +2,20 @@ import { faTimesCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { VSCodeButton, VSCodeDropdown, VSCodeOption, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"; import { FormEvent, useState } from "react"; +import { Preset } from "../../../src/panels/utilities/ClusterSpecCreationBuilder"; import { MessageSink } from "../../../src/webview-contract/messaging"; import { CreateClusterParams, - Preset, ResourceGroup, ToVsCodeMsgDef, } from "../../../src/webview-contract/webviewDefinitions/createCluster"; +import { Maybe, isNothing, just, nothing } from "../utilities/maybe"; import { EventHandlers } from "../utilities/state"; import { Validatable, hasMessage, invalid, isValid, isValueSet, missing, unset, valid } from "../utilities/validation"; import styles from "./CreateCluster.module.css"; import { CreateClusterPresetInput } from "./CreateClusterPresetInput"; import { CreateResourceGroupDialog } from "./CreateResourceGroup"; import { EventDef } from "./helpers/state"; -import { Maybe, isNothing, just, nothing } from "../utilities/maybe"; type ChangeEvent = Event | FormEvent; @@ -31,7 +31,7 @@ export function CreateClusterInput(props: CreateClusterInputProps) { const [name, setName] = useState>(unset()); const [isNewResourceGroupDialogShown, setIsNewResourceGroupDialogShown] = useState(false); const [newResourceGroupName, setNewResourceGroupName] = useState(null); - const [presetSelected, setPresetSelected] = useState("dev"); + const [presetSelected, setPresetSelected] = useState(Preset.Dev); const [selectedIndex, setSelectedIndex] = useState(0); const [location, setLocation] = useState>(unset()); diff --git a/webview-ui/src/CreateCluster/CreateClusterPresetInput.tsx b/webview-ui/src/CreateCluster/CreateClusterPresetInput.tsx index 38e61dd5..a7688906 100644 --- a/webview-ui/src/CreateCluster/CreateClusterPresetInput.tsx +++ b/webview-ui/src/CreateCluster/CreateClusterPresetInput.tsx @@ -1,4 +1,4 @@ -import { Preset } from "../../../src/webview-contract/webviewDefinitions/createCluster"; +import { Preset } from "../../../src/panels/utilities/ClusterSpecCreationBuilder"; import { DevTestIcon } from "../icons/DevTestIcon"; import styles from "./CreateCluster.module.css"; @@ -23,7 +23,7 @@ export function CreateClusterPresetInput(props: CreateClusterPresetInputProps) { click here  to visit the Azure Portal. -
handlePresetClick("dev")}> +
handlePresetClick(Preset.Dev)}>
Dev/Test
diff --git a/webview-ui/src/Kaito/state.ts b/webview-ui/src/Kaito/state.ts index 4493e865..6d64c24b 100644 --- a/webview-ui/src/Kaito/state.ts +++ b/webview-ui/src/Kaito/state.ts @@ -7,7 +7,7 @@ export type EventDef = Record; export type KaitoState = InitialState & { operationDescription: string; kaitoInstallStatus: ProgressEventType; - errors: string | null; + errors: string | undefined; models: ModelDetails[]; }; @@ -16,7 +16,7 @@ export const stateUpdater: WebviewStateUpdater<"kaito", EventDef, KaitoState> = ...initialState, operationDescription: "", kaitoInstallStatus: ProgressEventType.NotStarted, - errors: null, + errors: undefined, models: [], }), vscodeMessageHandler: {