From fd78e49af22d35f8bd5b65d9b412dc9e20480aa6 Mon Sep 17 00:00:00 2001 From: Reinier Cruz Date: Mon, 27 Jan 2025 14:05:04 -0500 Subject: [PATCH 1/2] namespace retrival --- .../devhub/aksAutomatedDeployments.ts | 7 +++++ src/panels/DevHubAutoDeployPanel.ts | 28 +++++++++++++++++++ .../automatedDeployments.ts | 8 ++++++ webview-ui/src/AutomatedDeployments/state.ts | 11 +++++++- .../manualTest/automatedDeploymentsTests.tsx | 4 +++ 5 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/commands/devhub/aksAutomatedDeployments.ts b/src/commands/devhub/aksAutomatedDeployments.ts index 1754d97b8..830039953 100644 --- a/src/commands/devhub/aksAutomatedDeployments.ts +++ b/src/commands/devhub/aksAutomatedDeployments.ts @@ -20,6 +20,12 @@ export default async function aksAutomatedDeployments(_context: IActionContext, return; } + const kubectl = await k8s.extension.kubectl.v1; + if (!kubectl.available) { + vscode.window.showErrorMessage(`Kubectl is unavailable.`); + return; + } + if (!cloudExplorer.available) { vscode.window.showWarningMessage(`Cloud explorer is unavailable.`); return; @@ -98,6 +104,7 @@ export default async function aksAutomatedDeployments(_context: IActionContext, devHubClient, octokitClient, graphClient, + kubectl, ); const panel = new AutomatedDeploymentsPanel(extension.result.extensionUri); diff --git a/src/panels/DevHubAutoDeployPanel.ts b/src/panels/DevHubAutoDeployPanel.ts index 66718e7be..f33354a1e 100644 --- a/src/panels/DevHubAutoDeployPanel.ts +++ b/src/panels/DevHubAutoDeployPanel.ts @@ -18,6 +18,8 @@ import { failed } from "../commands/utils/errorable"; //import * as acrUtils from "../commands/utils/acrs"; import { getResourceGroups } from "../commands/utils/resourceGroups"; import { Client as GraphClient } from "@microsoft/microsoft-graph-client"; +import { getClusterNamespaces } from "../commands/utils/clusters"; +import { APIAvailable, KubectlV1 } from "vscode-kubernetes-tools-api"; export class AutomatedDeploymentsPanel extends BasePanel<"automatedDeployments"> { constructor(extensionUri: vscode.Uri) { @@ -27,6 +29,7 @@ export class AutomatedDeploymentsPanel extends BasePanel<"automatedDeployments"> getSubscriptionsResponse: null, getWorkflowCreationResponse: null, getResourceGroupsResponse: null, + getNamespacesResponse: null, }); } } @@ -38,6 +41,7 @@ export class AutomatedDeploymentsDataProvider implements PanelDataProvider<"auto readonly devHubClient: DeveloperHubServiceClient, readonly octokitClient: Octokit, readonly graphClient: GraphClient, + readonly kubectl: APIAvailable, ) {} getTitle(): string { @@ -56,6 +60,7 @@ export class AutomatedDeploymentsDataProvider implements PanelDataProvider<"auto getSubscriptionsRequest: false, createWorkflowRequest: false, getResourceGroupsRequest: false, + getNamespacesRequest: false, }; } @@ -67,6 +72,8 @@ export class AutomatedDeploymentsDataProvider implements PanelDataProvider<"auto getSubscriptionsRequest: () => this.handleGetSubscriptionsRequest(webview), createWorkflowRequest: () => this.handleCreateWorkflowRequest(webview), getResourceGroupsRequest: () => this.handleGetResourceGroupsRequest(webview), + getNamespacesRequest: (key) => + this.handleGetNamespacesRequest(key.subscriptionId, key.resourceGroup, key.clusterName, webview), }; } @@ -112,6 +119,27 @@ export class AutomatedDeploymentsDataProvider implements PanelDataProvider<"auto webview.postGetResourceGroupsResponse(usableGroups); } + private async handleGetNamespacesRequest( + subscriptionId: string, + resourceGroup: string, + clusterName: string, + webview: MessageSink, + ) { + const namespacesResult = await getClusterNamespaces( + this.sessionProvider, + this.kubectl, + subscriptionId, + resourceGroup, + clusterName, + ); + if (failed(namespacesResult)) { + vscode.window.showErrorMessage("Error fetching namespaces: ", namespacesResult.error); + return; + } + + webview.postGetNamespacesResponse(namespacesResult.result); + } + private async handleCreateWorkflowRequest(webview: MessageSink) { //---Run Neccesary Checks prior to making the call to DevHub to create a workflow ---- diff --git a/src/webview-contract/webviewDefinitions/automatedDeployments.ts b/src/webview-contract/webviewDefinitions/automatedDeployments.ts index 3bed05be7..042fc50fb 100644 --- a/src/webview-contract/webviewDefinitions/automatedDeployments.ts +++ b/src/webview-contract/webviewDefinitions/automatedDeployments.ts @@ -16,18 +16,26 @@ export interface ResourceGroup { location: string; } +export type ClusterKey = { + subscriptionId: string; + resourceGroup: string; + clusterName: string; +}; + // Define messages sent from the webview to the VS Code extension export type ToVsCodeMsgDef = { getGitHubReposRequest: void; getSubscriptionsRequest: void; createWorkflowRequest: void; getResourceGroupsRequest: void; + getNamespacesRequest: ClusterKey; }; // Define messages sent from the VS Code extension to the webview export type ToWebViewMsgDef = { getGitHubReposResponse: { repos: string[] }; getSubscriptionsResponse: Subscription[]; + getNamespacesResponse: string[]; //getAcrsResponse: string[]; getResourceGroupsResponse: DefinedResourceGroup[]; getWorkflowCreationResponse: string; diff --git a/webview-ui/src/AutomatedDeployments/state.ts b/webview-ui/src/AutomatedDeployments/state.ts index b0500fe92..c165319f3 100644 --- a/webview-ui/src/AutomatedDeployments/state.ts +++ b/webview-ui/src/AutomatedDeployments/state.ts @@ -1,6 +1,6 @@ import { WebviewStateUpdater } from "../utilities/state"; import { getWebviewMessageContext } from "../utilities/vscode"; -import { Validatable, unset, valid } from "../utilities/validation"; +import { Validatable, unset, valid, missing } from "../utilities/validation"; import { NewOrExisting, Subscription } from "../../../src/webview-contract/webviewDefinitions/draft/types"; import { DefinedResourceGroup } from "../../../src/commands/utils/resourceGroups"; @@ -30,6 +30,7 @@ export type AutomatedDeploymentsState = { //azureReferenceData: AzureReferenceData; resourceGroups: Validatable; subscriptions: Validatable; + namespaces: Validatable; // Properties waiting to be automatically selected when data is available //pendingSelection: InitialSelection; @@ -61,6 +62,7 @@ export const stateUpdater: WebviewStateUpdater<"automatedDeployments", EventDef, resourceGroups: unset(), subscriptions: unset(), + namespaces: unset(), //Selected Items selectedWorkflowName: unset(), @@ -91,6 +93,12 @@ export const stateUpdater: WebviewStateUpdater<"automatedDeployments", EventDef, ...state, resourceGroups: valid(groups), }), + getNamespacesResponse: (state, namespaces) => ({ + ...state, + namespaces: Array.isArray(namespaces) + ? valid(namespaces) + : missing("Namespaces not in correct type or missing"), + }), getWorkflowCreationResponse: (state, prUrl) => ({ ...state, prUrl: valid(prUrl), @@ -134,4 +142,5 @@ export const vscode = getWebviewMessageContext<"automatedDeployments">({ getSubscriptionsRequest: null, createWorkflowRequest: null, getResourceGroupsRequest: null, + getNamespacesRequest: null, }); diff --git a/webview-ui/src/manualTest/automatedDeploymentsTests.tsx b/webview-ui/src/manualTest/automatedDeploymentsTests.tsx index b32057ac0..f09c16c9a 100644 --- a/webview-ui/src/manualTest/automatedDeploymentsTests.tsx +++ b/webview-ui/src/manualTest/automatedDeploymentsTests.tsx @@ -23,6 +23,10 @@ export function getAutomatedDeploymentScenarios() { getSubscriptionsRequest: () => { // implementation here }, + getNamespacesRequest: () => { + console.log("Returning namespaces from getNamespacesRequest"); + webview.postGetNamespacesResponse(["namespace1", "namespace2", "bestnamespaceever-11"]); + }, createWorkflowRequest: () => { // implementation here }, From 6eedefd908debcfcac6ca9ac66f75582c5d558a0 Mon Sep 17 00:00:00 2001 From: Reinier Cruz Date: Mon, 27 Jan 2025 15:25:36 -0500 Subject: [PATCH 2/2] Namespace creation and utility function update --- src/commands/utils/clusters.ts | 45 +++++++++++++++++++++++++++++ src/panels/DevHubAutoDeployPanel.ts | 26 +++++++++++++++-- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/commands/utils/clusters.ts b/src/commands/utils/clusters.ts index 3057019fa..51e195a8c 100644 --- a/src/commands/utils/clusters.ts +++ b/src/commands/utils/clusters.ts @@ -474,6 +474,45 @@ export async function getClusterNamespaces( }); } +export async function createClusterNamespace( + sessionProvider: ReadyAzureSessionProvider, + kubectl: APIAvailable, + subscriptionId: string, + resourceGroup: string, + clusterName: string, + namespace: string, +): Promise> { + if (!validateNamespaceName(namespace)) { + return { succeeded: false, error: `Invalid namespace name: ${namespace}` }; + } + + const cluster = await getManagedCluster(sessionProvider, subscriptionId, resourceGroup, clusterName); + if (failed(cluster)) { + return cluster; + } + + const kubeconfig = await getKubeconfigYaml(sessionProvider, subscriptionId, resourceGroup, cluster.result); + if (failed(kubeconfig)) { + return kubeconfig; + } + + return await withOptionalTempFile(kubeconfig.result, "yaml", async (kubeconfigPath) => { + const command = `create namespace ${namespace}`; + const output = await invokeKubectlCommand(kubectl, kubeconfigPath, command); + + if (output.succeeded) { + return { succeeded: true, result: `Namespace ${namespace} created` }; + } + + //Check For Namespace Already Exists + if (output.error.includes("AlreadyExists")) { + return { succeeded: true, result: "Namespace already exists" }; + } + + return output; + }); +} + export async function deleteCluster( sessionProvider: ReadyAzureSessionProvider, subscriptionId: string, @@ -568,6 +607,12 @@ export async function filterPodName( return { succeeded: true, result: filterPodName }; } +//Must meet RFC 1123: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +function validateNamespaceName(namespace: string): boolean { + const namespaceRegex = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/; + return namespaceRegex.test(namespace); +} + function isDefinedManagedCluster(cluster: azcs.ManagedCluster): cluster is DefinedManagedCluster { return ( cluster.id !== undefined && diff --git a/src/panels/DevHubAutoDeployPanel.ts b/src/panels/DevHubAutoDeployPanel.ts index f33354a1e..cc0938584 100644 --- a/src/panels/DevHubAutoDeployPanel.ts +++ b/src/panels/DevHubAutoDeployPanel.ts @@ -18,7 +18,7 @@ import { failed } from "../commands/utils/errorable"; //import * as acrUtils from "../commands/utils/acrs"; import { getResourceGroups } from "../commands/utils/resourceGroups"; import { Client as GraphClient } from "@microsoft/microsoft-graph-client"; -import { getClusterNamespaces } from "../commands/utils/clusters"; +import { getClusterNamespaces, createClusterNamespace } from "../commands/utils/clusters"; import { APIAvailable, KubectlV1 } from "vscode-kubernetes-tools-api"; export class AutomatedDeploymentsPanel extends BasePanel<"automatedDeployments"> { @@ -146,7 +146,29 @@ export class AutomatedDeploymentsDataProvider implements PanelDataProvider<"auto //Check if new resource group must be created //Check for isNewNamespace, to see if new namespace must be created. - + const isNewNamespace = true; //Actual Value Provided Later PR //PlaceHolder + if (isNewNamespace) { + //Create New Namespace + const subscriptionId = "feb5b150-60fe-4441-be73-8c02a524f55a"; // These values will be provided to the fuction call from the webview + const resourceGroup = "rei-rg"; //PlaceHolder + const clusterName = "reiCluster"; //PlaceHolder + const namespace = "not-default"; //PlaceHolder + const namespaceCreationResp = await createClusterNamespace( + this.sessionProvider, + this.kubectl, + subscriptionId, + resourceGroup, + clusterName, + namespace, + ); + + if (failed(namespaceCreationResp)) { + console.log("Failed to create namespace: ", namespace, "Error: ", namespaceCreationResp.error); + vscode.window.showErrorMessage(`Failed to create namespace: ${namespace}`); + return; + } + vscode.window.showInformationMessage(namespaceCreationResp.result); + } //Create ACR if required //Verify selected ACR has correct role assignments