From 5316219e30133891a5496d0f3f5962ac4af3cc4e Mon Sep 17 00:00:00 2001 From: Junyu Qian Date: Fri, 10 Jan 2025 18:31:27 +1100 Subject: [PATCH 1/4] basic panel without creating new resc group option --- src/commands/utils/arm.ts | 10 ++ src/panels/CreateFleetPanel.ts | 128 ++++++++++++++++++ .../webviewDefinitions/createFleet.ts | 52 +++++++ src/webview-contract/webviewTypes.ts | 2 + webview-ui/src/main.tsx | 3 + webview-ui/src/manualTest/main.tsx | 3 + 6 files changed, 198 insertions(+) create mode 100644 src/webview-contract/webviewDefinitions/createFleet.ts diff --git a/src/commands/utils/arm.ts b/src/commands/utils/arm.ts index 100c2c759..4bd540447 100644 --- a/src/commands/utils/arm.ts +++ b/src/commands/utils/arm.ts @@ -13,6 +13,7 @@ import { getCredential, getEnvironment } from "../../auth/azureAuth"; import { ReadyAzureSessionProvider } from "../../auth/types"; import { Errorable, getErrorMessage } from "./errorable"; import { ComputeManagementClient } from "@azure/arm-compute"; +import { ContainerServiceFleetClient } from "@azure/arm-containerservicefleet"; export function getSubscriptionClient(sessionProvider: ReadyAzureSessionProvider): SubscriptionClient { return new SubscriptionClient(getCredential(sessionProvider), { endpoint: getArmEndpoint() }); @@ -36,6 +37,15 @@ export function getAksClient( return new ContainerServiceClient(getCredential(sessionProvider), subscriptionId, { endpoint: getArmEndpoint() }); } +export function getAksFleetClient( + sessionProvider: ReadyAzureSessionProvider, + subscriptionId: string, +): ContainerServiceFleetClient { + return new ContainerServiceFleetClient(getCredential(sessionProvider), subscriptionId, { + endpoint: getArmEndpoint(), + }); +} + export function getMonitorClient(sessionProvider: ReadyAzureSessionProvider, subscriptionId: string): MonitorClient { return new MonitorClient(getCredential(sessionProvider), subscriptionId, { endpoint: getArmEndpoint() }); } diff --git a/src/panels/CreateFleetPanel.ts b/src/panels/CreateFleetPanel.ts index 7b9a0690f..8cf209893 100644 --- a/src/panels/CreateFleetPanel.ts +++ b/src/panels/CreateFleetPanel.ts @@ -1,4 +1,132 @@ import { ContainerServiceFleetClient, Fleet } from "@azure/arm-containerservicefleet"; +import { BasePanel, PanelDataProvider } from "./BasePanel"; +import { Uri, window } from "vscode"; +import { MessageHandler, MessageSink } from "../webview-contract/messaging"; +import { + InitialState, + ProgressEventType, + ToVsCodeMsgDef, + ToWebViewMsgDef, + ResourceGroup as WebViewResourceGroup, +} from "../webview-contract/webviewDefinitions/createFleet"; +import { TelemetryDefinition } from "../webview-contract/webviewTypes"; +import { ResourceGroup as ARMResourceGroup, ResourceManagementClient } from "@azure/arm-resources"; +import { ReadyAzureSessionProvider } from "../auth/types"; +import { getAksFleetClient, getResourceManagementClient } from "../commands/utils/arm"; +import { getResourceGroups } from "../commands/utils/resourceGroups"; +import { failed } from "../commands/utils/errorable"; + +export class CreateFleetPanel extends BasePanel<"createFleet"> { + constructor(extensionUri: Uri) { + super(extensionUri, "createFleet", { + getLocationsResponse: null, + getResourceGroupsResponse: null, + progressUpdate: null, + }); + } +} + +export class CreateFleetDataProvider implements PanelDataProvider<"createFleet"> { + private readonly resourceManagementClient: ResourceManagementClient; + private readonly fleetClient: ContainerServiceFleetClient; + + constructor( + private readonly sessionProvider: ReadyAzureSessionProvider, + private readonly subscriptionId: string, + private readonly subscriptionName: string, + ) { + this.resourceManagementClient = getResourceManagementClient(sessionProvider, this.subscriptionId); + this.fleetClient = getAksFleetClient(sessionProvider, this.subscriptionId); + } + + getTitle(): string { + return `Create Fleet in ${this.subscriptionName}`; + } + + getInitialState(): InitialState { + return { + subscriptionId: this.subscriptionId, + subscriptionName: this.subscriptionName, + }; + } + + getTelemetryDefinition(): TelemetryDefinition<"createFleet"> { + return { + getResourceGroupsRequest: false, + getLocationsRequest: false, + createFleetRequest: true, + }; + } + + getMessageHandler(webview: MessageSink): MessageHandler { + return { + getLocationsRequest: () => this.handleGetLocationsRequest(webview), + getResourceGroupsRequest: () => this.handleGetResourceGroupsRequest(webview), + createFleetRequest: (args) => + this.handleCreateFleetRequest(args.resourceGroupName, args.location, args.name), + }; + } + + private async handleGetLocationsRequest(webview: MessageSink) { + const provider = await this.resourceManagementClient.providers.get("Microsoft.ContainerService"); + const resourceTypes = provider.resourceTypes?.filter((type) => type.resourceType === "fleets"); + if (!resourceTypes || resourceTypes.length > 1) { + window.showErrorMessage( + `Unexpected number of fleets resource types for provider (${resourceTypes?.length || 0}).`, + ); + return; + } + + const resourceType = resourceTypes[0]; + if (!resourceType.locations || resourceType.locations.length === 0) { + window.showErrorMessage("No locations found for fleets resource type."); + return; + } + + webview.postGetLocationsResponse({ locations: resourceType.locations }); + } + + private async handleGetResourceGroupsRequest(webview: MessageSink) { + const groups = await getResourceGroups(this.sessionProvider, this.subscriptionId); + if (failed(groups)) { + webview.postProgressUpdate({ + event: ProgressEventType.Failed, + operationDescription: "Retriving resource groups for fleet creation", + errorMessage: groups.error, + deploymentPortalUrl: null, + createdFleet: null, + }); + return; + } + + const usableGroups = groups.result.filter(isValidResourceGroup).map((group) => ({ + label: `${group.name} (${group.location})`, + name: group.name, + location: group.location, + })); + + webview.postGetResourceGroupsResponse({ groups: usableGroups }); + } + + private async handleCreateFleetRequest(resourceGroupName: string, location: string, name: string) { + const resource = { + location: location, + }; + + try { + await createFleet(this.fleetClient, resourceGroupName, name, resource); + } catch (error) { + console.log(error); + } + } +} + +function isValidResourceGroup(group: ARMResourceGroup): group is WebViewResourceGroup { + if (!group.name || !group.id) return false; + if (group.name?.startsWith("MC_")) return false; + + return true; +} export async function createFleet( client: ContainerServiceFleetClient, diff --git a/src/webview-contract/webviewDefinitions/createFleet.ts b/src/webview-contract/webviewDefinitions/createFleet.ts new file mode 100644 index 000000000..5aad69217 --- /dev/null +++ b/src/webview-contract/webviewDefinitions/createFleet.ts @@ -0,0 +1,52 @@ +import { WebviewDefinition } from "../webviewTypes"; + +export interface InitialState { + subscriptionId: string; + subscriptionName: string; +} + +export interface ResourceGroup { + name: string; + location: string; +} + +export enum ProgressEventType { + InProgress, + Cancelled, + Failed, + Success, +} + +export type CreatedFleet = { + portalUrl: string; +}; + +export interface CreateFleetParams { + resourceGroupName: string; + location: string; + name: string; +} + +export type ToVsCodeMsgDef = { + getLocationsRequest: void; + getResourceGroupsRequest: void; + createFleetRequest: CreateFleetParams; +}; + +export type ToWebViewMsgDef = { + getLocationsResponse: { + locations: string[]; + }; + getResourceGroupsResponse: { + groups: ResourceGroup[]; + }; + progressUpdate: { + operationDescription: string; + event: ProgressEventType; + errorMessage: string | null; + deploymentPortalUrl: string | null; + createdFleet: CreatedFleet | null; + }; +}; + +export type CreateFleetDefinition = WebviewDefinition; diff --git a/src/webview-contract/webviewTypes.ts b/src/webview-contract/webviewTypes.ts index 86e789f2c..a0ce5e2a3 100644 --- a/src/webview-contract/webviewTypes.ts +++ b/src/webview-contract/webviewTypes.ts @@ -18,6 +18,7 @@ import { PeriscopeDefinition } from "./webviewDefinitions/periscope"; import { RetinaCaptureDefinition } from "./webviewDefinitions/retinaCapture"; import { TCPDumpDefinition } from "./webviewDefinitions/tcpDump"; import { TestStyleViewerDefinition } from "./webviewDefinitions/testStyleViewer"; +import { CreateFleetDefinition } from "./webviewDefinitions/createFleet"; /** * Groups all the related types for a single webview. @@ -56,6 +57,7 @@ type AllWebviewDefinitions = { kaitoModels: KaitoModelsDefinition; kaitoManage: KaitoManageDefinition; kaitoTest: KaitoTestDefinition; + createFleet: CreateFleetDefinition; }; type ContentIdLookup = { diff --git a/webview-ui/src/main.tsx b/webview-ui/src/main.tsx index 677a53d3f..c34a5e742 100644 --- a/webview-ui/src/main.tsx +++ b/webview-ui/src/main.tsx @@ -68,6 +68,9 @@ function getVsCodeContent(): JSX.Element { kaitoModels: () => , kaitoManage: () => , kaitoTest: () => , + createFleet: function (): JSX.Element { + throw new Error("Function not implemented."); + }, }; return rendererLookup[vscodeContentId](); diff --git a/webview-ui/src/manualTest/main.tsx b/webview-ui/src/manualTest/main.tsx index 91f4648cb..50c64c69c 100644 --- a/webview-ui/src/manualTest/main.tsx +++ b/webview-ui/src/manualTest/main.tsx @@ -56,6 +56,9 @@ const contentTestScenarios: Record = { kaitoModels: getKaitoModelScenarios(), kaitoManage: getKaitoManageScenarios(), kaitoTest: getKaitoTestScenarios(), + // Only to ensure the dependencies are resolved for compilation. + // TODO: Replace with the actual scenarios when available. + createFleet: [], // Fleet scenarios are not yet available. }; const testScenarios = Object.values(contentTestScenarios).flatMap((s) => s); From a1051c316eee33621b5e528f8d3c8c9405ac2024 Mon Sep 17 00:00:00 2001 From: Junyu Qian Date: Mon, 13 Jan 2025 11:37:24 +1100 Subject: [PATCH 2/4] adding comments for hardcoded parts --- webview-ui/src/main.tsx | 6 +++++- webview-ui/src/manualTest/main.tsx | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/webview-ui/src/main.tsx b/webview-ui/src/main.tsx index c34a5e742..b7602f06d 100644 --- a/webview-ui/src/main.tsx +++ b/webview-ui/src/main.tsx @@ -69,7 +69,11 @@ function getVsCodeContent(): JSX.Element { kaitoManage: () => , kaitoTest: () => , createFleet: function (): JSX.Element { - throw new Error("Function not implemented."); + // Hardcoded: Only to ensure the dependencies are resolved for compilation. + // TODO: Replace with the actual scenarios when available. + return
createFleet testcases not implemented.
; // createFleet scenarios are not yet available. + // Testcases for createFleet will be added in the next PR, together with the webpage for user input. + // User experience will not be affected, as the right-click entry point for createFleet is not yet visible. }, }; diff --git a/webview-ui/src/manualTest/main.tsx b/webview-ui/src/manualTest/main.tsx index 50c64c69c..c38cca711 100644 --- a/webview-ui/src/manualTest/main.tsx +++ b/webview-ui/src/manualTest/main.tsx @@ -56,9 +56,11 @@ const contentTestScenarios: Record = { kaitoModels: getKaitoModelScenarios(), kaitoManage: getKaitoManageScenarios(), kaitoTest: getKaitoTestScenarios(), - // Only to ensure the dependencies are resolved for compilation. + // Hardcoded createFleet: Only to ensure the dependencies are resolved for compilation. // TODO: Replace with the actual scenarios when available. - createFleet: [], // Fleet scenarios are not yet available. + createFleet: [], // createFleet scenarios are not yet available. + // Testcases for createFleet will be added in the next PR, together with the webpage for user input. + // User experience will not be affected, as the right-click entry point for createFleet is not yet visible. }; const testScenarios = Object.values(contentTestScenarios).flatMap((s) => s); From fba9774ed2919260c29ba10377f49de196c8ec6b Mon Sep 17 00:00:00 2001 From: Junyu Qian Date: Tue, 14 Jan 2025 15:52:50 +1100 Subject: [PATCH 3/4] improve createFleetPanel.ts --- src/panels/CreateFleetPanel.ts | 40 ++++++++++++++-------------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/src/panels/CreateFleetPanel.ts b/src/panels/CreateFleetPanel.ts index 8cf209893..586b0725f 100644 --- a/src/panels/CreateFleetPanel.ts +++ b/src/panels/CreateFleetPanel.ts @@ -7,18 +7,19 @@ import { ProgressEventType, ToVsCodeMsgDef, ToWebViewMsgDef, - ResourceGroup as WebViewResourceGroup, } from "../webview-contract/webviewDefinitions/createFleet"; import { TelemetryDefinition } from "../webview-contract/webviewTypes"; -import { ResourceGroup as ARMResourceGroup, ResourceManagementClient } from "@azure/arm-resources"; +import { ResourceManagementClient } from "@azure/arm-resources"; import { ReadyAzureSessionProvider } from "../auth/types"; import { getAksFleetClient, getResourceManagementClient } from "../commands/utils/arm"; import { getResourceGroups } from "../commands/utils/resourceGroups"; import { failed } from "../commands/utils/errorable"; -export class CreateFleetPanel extends BasePanel<"createFleet"> { +const contentId = "createFleet"; + +export class CreateFleetPanel extends BasePanel { constructor(extensionUri: Uri) { - super(extensionUri, "createFleet", { + super(extensionUri, contentId, { getLocationsResponse: null, getResourceGroupsResponse: null, progressUpdate: null, @@ -26,7 +27,7 @@ export class CreateFleetPanel extends BasePanel<"createFleet"> { } } -export class CreateFleetDataProvider implements PanelDataProvider<"createFleet"> { +export class CreateFleetDataProvider implements PanelDataProvider { private readonly resourceManagementClient: ResourceManagementClient; private readonly fleetClient: ContainerServiceFleetClient; @@ -50,7 +51,7 @@ export class CreateFleetDataProvider implements PanelDataProvider<"createFleet"> }; } - getTelemetryDefinition(): TelemetryDefinition<"createFleet"> { + getTelemetryDefinition(): TelemetryDefinition { return { getResourceGroupsRequest: false, getLocationsRequest: false, @@ -91,7 +92,7 @@ export class CreateFleetDataProvider implements PanelDataProvider<"createFleet"> if (failed(groups)) { webview.postProgressUpdate({ event: ProgressEventType.Failed, - operationDescription: "Retriving resource groups for fleet creation", + operationDescription: "Retrieving resource groups for fleet creation", errorMessage: groups.error, deploymentPortalUrl: null, createdFleet: null, @@ -99,11 +100,13 @@ export class CreateFleetDataProvider implements PanelDataProvider<"createFleet"> return; } - const usableGroups = groups.result.filter(isValidResourceGroup).map((group) => ({ - label: `${group.name} (${group.location})`, - name: group.name, - location: group.location, - })); + const usableGroups = groups.result + .map((group) => ({ + label: `${group.name} (${group.location})`, + name: group.name, + location: group.location, + })) + .sort((a, b) => (a.name > b.name ? 1 : -1)); webview.postGetResourceGroupsResponse({ groups: usableGroups }); } @@ -113,21 +116,10 @@ export class CreateFleetDataProvider implements PanelDataProvider<"createFleet"> location: location, }; - try { - await createFleet(this.fleetClient, resourceGroupName, name, resource); - } catch (error) { - console.log(error); - } + await createFleet(this.fleetClient, resourceGroupName, name, resource); } } -function isValidResourceGroup(group: ARMResourceGroup): group is WebViewResourceGroup { - if (!group.name || !group.id) return false; - if (group.name?.startsWith("MC_")) return false; - - return true; -} - export async function createFleet( client: ContainerServiceFleetClient, resourceGroupName: string, From 6830d47b24ffc558959e2a366d1e8a9a2751539f Mon Sep 17 00:00:00 2001 From: Junyu Qian Date: Wed, 15 Jan 2025 10:18:13 +1100 Subject: [PATCH 4/4] matching panel type with webviewTypes --- src/panels/CreateFleetPanel.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/panels/CreateFleetPanel.ts b/src/panels/CreateFleetPanel.ts index 586b0725f..4c05384ea 100644 --- a/src/panels/CreateFleetPanel.ts +++ b/src/panels/CreateFleetPanel.ts @@ -15,11 +15,9 @@ import { getAksFleetClient, getResourceManagementClient } from "../commands/util import { getResourceGroups } from "../commands/utils/resourceGroups"; import { failed } from "../commands/utils/errorable"; -const contentId = "createFleet"; - -export class CreateFleetPanel extends BasePanel { +export class CreateFleetPanel extends BasePanel<"createFleet"> { constructor(extensionUri: Uri) { - super(extensionUri, contentId, { + super(extensionUri, "createFleet", { getLocationsResponse: null, getResourceGroupsResponse: null, progressUpdate: null, @@ -27,7 +25,7 @@ export class CreateFleetPanel extends BasePanel { } } -export class CreateFleetDataProvider implements PanelDataProvider { +export class CreateFleetDataProvider implements PanelDataProvider<"createFleet"> { private readonly resourceManagementClient: ResourceManagementClient; private readonly fleetClient: ContainerServiceFleetClient; @@ -51,7 +49,7 @@ export class CreateFleetDataProvider implements PanelDataProvider { + getTelemetryDefinition(): TelemetryDefinition<"createFleet"> { return { getResourceGroupsRequest: false, getLocationsRequest: false,