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

Namespace selection #164

Merged
Merged
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
1 change: 1 addition & 0 deletions changelog.d/+namespace-selection.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mirrord target quick pick now allows for switching between Kubernetes namespaces.
180 changes: 80 additions & 100 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { NotificationBuilder } from './notification';
import { MirrordStatus } from './status';
import { EnvVars, VerifiedConfig } from './config';
import { PathLike } from 'fs';
import { UserSelection } from './targetQuickPick';

/**
* Key to access the feedback counter (see `tickFeedbackCounter`) from the global user config.
Expand All @@ -27,18 +28,6 @@ const DISCORD_COUNTER = 'mirrord-discord-counter';
*/
const DISCORD_COUNTER_PROMPT_AFTER = 10;

const TARGET_TYPE_DISPLAY: Record<string, string> = {
pod: 'Pod',
deployment: 'Deployment',
rollout: 'Rollout',
};

// Option in the target selector that represents no target.
const TARGETLESS_TARGET: TargetQuickPick = {
label: "No Target (\"targetless\")",
type: 'targetless'
};

/**
* Level of the notification, different levels map to different notification boxes.
*/
Expand Down Expand Up @@ -122,77 +111,52 @@ function handleIdeMessage(message: IdeMessage) {
}
}

type TargetQuickPick = vscode.QuickPickItem & (
{ type: 'targetless' } |
{ type: 'target' | 'page', value: string }
);

export class Targets {
private activePage: string;

private readonly inner: Record<string, TargetQuickPick[] | undefined>;
readonly length: number;

constructor(targets: string[], lastTarget?: string) {
this.length = targets.length;

this.inner = targets.reduce((acc, value) => {
const targetType = value.split('/')[0];
const target: TargetQuickPick = {
label: value,
type: 'target',
value
};

if (Array.isArray(acc[targetType])) {
acc[targetType]!.push(target);
} else {
acc[targetType] = [target];
}

return acc;
}, {} as Targets['inner']);


const types = Object.keys(this.inner);
const lastPage = lastTarget?.split("/")?.[0] ?? '';

if (types.includes(lastPage)) {
this.activePage = lastPage;
} else {
this.activePage = types[0] ?? '';
}
}

private quickPickSelects(): TargetQuickPick[] {
return Object.keys(this.inner)
.filter((value) => value !== this.activePage)
.map((value) => ({
label: `Show ${TARGET_TYPE_DISPLAY[value] ?? value}s`,
type: 'page',
value
}));
}

/**
* A mirrord target found in the cluster.
*/
export type FoundTarget = {
/**
* The path of this target, as in the mirrord config.
*/
path: string;
/**
* Whether this target is available.
*/
available: boolean;
};

quickPickItems(): TargetQuickPick[] {
return [
...(this.inner[this.activePage] ?? []),
TARGETLESS_TARGET,
...this.quickPickSelects()
];
}
/**
* The new format of `mirrord ls`, including target availability and namespaces info.
*/
export type MirrordLsOutput = {
/**
* The targets found in the current namespace.
*/
targets: FoundTarget[];
/**
* The namespace where the lookup was done.
*
* If the CLI does not support listing namespaces, this is undefined.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
current_namespace?: string;
/**
* All namespaces visible to the user.
*
* If the CLI does not support listing namespaces, this is undefined.
*/
namespaces?: string[];
};

switchPage(nextPage: TargetQuickPick) {
if (nextPage.type === 'page') {
this.activePage = nextPage.value;
}
}
/**
* Checks whether the JSON value is in the @see MirrordLsOutput format.
*
* @param output JSON parsed from `mirrord ls` stdout
*/
function isRichMirrordLsOutput(output: any): output is MirrordLsOutput {
return "targets" in output && "current_namespace" in output && "namespaces" in output;
}

/// Key used to store the last selected target in the persistent state.
export const LAST_TARGET_KEY = "mirrord-last-target";

// Display error message with help
export function mirrordFailure(error: string) {
new NotificationBuilder()
Expand Down Expand Up @@ -239,7 +203,7 @@ export class MirrordExecution {
/**
* Sets up the args that are going to be passed to the mirrord cli.
*/
const makeMirrordArgs = (target: string | null, configFilePath: PathLike | null, userExecutable: PathLike | null): readonly string[] => {
const makeMirrordArgs = (target: string | undefined, configFilePath: PathLike | null, userExecutable: PathLike | null): readonly string[] => {
let args = ["ext"];

if (target) {
Expand Down Expand Up @@ -280,7 +244,10 @@ export class MirrordAPI {
"MIRRORD_PROGRESS_MODE": "json",
// to have "advanced" progress in IDE
// eslint-disable-next-line @typescript-eslint/naming-convention
"MIRRORD_PROGRESS_SUPPORT_IDE": "true"
"MIRRORD_PROGRESS_SUPPORT_IDE": "true",
// to have namespaces in the `mirrord ls` output
// eslint-disable-next-line @typescript-eslint/naming-convention
"MIRRORD_LS_RICH_OUTPUT": "true"
};
}

Expand Down Expand Up @@ -345,35 +312,41 @@ export class MirrordAPI {
async getBinaryVersion(): Promise<string | undefined> {
const stdout = await this.exec(["--version"], {});
// parse mirrord x.y.z
return stdout.split(" ")[1].trim();
return stdout.split(" ")[1]?.trim();
}

/**
* Uses `mirrord ls` to get a list of all targets.
* Targets come sorted, with an exception of the last used target being the first on the list.
* Uses `mirrord ls` to get lists of targets and namespaces.
*
* Note that old CLI versions return only targets.
*
* @see MirrordLsOutput
*/
async listTargets(configPath: string | null | undefined): Promise<Targets> {
async listTargets(configPath: string | null | undefined, configEnv: EnvVars, namespace?: string): Promise<MirrordLsOutput> {
const args = ['ls'];
if (configPath) {
args.push('-f', configPath);
}

const stdout = await this.exec(args, {});

const targets: string[] = JSON.parse(stdout);
if (namespace !== undefined) {
args.push('-n', namespace);
}

let lastTarget: string | undefined = globalContext.workspaceState.get(LAST_TARGET_KEY)
|| globalContext.globalState.get(LAST_TARGET_KEY);
const stdout = await this.exec(args, configEnv);

if (lastTarget !== undefined) {
const idx = targets.indexOf(lastTarget);
if (idx !== -1) {
targets.splice(idx, 1);
targets.unshift(lastTarget);
}
const targets = JSON.parse(stdout);
let mirrordLsOutput: MirrordLsOutput;
if (isRichMirrordLsOutput(targets)) {
mirrordLsOutput = targets;
} else {
mirrordLsOutput = {
targets: (targets as string[]).map(path => {
return {path, available: true };
}),
};
}

return new Targets(targets, lastTarget);
return mirrordLsOutput;
}

/**
Expand All @@ -398,7 +371,7 @@ export class MirrordAPI {
*
* Has 60 seconds timeout
*/
async binaryExecute(target: string | null, configFile: string | null, executable: string | null, configEnv: EnvVars): Promise<MirrordExecution> {
async binaryExecute(target: UserSelection, configFile: string | null, executable: string | null, configEnv: EnvVars): Promise<MirrordExecution> {
tickMirrordForTeamsCounter();
tickFeedbackCounter();
tickDiscordCounter();
Expand All @@ -414,9 +387,16 @@ export class MirrordAPI {
reject("timeout");
}, 120 * 1000);

const args = makeMirrordArgs(target, configFile, executable);
const args = makeMirrordArgs(target.path ?? "targetless", configFile, executable);
let env: EnvVars;
if (target.namespace) {
// eslint-disable-next-line @typescript-eslint/naming-convention
env = { MIRRORD_TARGET_NAMESPACE: target.namespace, ...configEnv };
} else {
env = configEnv;
}

const child = this.spawnCliWithArgsAndEnv(args, configEnv);
const child = this.spawnCliWithArgsAndEnv(args, env);

let stderrData = "";
child.stderr.on("data", (data) => stderrData += data.toString());
Expand Down
53 changes: 9 additions & 44 deletions src/debugger.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import * as vscode from 'vscode';
import { globalContext } from './extension';
import { isTargetSet, MirrordConfigManager } from './config';
import { LAST_TARGET_KEY, MirrordAPI, mirrordFailure, MirrordExecution } from './api';
import { MirrordAPI, mirrordFailure, MirrordExecution } from './api';
import { updateTelemetries } from './versionCheck';
import { getMirrordBinary } from './binaryManager';
import { platform } from 'node:os';
import { NotificationBuilder } from './notification';
import { setOperatorUsed } from './mirrordForTeams';
import fs from 'fs';
import { TargetQuickPick, UserSelection } from './targetQuickPick';

const DYLD_ENV_VAR_NAME = "DYLD_INSERT_LIBRARIES";

Expand Down Expand Up @@ -109,60 +110,24 @@ async function main(
let mirrordApi = new MirrordAPI(cliPath);

config.env ||= {};
let target = null;
let target: UserSelection = {};

let configPath = await MirrordConfigManager.getInstance().resolveMirrordConfig(folder, config);
const verifiedConfig = await mirrordApi.verifyConfig(configPath, config.env);

// If target wasn't specified in the config file (or there's no config file), let user choose pod from dropdown
if (!configPath || (verifiedConfig && !isTargetSet(verifiedConfig))) {
let targets;
const getTargets = async (namespace?: string) => {
return mirrordApi.listTargets(configPath?.path, config.env, namespace);
};

try {
targets = await mirrordApi.listTargets(configPath?.path);
const quickPick = await TargetQuickPick.new(getTargets);
target = await quickPick.showAndGet();
} catch (err) {
mirrordFailure(`mirrord failed to list targets: ${err}`);
return null;
}
if (targets.length === 0) {
new NotificationBuilder()
.withMessage(
"No mirrord target available in the configured namespace. " +
"You can run targetless, or set a different target namespace or kubeconfig in the mirrord configuration file.",
)
.info();
}

let selected = false;

while (!selected) {
let targetPick = await vscode.window.showQuickPick(targets.quickPickItems(), {
placeHolder: 'Select a target path to mirror'
});

if (targetPick) {
if (targetPick.type === 'page') {
targets.switchPage(targetPick);

continue;
}

if (targetPick.type !== 'targetless') {
target = targetPick.value;
}

globalContext.globalState.update(LAST_TARGET_KEY, target);
globalContext.workspaceState.update(LAST_TARGET_KEY, target);
}

selected = true;
}

if (!target) {
new NotificationBuilder()
.withMessage("mirrord running targetless")
.withDisableAction("promptTargetless")
.info();
}
}

if (config.type === "go") {
Expand Down
Loading