kubepile 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,6 +36,15 @@ Kubepile will never set a `current-context`, out of the design belief that
36
36
  npm install -g kubepile
37
37
  ```
38
38
 
39
+ Kubepile also includes a small shell helper that you need to install once:
40
+
41
+ ```sh
42
+ kubepile install
43
+ ```
44
+
45
+ Once you've installed the shell helper, either start a new shell or re-source
46
+ your `.zshrc`/`.bashrc`/`.profile`/etc. Kubepile supports Zsh, Bash, and Fish.
47
+
39
48
  ## Compile
40
49
 
41
50
  ```sh
@@ -63,6 +72,38 @@ kubepile compile --no-backup
63
72
 
64
73
  Running `kubepile` with no command prints help.
65
74
 
75
+ ## Source
76
+
77
+ `kubepile source <context>` switches your current shell to use a specific
78
+ Kubernetes context by default by creating a temporary kubeconfig with that
79
+ context set as the current-context, and exporting `KUBECONFIG` to point at it.
80
+ It also prefixes your shell prompt with the context name.
81
+
82
+ ```sh
83
+ kubepile source prod
84
+ # Your shell prompt is now:
85
+ # (prod) WHATEVER_YOUR_OLD_PROMPT_WAS
86
+ # All kubectl commands will use the prod context
87
+ ```
88
+
89
+ To switch to a different context, just run the `source` command with a new
90
+ context:
91
+
92
+ ```sh
93
+ kubepile source dev
94
+ ```
95
+
96
+ Note that this requires installing the shell helpers listed in the
97
+ installation instructions. If you haven't installed them yet, install them
98
+ with:
99
+
100
+ ```sh
101
+ kubepile install
102
+ ```
103
+
104
+ And re-source your main shell config (such as e.g. a `.zshrc`) or start a new
105
+ shell.
106
+
66
107
  ## Split
67
108
 
68
109
  Do you already have a giant unmaintainable mess of a kubeconfig? No worries!
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kubepile",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Compile and split kubeconfig files from ~/.config/kubepile.",
5
5
  "type": "module",
6
6
  "main": "./dist/src/kubepile.js",
package/dist/src/cli.js CHANGED
@@ -4,6 +4,7 @@ import { createInterface } from "node:readline/promises";
4
4
  import { Command } from "@commander-js/extra-typings";
5
5
  import packageJson from "../package.json" with { type: "json" };
6
6
  import { compileToKubeConfig, defaultKubeConfigPath, defaultKubepileDir, splitKubeConfigFile, } from "./kubepile.js";
7
+ import { generateShellCommand, installShellIntegration, } from "./shell.js";
7
8
  const program = createProgram();
8
9
  if (process.argv.slice(2).length === 0) {
9
10
  program.outputHelp();
@@ -73,5 +74,35 @@ Defaults:
73
74
  });
74
75
  process.stdout.write(`Wrote ${result.writtenFiles.length} kubeconfig file(s) into ${result.outputDir}\n`);
75
76
  });
77
+ program
78
+ .command("install")
79
+ .description("Install the kubepile shell function for the current shell.")
80
+ .action(async () => {
81
+ const result = await installShellIntegration();
82
+ const action = result.updated ? "Installed" : "Updated";
83
+ process.stdout.write(`${action} kubepile shell integration in ${result.rcFile}\n`);
84
+ process.stdout.write("Start a new shell, then run: kubepile source <context>\n");
85
+ });
86
+ program
87
+ .command("generate-shell-command")
88
+ .description("Generate shell code for kubepile source. Usually called by the installed shell function.")
89
+ .argument("<context>", "context name to source")
90
+ .option("--source <file>", "source kubeconfig path")
91
+ .option("--shell <shell>", "shell command format: bash, zsh, or fish", "bash")
92
+ .action(async (context, options) => {
93
+ const shell = options.shell === "fish" ? "fish" : "posix";
94
+ const result = await generateShellCommand(context, {
95
+ sourcePath: options.source,
96
+ shell,
97
+ });
98
+ process.stdout.write(`${result.shellCommand}\n`);
99
+ });
100
+ program
101
+ .command("source")
102
+ .description("Switch to one kube context in the current shell.")
103
+ .argument("<context>", "context name to source")
104
+ .action(() => {
105
+ throw new Error("kubepile source requires shell integration. Run `kubepile install`, start a new shell, then run `kubepile source <context>`.");
106
+ });
76
107
  return program;
77
108
  }
@@ -61,6 +61,7 @@ export declare function buildMergedConfig(options?: CompileOptions): Promise<{
61
61
  export declare function compileToKubeConfig(options?: CompileToFileOptions): Promise<CompileResult>;
62
62
  export declare function splitKubeConfigFile(options?: SplitOptions): Promise<SplitResult>;
63
63
  export declare function splitKubeConfig(config: KubeConfig, sourceLabel?: string): SplitConfig[];
64
+ export declare function extractContextConfig(config: KubeConfig, contextName: string, sourceLabel?: string): KubeConfig;
64
65
  export declare function readKubeConfigFile(filePath: string): Promise<KubeConfig>;
65
66
  export declare function parseKubeConfig(source: string, sourceLabel?: string): KubeConfig;
66
67
  export declare function serializeKubeConfig(config: KubeConfig): string;
@@ -163,6 +163,16 @@ export function splitKubeConfig(config, sourceLabel = "kubeconfig") {
163
163
  };
164
164
  });
165
165
  }
166
+ export function extractContextConfig(config, contextName, sourceLabel = "kubeconfig") {
167
+ const splitConfig = splitKubeConfig(config, sourceLabel).find((candidate) => candidate.contextName === contextName);
168
+ if (!splitConfig) {
169
+ throw new Error(`${sourceLabel} does not contain context "${contextName}"`);
170
+ }
171
+ return {
172
+ ...splitConfig.config,
173
+ "current-context": contextName,
174
+ };
175
+ }
166
176
  export async function readKubeConfigFile(filePath) {
167
177
  const source = await readFile(filePath, "utf8");
168
178
  return parseKubeConfig(source, filePath);
@@ -0,0 +1,27 @@
1
+ export type ShellKind = "bash" | "zsh" | "fish";
2
+ export type ShellCommandKind = "posix" | "fish";
3
+ export interface GenerateShellCommandOptions {
4
+ sourcePath?: string;
5
+ shell?: ShellCommandKind;
6
+ tempDir?: string;
7
+ }
8
+ export interface GenerateShellCommandResult {
9
+ kubeConfigPath: string;
10
+ shellCommand: string;
11
+ }
12
+ export interface InstallShellIntegrationOptions {
13
+ shell?: ShellKind;
14
+ rcFile?: string;
15
+ homeDir?: string;
16
+ }
17
+ export interface InstallShellIntegrationResult {
18
+ shell: ShellKind;
19
+ rcFile: string;
20
+ updated: boolean;
21
+ }
22
+ export declare function generateShellCommand(contextName: string, options?: GenerateShellCommandOptions): Promise<GenerateShellCommandResult>;
23
+ export declare function installShellIntegration(options?: InstallShellIntegrationOptions): Promise<InstallShellIntegrationResult>;
24
+ export declare function detectCurrentShell(shellPath?: string | undefined): ShellKind;
25
+ export declare function shellRcFile(shell: ShellKind, homeDir?: string): Promise<string>;
26
+ export declare function shellIntegrationBlock(shell: ShellKind): string;
27
+ export declare function upsertShellIntegrationBlock(existing: string, block: string): string;
@@ -0,0 +1,161 @@
1
+ import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { defaultKubeConfigPath, readKubeConfigFile, serializeKubeConfig, } from "./kubepile.js";
5
+ const SHELL_BLOCK_START = "# >>> kubepile shell integration >>>";
6
+ const SHELL_BLOCK_END = "# <<< kubepile shell integration <<<";
7
+ export async function generateShellCommand(contextName, options = {}) {
8
+ const sourcePath = options.sourcePath ?? defaultKubeConfigPath();
9
+ const shell = options.shell ?? "posix";
10
+ const sourceConfig = await readKubeConfigFile(sourcePath);
11
+ const contextConfig = {
12
+ ...sourceConfig,
13
+ "current-context": contextName,
14
+ };
15
+ const tempRoot = await mkdtemp(path.join(options.tempDir ?? os.tmpdir(), "kubepile-source-"));
16
+ const kubeConfigPath = path.join(tempRoot, "config");
17
+ if (!sourceConfig.contexts?.some((context) => context.name === contextName)) {
18
+ throw new Error(`${sourcePath} does not contain context "${contextName}"`);
19
+ }
20
+ await writeFile(kubeConfigPath, serializeKubeConfig(contextConfig), {
21
+ encoding: "utf8",
22
+ mode: 0o600,
23
+ });
24
+ return {
25
+ kubeConfigPath,
26
+ shellCommand: shell === "fish"
27
+ ? fishSourceCommand(contextName, kubeConfigPath)
28
+ : posixSourceCommand(contextName, kubeConfigPath),
29
+ };
30
+ }
31
+ export async function installShellIntegration(options = {}) {
32
+ const shell = options.shell ?? detectCurrentShell();
33
+ const rcFile = options.rcFile ?? await shellRcFile(shell, options.homeDir ?? os.homedir());
34
+ const block = shellIntegrationBlock(shell);
35
+ const existing = await readTextIfExists(rcFile);
36
+ const next = upsertShellIntegrationBlock(existing, block);
37
+ await mkdir(path.dirname(rcFile), { recursive: true });
38
+ await writeFile(rcFile, next, "utf8");
39
+ return {
40
+ shell,
41
+ rcFile,
42
+ updated: next !== existing,
43
+ };
44
+ }
45
+ export function detectCurrentShell(shellPath = process.env.SHELL) {
46
+ const shellName = shellPath ? path.basename(shellPath) : "";
47
+ if (shellName === "bash" || shellName === "zsh" || shellName === "fish") {
48
+ return shellName;
49
+ }
50
+ throw new Error(`Unsupported shell "${shellPath ?? ""}". Supported shells: bash, zsh, fish.`);
51
+ }
52
+ export async function shellRcFile(shell, homeDir = os.homedir()) {
53
+ if (shell === "zsh") {
54
+ return path.join(homeDir, ".zshrc");
55
+ }
56
+ if (shell === "fish") {
57
+ return path.join(homeDir, ".config", "fish", "config.fish");
58
+ }
59
+ const bashCandidates = [
60
+ path.join(homeDir, ".bashrc"),
61
+ path.join(homeDir, ".bash_profile"),
62
+ path.join(homeDir, ".bash_login"),
63
+ path.join(homeDir, ".profile"),
64
+ ];
65
+ return await firstExistingPath(bashCandidates) ?? bashCandidates[0];
66
+ }
67
+ export function shellIntegrationBlock(shell) {
68
+ return [
69
+ SHELL_BLOCK_START,
70
+ shell === "fish" ? fishIntegrationFunction() : posixIntegrationFunction(shell),
71
+ SHELL_BLOCK_END,
72
+ "",
73
+ ].join("\n");
74
+ }
75
+ export function upsertShellIntegrationBlock(existing, block) {
76
+ const blockPattern = new RegExp(`${escapeRegExp(SHELL_BLOCK_START)}\\n[\\s\\S]*?\\n${escapeRegExp(SHELL_BLOCK_END)}\\n?`);
77
+ const normalizedBlock = block.endsWith("\n") ? block : `${block}\n`;
78
+ if (blockPattern.test(existing)) {
79
+ return existing.replace(blockPattern, normalizedBlock);
80
+ }
81
+ const separator = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
82
+ return `${existing}${separator}${normalizedBlock}`;
83
+ }
84
+ function posixIntegrationFunction(shell) {
85
+ return `kubepile() {
86
+ if [ "$1" = "source" ]; then
87
+ shift
88
+ eval "$(command \\kubepile generate-shell-command --shell ${shell} "$@")"
89
+ else
90
+ command \\kubepile "$@"
91
+ fi
92
+ }`;
93
+ }
94
+ function fishIntegrationFunction() {
95
+ return `function kubepile
96
+ if test (count $argv) -gt 0; and test "$argv[1]" = "source"
97
+ set -e argv[1]
98
+ command kubepile generate-shell-command --shell fish $argv | source
99
+ else
100
+ command kubepile $argv
101
+ end
102
+ end`;
103
+ }
104
+ function posixSourceCommand(contextName, kubeConfigPath) {
105
+ return [
106
+ `export KUBECONFIG=${shellQuote(kubeConfigPath)}`,
107
+ "if [ -z \"${KUBEPILE_OLD_PS1+x}\" ]; then",
108
+ " export KUBEPILE_OLD_PS1=${PS1-}",
109
+ "fi",
110
+ `export PS1=${shellQuote(`(${contextName}) `)}"$KUBEPILE_OLD_PS1"`,
111
+ ].join("\n");
112
+ }
113
+ function fishSourceCommand(contextName, kubeConfigPath) {
114
+ return [
115
+ `set -gx KUBECONFIG ${fishQuote(kubeConfigPath)}`,
116
+ "if not set -q KUBEPILE_OLD_PROMPT",
117
+ " functions -c fish_prompt KUBEPILE_OLD_PROMPT",
118
+ "end",
119
+ "function fish_prompt",
120
+ ` printf ${fishQuote(`(${contextName}) `)}`,
121
+ " KUBEPILE_OLD_PROMPT",
122
+ "end",
123
+ ].join("\n");
124
+ }
125
+ function shellQuote(value) {
126
+ if (/^[A-Za-z0-9_./:=@%+-]+$/.test(value)) {
127
+ return value;
128
+ }
129
+ return `'${value.replaceAll("'", "'\\''")}'`;
130
+ }
131
+ function fishQuote(value) {
132
+ return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
133
+ }
134
+ async function readTextIfExists(filePath) {
135
+ try {
136
+ return await readFile(filePath, "utf8");
137
+ }
138
+ catch (error) {
139
+ if (error.code === "ENOENT") {
140
+ return "";
141
+ }
142
+ throw error;
143
+ }
144
+ }
145
+ async function firstExistingPath(filePaths) {
146
+ for (const filePath of filePaths) {
147
+ try {
148
+ await readFile(filePath, "utf8");
149
+ return filePath;
150
+ }
151
+ catch (error) {
152
+ if (error.code !== "ENOENT") {
153
+ throw error;
154
+ }
155
+ }
156
+ }
157
+ return undefined;
158
+ }
159
+ function escapeRegExp(value) {
160
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
161
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kubepile",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Compile and split kubeconfig files from ~/.config/kubepile.",
5
5
  "type": "module",
6
6
  "main": "./dist/src/kubepile.js",