diff --git a/src/commands/utils/arm.ts b/src/commands/utils/arm.ts index 100c2c75..4bd54044 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 7b9a0690..4c05384e 100644 --- a/src/panels/CreateFleetPanel.ts +++ b/src/panels/CreateFleetPanel.ts @@ -1,4 +1,122 @@ 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, +} from "../webview-contract/webviewDefinitions/createFleet"; +import { TelemetryDefinition } from "../webview-contract/webviewTypes"; +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"> { + 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: "Retrieving resource groups for fleet creation", + errorMessage: groups.error, + deploymentPortalUrl: null, + createdFleet: null, + }); + return; + } + + 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 }); + } + + private async handleCreateFleetRequest(resourceGroupName: string, location: string, name: string) { + const resource = { + location: location, + }; + + await createFleet(this.fleetClient, resourceGroupName, name, resource); + } +} 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 00000000..5aad6921 --- /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 86e789f2..a0ce5e2a 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 677a53d3..b7602f06 100644 --- a/webview-ui/src/main.tsx +++ b/webview-ui/src/main.tsx @@ -68,6 +68,13 @@ function getVsCodeContent(): JSX.Element { kaitoModels: () => , kaitoManage: () => , kaitoTest: () => , + createFleet: function (): JSX.Element { + // 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. + }, }; return rendererLookup[vscodeContentId](); diff --git a/webview-ui/src/manualTest/main.tsx b/webview-ui/src/manualTest/main.tsx index 91f4648c..c38cca71 100644 --- a/webview-ui/src/manualTest/main.tsx +++ b/webview-ui/src/manualTest/main.tsx @@ -56,6 +56,11 @@ const contentTestScenarios: Record = { kaitoModels: getKaitoModelScenarios(), kaitoManage: getKaitoManageScenarios(), kaitoTest: getKaitoTestScenarios(), + // Hardcoded createFleet: Only to ensure the dependencies are resolved for compilation. + // TODO: Replace with the actual scenarios when 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);