Skip to content

Commit

Permalink
Get Namespaces & Create Namespace Functionality [Backend] (#1196)
Browse files Browse the repository at this point in the history
* namespace retrival

* Namespace creation and utility function update
ReinierCC authored Jan 29, 2025

Verified

This commit was signed with the committer’s verified signature.
janicduplessis Janic Duplessis
1 parent 2269500 commit 6eef211
Showing 6 changed files with 125 additions and 2 deletions.
7 changes: 7 additions & 0 deletions src/commands/devhub/aksAutomatedDeployments.ts
Original file line number Diff line number Diff line change
@@ -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);
45 changes: 45 additions & 0 deletions src/commands/utils/clusters.ts
Original file line number Diff line number Diff line change
@@ -474,6 +474,45 @@ export async function getClusterNamespaces(
});
}

export async function createClusterNamespace(
sessionProvider: ReadyAzureSessionProvider,
kubectl: APIAvailable<KubectlV1>,
subscriptionId: string,
resourceGroup: string,
clusterName: string,
namespace: string,
): Promise<Errorable<string>> {
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 &&
52 changes: 51 additions & 1 deletion src/panels/DevHubAutoDeployPanel.ts
Original file line number Diff line number Diff line change
@@ -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, createClusterNamespace } 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<KubectlV1>,
) {}

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,13 +119,56 @@ export class AutomatedDeploymentsDataProvider implements PanelDataProvider<"auto
webview.postGetResourceGroupsResponse(usableGroups);
}

private async handleGetNamespacesRequest(
subscriptionId: string,
resourceGroup: string,
clusterName: string,
webview: MessageSink<ToWebViewMsgDef>,
) {
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<ToWebViewMsgDef>) {
//---Run Neccesary Checks prior to making the call to DevHub to create a workflow ----

//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
Original file line number Diff line number Diff line change
@@ -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;
11 changes: 10 additions & 1 deletion webview-ui/src/AutomatedDeployments/state.ts
Original file line number Diff line number Diff line change
@@ -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<DefinedResourceGroup[]>;
subscriptions: Validatable<Subscription[]>;
namespaces: Validatable<string[]>;

// 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,
});
4 changes: 4 additions & 0 deletions webview-ui/src/manualTest/automatedDeploymentsTests.tsx
Original file line number Diff line number Diff line change
@@ -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
},

0 comments on commit 6eef211

Please sign in to comment.