Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create Fleet: panel implementation #1161

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/commands/utils/arm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() });
Expand All @@ -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(),
});
JunyuQian marked this conversation as resolved.
Show resolved Hide resolved
}

export function getMonitorClient(sessionProvider: ReadyAzureSessionProvider, subscriptionId: string): MonitorClient {
return new MonitorClient(getCredential(sessionProvider), subscriptionId, { endpoint: getArmEndpoint() });
}
Expand Down
120 changes: 120 additions & 0 deletions src/panels/CreateFleetPanel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,124 @@
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";

const contentId = "createFleet";

export class CreateFleetPanel extends BasePanel<typeof contentId> {
Copy link
Collaborator

@hsubramanianaks hsubramanianaks Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should be extends BasePanel<"createFleet"> would work, base panel is abstract class that has the basic methods for a panel and data providers which is used for the communication between main extension and webview part.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this type refers to the one you have defined it in webviewTypes.ts file @JunyuQian

image

constructor(extensionUri: Uri) {
super(extensionUri, contentId, {
getLocationsResponse: null,
getResourceGroupsResponse: null,
progressUpdate: null,
});
}
}

export class CreateFleetDataProvider implements PanelDataProvider<typeof contentId> {
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<typeof contentId> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is typeof contentId going to work?
I don't know typescript well enough to answer that, but syntax wise, typeof contentId looks like it would return a string type, and not the value "createFleet". is that a problem?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering that too, but I think it does make sense with typeof. When you look at how the generic type is used, it's matched up to the keys of this AllWebviewDefinitions object, which are themselves strings. You can in Typescript define a type which is just a union of string values (e.g. for an enum):

type colorEnum = "Red" | "Blue" | "Green"

So this is doing effectively the same thing as that, just with only a single legal value.

Copy link
Collaborator

@hsubramanianaks hsubramanianaks Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TelemetryDefinition<"createFleet"> would work here as well. @JunyuQian similar to the other comment.

return {
getResourceGroupsRequest: false,
getLocationsRequest: false,
createFleetRequest: true,
};
}

getMessageHandler(webview: MessageSink<ToWebViewMsgDef>): MessageHandler<ToVsCodeMsgDef> {
return {
getLocationsRequest: () => this.handleGetLocationsRequest(webview),
getResourceGroupsRequest: () => this.handleGetResourceGroupsRequest(webview),
createFleetRequest: (args) =>
this.handleCreateFleetRequest(args.resourceGroupName, args.location, args.name),
};
}

private async handleGetLocationsRequest(webview: MessageSink<ToWebViewMsgDef>) {
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<ToWebViewMsgDef>) {
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,
Expand Down
52 changes: 52 additions & 0 deletions src/webview-contract/webviewDefinitions/createFleet.ts
Original file line number Diff line number Diff line change
@@ -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 {
JunyuQian marked this conversation as resolved.
Show resolved Hide resolved
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<InitialState, ToVsCodeMsgDef, ToWebViewMsgDef>;
2 changes: 2 additions & 0 deletions src/webview-contract/webviewTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -56,6 +57,7 @@ type AllWebviewDefinitions = {
kaitoModels: KaitoModelsDefinition;
kaitoManage: KaitoManageDefinition;
kaitoTest: KaitoTestDefinition;
createFleet: CreateFleetDefinition;
};

type ContentIdLookup = {
Expand Down
7 changes: 7 additions & 0 deletions webview-ui/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ function getVsCodeContent(): JSX.Element {
kaitoModels: () => <KaitoModels {...getInitialState()} />,
kaitoManage: () => <KaitoManage {...getInitialState()} />,
kaitoTest: () => <KaitoTest {...getInitialState()} />,
createFleet: function (): JSX.Element {
// Hardcoded: Only to ensure the dependencies are resolved for compilation.
// TODO: Replace with the actual scenarios when available.
return <div>createFleet testcases not implemented.</div>; // 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]();
Expand Down
5 changes: 5 additions & 0 deletions webview-ui/src/manualTest/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ const contentTestScenarios: Record<ContentId, Scenario[]> = {
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);
Expand Down
Loading