Skip to content
Draft
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
18 changes: 18 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,10 @@
{
"command": "aks.aksFleetProperties",
"title": "Show Fleet Properties"
},
{
"command": "aks.deployHeadlamp",
"title": "Deploy Headlamp"
}
],
"menus": {
Expand Down Expand Up @@ -492,6 +496,16 @@
{
"command": "aks.aksFleetProperties",
"when": "view == kubernetes.cloudExplorer && viewItem =~ /aks\\.fleet/i"
},
{
"submenu": "aks.observabilityToolsSubMenu",
"when": "view == kubernetes.cloudExplorer && viewItem =~ /aks\\.cluster/i"
}
],
"aks.observabilityToolsSubMenu": [
{
"command": "aks.deployHeadlamp",
"group": "navigation"
}
],
"aks.createClusterSubMenu": [
Expand Down Expand Up @@ -645,6 +659,10 @@
{
"id": "aks.aksRetinaCaptureSubMenu",
"label": "Run Retina Capture"
},
{
"id": "aks.observabilityToolsSubMenu",
"label": "Observability Tools"
}
]
},
Expand Down
162 changes: 162 additions & 0 deletions src/commands/oservabilitytools/deployHeadlamp/deployHeadlamp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import * as vscode from "vscode";
import * as k8s from "vscode-kubernetes-tools-api";
import { IActionContext } from "@microsoft/vscode-azext-utils";
import * as tmpfile from "../../utils/tempfile";
import { getAksClusterTreeNode, getKubeconfigYaml, getManagedCluster } from "../../utils/clusters";
import { getExtension, longRunning } from "../../utils/host";
import { AksClusterTreeNode } from "../../../tree/aksClusterTreeItem";
import { Errorable, failed } from "../../utils/errorable";
import { invokeKubectlCommand } from "../../utils/kubectl";
import { getEnvironment, getReadySessionProvider } from "../../../auth/azureAuth";
import { HeadlampPanel } from "../../../panels/HeadlampPanel";
import { HeadlampPanelDataProvider } from "../../../panels/HeadlampPanel";

export default async function deployHeadlamp(_context: IActionContext, target: unknown): Promise<void> {
const kubectl = await k8s.extension.kubectl.v1;
if (!kubectl.available) {
return;
}

const sessionProvider = await getReadySessionProvider();
if (failed(sessionProvider)) {
vscode.window.showErrorMessage(sessionProvider.error);
return;
}

const extension = getExtension();
if (failed(extension)) {
vscode.window.showErrorMessage(extension.error);
return;
}

const cloudExplorer = await k8s.extension.cloudExplorer.v1;

const clusterNode = getAksClusterTreeNode(target, cloudExplorer);
if (failed(clusterNode)) {
vscode.window.showErrorMessage(clusterNode.error);
return;
}

// Once Periscope will support usgov endpoints all we need is to remove this check.
// I have done background plumbing for vscode to fetch downlodable link from correct endpoint.
const cloudName = getEnvironment().name;
if (cloudName !== "AzureCloud") {
vscode.window.showInformationMessage(`Periscope is not supported in ${cloudName} cloud.`);
return;
}

const properties = await longRunning(`Getting properties for cluster ${clusterNode.result.name}.`, () =>
getManagedCluster(
sessionProvider.result,
clusterNode.result.subscriptionId,
clusterNode.result.resourceGroupName,
clusterNode.result.name,
),
);
if (failed(properties)) {
vscode.window.showErrorMessage(properties.error);
return undefined;
}

const kubeconfig = await longRunning(`Retrieving kubeconfig for cluster ${clusterNode.result.name}.`, () =>
getKubeconfigYaml(
sessionProvider.result,
clusterNode.result.subscriptionId,
clusterNode.result.resourceGroupName,
properties.result,
),
);

if (failed(kubeconfig)) {
vscode.window.showErrorMessage(kubeconfig.error);
return undefined;
}

const kubeConfigFile = await tmpfile.createTempFile(kubeconfig.result, "yaml");

const panel = new HeadlampPanel(extension.result.extensionUri);
const dataProvider = new HeadlampPanelDataProvider(clusterNode.result.name, kubectl, kubeConfigFile.filePath);
panel.show(dataProvider);

// await longRunning(`Retrieving kubeconfig for cluster ${clusterNode.result.name}.`, () =>
// deployHeadlampInCluster(kubectl, clusterNode.result, kubeconfig.result)
// );
}

// below function isn't being used anymore - can maybe be removed unless refactor
void deployHeadlampInCluster;
async function deployHeadlampInCluster(
kubectl: k8s.APIAvailable<k8s.KubectlV1>,
clusterNode: AksClusterTreeNode,
clusterKubeConfig: string,
): Promise<void> {
const clusterName = clusterNode.name;
console.log(`Deploying Headlamp in cluster ${clusterName}.`);

const sessionProvider = await getReadySessionProvider();
if (failed(sessionProvider)) {
vscode.window.showErrorMessage(sessionProvider.error);
return;
}

const extension = getExtension();
if (failed(extension)) {
vscode.window.showErrorMessage(extension.error);
return;
}

await tmpfile.withOptionalTempFile<Errorable<k8s.KubectlV1.ShellResult>>(
clusterKubeConfig,
"YAML",
async (kubeConfigFile) => {
// Clean up running instance (without an error if it doesn't yet exist).
// const deleteResult = await invokeKubectlCommand(
// kubectl,
// kubeConfigFile,
// "delete ns aks-periscope --ignore-not-found=true",
// );
// if (failed(deleteResult)) return deleteResult;

// Deploy headlamp.
const applyResult = await invokeKubectlCommand(
kubectl,
kubeConfigFile,
`apply -k https://raw.githubusercontent.com/kinvolk/headlamp/main/kubernetes-headlamp-ingress-sample.yaml`,
);
if (failed(applyResult)) return applyResult;

// kubectl port-forward -n kube-system service/headlamp 8080:80
return invokeKubectlCommand(
kubectl,
kubeConfigFile,
" port-forward -n kube-system service/headlamp 8080:80",
);
},
);
}

Comment on lines +86 to +137
Copy link

Copilot AI May 26, 2025

Choose a reason for hiding this comment

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

The unused reference to 'deployHeadlampInCluster' can be removed to clean up the code. Consider deleting it and its associated function if it is no longer required.

Suggested change
// below function isn't being used anymore - can maybe be removed unless refactor
void deployHeadlampInCluster;
async function deployHeadlampInCluster(
kubectl: k8s.APIAvailable<k8s.KubectlV1>,
clusterNode: AksClusterTreeNode,
clusterKubeConfig: string,
): Promise<void> {
const clusterName = clusterNode.name;
console.log(`Deploying Headlamp in cluster ${clusterName}.`);
const sessionProvider = await getReadySessionProvider();
if (failed(sessionProvider)) {
vscode.window.showErrorMessage(sessionProvider.error);
return;
}
const extension = getExtension();
if (failed(extension)) {
vscode.window.showErrorMessage(extension.error);
return;
}
await tmpfile.withOptionalTempFile<Errorable<k8s.KubectlV1.ShellResult>>(
clusterKubeConfig,
"YAML",
async (kubeConfigFile) => {
// Clean up running instance (without an error if it doesn't yet exist).
// const deleteResult = await invokeKubectlCommand(
// kubectl,
// kubeConfigFile,
// "delete ns aks-periscope --ignore-not-found=true",
// );
// if (failed(deleteResult)) return deleteResult;
// Deploy headlamp.
const applyResult = await invokeKubectlCommand(
kubectl,
kubeConfigFile,
`apply -k https://raw.githubusercontent.com/kinvolk/headlamp/main/kubernetes-headlamp-ingress-sample.yaml`,
);
if (failed(applyResult)) return applyResult;
// kubectl port-forward -n kube-system service/headlamp 8080:80
return invokeKubectlCommand(
kubectl,
kubeConfigFile,
" port-forward -n kube-system service/headlamp 8080:80",
);
},
);
}
// (Removed unused deployHeadlampInCluster declaration and function)

Copilot uses AI. Check for mistakes.
// async function deployKustomizeOverlay(
// kubectl: k8s.APIAvailable<k8s.KubectlV1>,
// overlayDir: string,
// clusterKubeConfig: string,
// ): Promise<Errorable<k8s.KubectlV1.ShellResult>> {
// return await tmpfile.withOptionalTempFile<Errorable<k8s.KubectlV1.ShellResult>>(
// clusterKubeConfig,
// "YAML",
// async (kubeConfigFile) => {
// // Clean up running instance (without an error if it doesn't yet exist).
// const deleteResult = await invokeKubectlCommand(
// kubectl,
// kubeConfigFile,
// "delete ns aks-periscope --ignore-not-found=true",
// );
// if (failed(deleteResult)) return deleteResult;

// // Deploy aks-periscope.
// const applyResult = await invokeKubectlCommand(kubectl, kubeConfigFile, `apply -k ${overlayDir}`);
// if (failed(applyResult)) return applyResult;

// return invokeKubectlCommand(kubectl, kubeConfigFile, "cluster-info");
// },
// );
// }
2 changes: 2 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import aksClusterFilter from "./commands/utils/clusterfilter";
//import aksAutomatedDeployments from "./commands/devhub/aksAutomatedDeployments";
import aksCreateFleet from "./commands/aksFleet/aksFleetManager";
import aksFleetProperties from "./commands/aksFleetProperties/askFleetProperties";
import deployHeadlamp from "./commands/oservabilitytools/deployHeadlamp/deployHeadlamp";

export async function activate(context: vscode.ExtensionContext) {
const cloudExplorer = await k8s.extension.cloudExplorer.v1;
Expand Down Expand Up @@ -131,6 +132,7 @@ export async function activate(context: vscode.ExtensionContext) {
//registerCommandWithTelemetry("aks.aksAutomatedDeployments", aksAutomatedDeployments);
registerCommandWithTelemetry("aks.aksCreateFleet", aksCreateFleet);
registerCommandWithTelemetry("aks.aksFleetProperties", aksFleetProperties);
registerCommandWithTelemetry("aks.deployHeadlamp", deployHeadlamp);

await registerAzureServiceNodes(context);

Expand Down
179 changes: 179 additions & 0 deletions src/panels/HeadlampPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import * as vscode from "vscode";
import * as k8s from "vscode-kubernetes-tools-api";
import { MessageHandler, MessageSink } from "../webview-contract/messaging";
import { InitialState, ToVsCodeMsgDef, ToWebViewMsgDef } from "../webview-contract/webviewDefinitions/headlamp";
import { TelemetryDefinition } from "../webview-contract/webviewTypes";
import { BasePanel, PanelDataProvider } from "./BasePanel";
import { invokeKubectlCommand } from "../commands/utils/kubectl";
import { failed } from "../commands/utils/errorable";
import { spawn, ChildProcess } from "child_process";

export class HeadlampPanel extends BasePanel<"headlamp"> {
constructor(extensionUri: vscode.Uri) {
super(extensionUri, "headlamp", {
headlampUpdate: null,
});
}
}

export class HeadlampPanelDataProvider implements PanelDataProvider<"headlamp"> {
public constructor(
readonly clusterName: string,
readonly kubectl: k8s.APIAvailable<k8s.KubectlV1>,
readonly kubeConfigFilePath: string,
) {
this.clusterName = clusterName;
this.kubectl = kubectl;
this.kubeConfigFilePath = kubeConfigFilePath;
}
getTitle(): string {
return `Deploy Headlamp`;
}

getInitialState(): InitialState {
return {
deploymentStatus: "undeployed",
token: "",
};
}
getTelemetryDefinition(): TelemetryDefinition<"headlamp"> {
return {
deployHeadlampRequest: true,
generateTokenRequest: true,
startPortForwardingRequest: true,
stopPortForwardingRequest: true,
};
}
getMessageHandler(webview: MessageSink<ToWebViewMsgDef>): MessageHandler<ToVsCodeMsgDef> {
return {
deployHeadlampRequest: () => {
this.handleDeployHeadlampRequest(webview);
},
generateTokenRequest: () => {
this.handleGenerateTokenRequest(webview);
},
startPortForwardingRequest: () => {
this.startPortForwarding();
},
stopPortForwardingRequest: () => {
this.stopPortForwarding();
},
};
}

private portForwardProcess?: ChildProcess;

private async handleDeployHeadlampRequest(webview: MessageSink<ToWebViewMsgDef>) {
// deploy headlamp
const deploy = await invokeKubectlCommand(
this.kubectl,
this.kubeConfigFilePath,
"apply -f https://raw.githubusercontent.com/kinvolk/headlamp/main/kubernetes-headlamp.yaml",
);
if (failed(deploy)) {
vscode.window.showErrorMessage(deploy.error);
console.log(deploy.error);
return;
}

// wait for headlamp pod to be ready
webview.postHeadlampUpdate({ deploymentStatus: "deploying", token: "" });
const wait = await invokeKubectlCommand(
this.kubectl,
this.kubeConfigFilePath,
"wait --namespace kube-system --for=condition=Ready pod -l k8s-app=headlamp --timeout=90s",
);
if (failed(wait)) {
vscode.window.showErrorMessage(wait.error);
return;
}

vscode.window.showInformationMessage("Headlamp deployed successfully!");
webview.postHeadlampUpdate({ deploymentStatus: "deployed", token: "" });
}

private async handleGenerateTokenRequest(webview: MessageSink<ToWebViewMsgDef>) {
// create service account
const createServiceAccount = await invokeKubectlCommand(
this.kubectl,
this.kubeConfigFilePath,
"-n kube-system create serviceaccount headlamp-admin",
);
if (failed(createServiceAccount)) {
if (!createServiceAccount.error.includes("already exists")) {
vscode.window.showErrorMessage(createServiceAccount.error);
return;
}
}

// create cluster role binding
const createClusterRoleBinding = await invokeKubectlCommand(
this.kubectl,
this.kubeConfigFilePath,
"create clusterrolebinding headlamp-admin --serviceaccount=kube-system:headlamp-admin --clusterrole=cluster-admin",
);
if (failed(createClusterRoleBinding)) {
if (!createClusterRoleBinding.error.includes("already exists")) {
vscode.window.showErrorMessage(createClusterRoleBinding.error);
return;
}
}

// create token
const createToken = await invokeKubectlCommand(
this.kubectl,
this.kubeConfigFilePath,
"create token headlamp-admin -n kube-system --duration 30m",
);
if (failed(createToken)) {
vscode.window.showErrorMessage(createToken.error);
return;
} else {
const token = createToken.result.stdout.trim();
console.log(token);
webview.postHeadlampUpdate({ deploymentStatus: "deployed", token: token });
}
}

private startPortForwarding() {
if (this.portForwardProcess) {
vscode.window.showWarningMessage("Port forwarding already running.");
return;
}

this.portForwardProcess = spawn("kubectl", [
"--kubeconfig",
this.kubeConfigFilePath,
"port-forward",
"-n",
"kube-system",
"service/headlamp",
"8080:80",
]);

this.portForwardProcess.stdout?.on("data", (data) => {
console.log(`[port-forward] ${data}`);
});

this.portForwardProcess.stderr?.on("data", (data) => {
console.error(`[port-forward error] ${data}`);
});

this.portForwardProcess.on("exit", (code) => {
console.log(`Port forward exited with code ${code}`);
this.portForwardProcess = undefined;
});
vscode.window.showInformationMessage("Port forwarding started.");
vscode.env.openExternal(vscode.Uri.parse("http://localhost:8080"));
}

private stopPortForwarding() {
if (this.portForwardProcess) {
this.portForwardProcess.kill();
this.portForwardProcess = undefined;
vscode.window.showInformationMessage("Port forwarding stopped.");
} else {
vscode.window.showWarningMessage("No port forwarding process running.");
}
}
}
Loading