@enspirit/emb 0.23.0 → 0.24.0

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.
Files changed (28) hide show
  1. package/README.md +42 -42
  2. package/dist/src/cli/abstract/KubernetesCommand.d.ts +5 -1
  3. package/dist/src/cli/abstract/KubernetesCommand.js +11 -2
  4. package/dist/src/cli/commands/kubernetes/logs.js +7 -12
  5. package/dist/src/cli/commands/kubernetes/ps.js +2 -1
  6. package/dist/src/cli/commands/kubernetes/restart.js +2 -1
  7. package/dist/src/cli/commands/kubernetes/shell.js +7 -10
  8. package/dist/src/config/schema.d.ts +33 -1
  9. package/dist/src/config/schema.json +42 -1
  10. package/dist/src/docker/compose/client.js +2 -2
  11. package/dist/src/docker/compose/operations/ComposeLogsArchiveOperation.d.ts +1 -1
  12. package/dist/src/kubernetes/index.d.ts +1 -0
  13. package/dist/src/kubernetes/index.js +1 -0
  14. package/dist/src/kubernetes/operations/GetComponentPodOperation.d.ts +17 -0
  15. package/dist/src/kubernetes/operations/GetComponentPodOperation.js +77 -0
  16. package/dist/src/kubernetes/operations/GetDeploymentPodsOperation.js +3 -2
  17. package/dist/src/kubernetes/operations/PodExecOperation.d.ts +20 -0
  18. package/dist/src/kubernetes/operations/PodExecOperation.js +158 -0
  19. package/dist/src/kubernetes/operations/index.d.ts +3 -0
  20. package/dist/src/kubernetes/operations/index.js +3 -0
  21. package/dist/src/kubernetes/utils/index.d.ts +1 -0
  22. package/dist/src/kubernetes/utils/index.js +1 -0
  23. package/dist/src/kubernetes/utils/resolveNamespace.d.ts +13 -0
  24. package/dist/src/kubernetes/utils/resolveNamespace.js +12 -0
  25. package/dist/src/monorepo/operations/tasks/RunTasksOperation.d.ts +2 -0
  26. package/dist/src/monorepo/operations/tasks/RunTasksOperation.js +43 -3
  27. package/oclif.manifest.json +142 -145
  28. package/package.json +1 -1
@@ -1,2 +1,3 @@
1
1
  export * from './client.js';
2
2
  export * from './operations/index.js';
3
+ export * from './utils/index.js';
@@ -0,0 +1,17 @@
1
+ import { V1Pod } from '@kubernetes/client-node';
2
+ import * as z from 'zod';
3
+ import { Component } from '../../monorepo/index.js';
4
+ import { AbstractOperation } from '../../operations/index.js';
5
+ declare const schema: z.ZodObject<{
6
+ component: z.ZodCustom<Component, Component>;
7
+ namespace: z.ZodString;
8
+ }, z.core.$strip>;
9
+ export interface GetComponentPodOutput {
10
+ container: string;
11
+ pod: V1Pod;
12
+ }
13
+ export declare class GetComponentPodOperation extends AbstractOperation<typeof schema, GetComponentPodOutput> {
14
+ constructor();
15
+ protected _run(input: z.input<typeof schema>): Promise<GetComponentPodOutput>;
16
+ }
17
+ export {};
@@ -0,0 +1,77 @@
1
+ import * as z from 'zod';
2
+ import { CliError } from '../../errors.js';
3
+ import { Component } from '../../monorepo/index.js';
4
+ import { AbstractOperation } from '../../operations/index.js';
5
+ const schema = z.object({
6
+ component: z
7
+ .instanceof(Component)
8
+ .describe('The component to get the pod for'),
9
+ namespace: z.string().describe('The Kubernetes namespace'),
10
+ });
11
+ export class GetComponentPodOperation extends AbstractOperation {
12
+ constructor() {
13
+ super(schema);
14
+ }
15
+ async _run(input) {
16
+ const { kubernetes, monorepo } = this.context;
17
+ const { component, namespace } = input;
18
+ const k8sConfig = component.config.kubernetes;
19
+ const projectK8sConfig = monorepo.config.defaults?.kubernetes;
20
+ // Build label selector: use explicit config or default convention
21
+ // Priority: component.kubernetes.selector > project.kubernetes.selectorLabel > default
22
+ const selectorLabel = projectK8sConfig?.selectorLabel ?? 'app.kubernetes.io/component';
23
+ const labelSelector = k8sConfig?.selector ?? `${selectorLabel}=${component.name}`;
24
+ // List pods matching the selector
25
+ const res = await kubernetes.core.listNamespacedPod({
26
+ namespace,
27
+ labelSelector,
28
+ });
29
+ // Filter to ready pods
30
+ const readyPods = res.items.filter((pod) => {
31
+ const conditions = pod.status?.conditions ?? [];
32
+ return conditions.some((c) => c.type === 'Ready' && c.status === 'True');
33
+ });
34
+ if (readyPods.length === 0) {
35
+ throw new CliError('K8S_NO_READY_PODS', `No ready pods found for component "${component.name}" in namespace "${namespace}"`, [
36
+ `Label selector used: ${labelSelector}`,
37
+ `Check pod status: kubectl get pods -l ${labelSelector} -n ${namespace}`,
38
+ `To use a different selector, add kubernetes.selector to component config`,
39
+ ]);
40
+ }
41
+ // Get first ready pod
42
+ const pod = readyPods[0];
43
+ // Determine container name
44
+ const containers = pod.spec?.containers ?? [];
45
+ if (containers.length === 0) {
46
+ throw new CliError('K8S_NO_CONTAINERS', `Pod "${pod.metadata?.name}" has no containers`);
47
+ }
48
+ let containerName;
49
+ if (k8sConfig?.container) {
50
+ // Use explicit container config
51
+ containerName = k8sConfig.container;
52
+ const containerExists = containers.some((c) => c.name === containerName);
53
+ if (!containerExists) {
54
+ throw new CliError('K8S_CONTAINER_NOT_FOUND', `Container "${containerName}" not found in pod "${pod.metadata?.name}"`, [
55
+ `Available containers: ${containers.map((c) => c.name).join(', ')}`,
56
+ `Update kubernetes.container in component config if needed`,
57
+ ]);
58
+ }
59
+ }
60
+ else if (containers.length === 1) {
61
+ // Single container pod: use it
62
+ containerName = containers[0].name;
63
+ }
64
+ else {
65
+ // Multi-container pod: require explicit config
66
+ throw new CliError('K8S_MULTI_CONTAINER', `Pod "${pod.metadata?.name}" has multiple containers, explicit container config required`, [
67
+ `Available containers: ${containers.map((c) => c.name).join(', ')}`,
68
+ `Add kubernetes.container to component "${component.name}" config:`,
69
+ ` components:`,
70
+ ` ${component.name}:`,
71
+ ` kubernetes:`,
72
+ ` container: <container-name>`,
73
+ ]);
74
+ }
75
+ return { pod, container: containerName };
76
+ }
77
+ }
@@ -10,10 +10,11 @@ export class GetDeploymentPodsOperation extends AbstractOperation {
10
10
  super(schema);
11
11
  }
12
12
  async _run(input) {
13
- const { kubernetes } = getContext();
13
+ const { kubernetes, monorepo } = getContext();
14
+ const selectorLabel = monorepo.config.defaults?.kubernetes?.selectorLabel ?? 'app.kubernetes.io/component';
14
15
  const res = await kubernetes.core.listNamespacedPod({
15
16
  namespace: input.namespace,
16
- labelSelector: `component=${input.deployment}`,
17
+ labelSelector: `${selectorLabel}=${input.deployment}`,
17
18
  });
18
19
  return res.items;
19
20
  }
@@ -0,0 +1,20 @@
1
+ import { Writable } from 'node:stream';
2
+ import * as z from 'zod';
3
+ import { AbstractOperation } from '../../operations/index.js';
4
+ declare const schema: z.ZodObject<{
5
+ namespace: z.ZodString;
6
+ podName: z.ZodString;
7
+ container: z.ZodOptional<z.ZodString>;
8
+ script: z.ZodString;
9
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
10
+ interactive: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
11
+ tty: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
12
+ workingDir: z.ZodOptional<z.ZodString>;
13
+ }, z.core.$strip>;
14
+ export declare class PodExecOperation extends AbstractOperation<typeof schema, void> {
15
+ protected out?: Writable | undefined;
16
+ constructor(out?: Writable | undefined);
17
+ protected _run(input: z.input<typeof schema>): Promise<void>;
18
+ private wrapError;
19
+ }
20
+ export {};
@@ -0,0 +1,158 @@
1
+ import { Exec } from '@kubernetes/client-node';
2
+ import * as z from 'zod';
3
+ import { CliError } from '../../errors.js';
4
+ import { AbstractOperation } from '../../operations/index.js';
5
+ const schema = z.object({
6
+ namespace: z.string().describe('The namespace of the pod'),
7
+ podName: z.string().describe('The name of the pod'),
8
+ container: z
9
+ .string()
10
+ .optional()
11
+ .describe('The container name (required for multi-container pods)'),
12
+ script: z.string().describe('Command to run, as a string'),
13
+ env: z
14
+ .record(z.string(), z.string())
15
+ .optional()
16
+ .describe('Environment variables to pass to the command'),
17
+ interactive: z
18
+ .boolean()
19
+ .default(false)
20
+ .optional()
21
+ .describe('Whether the command is interactive'),
22
+ tty: z.boolean().default(false).optional().describe('Allocate a pseudo-TTY'),
23
+ workingDir: z
24
+ .string()
25
+ .optional()
26
+ .describe('The working directory for the command'),
27
+ });
28
+ export class PodExecOperation extends AbstractOperation {
29
+ out;
30
+ constructor(out) {
31
+ super(schema);
32
+ this.out = out;
33
+ }
34
+ async _run(input) {
35
+ const { kubernetes } = this.context;
36
+ const exec = new Exec(kubernetes.config);
37
+ const isInteractive = input.interactive || input.tty;
38
+ // Build the command with optional workdir and env vars
39
+ let { script } = input;
40
+ // Handle working directory by wrapping the command
41
+ if (input.workingDir) {
42
+ script = `cd ${JSON.stringify(input.workingDir)} && ${script}`;
43
+ }
44
+ // Build environment variable exports
45
+ const envExports = Object.entries(input.env || {})
46
+ .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`)
47
+ .join('; ');
48
+ if (envExports) {
49
+ script = `${envExports}; ${script}`;
50
+ }
51
+ const command = ['sh', '-c', script];
52
+ // Determine container name
53
+ const containerName = input.container;
54
+ if (!containerName) {
55
+ throw new CliError('K8S_NO_CONTAINER', 'Container name is required', [
56
+ 'Specify kubernetes.container in component config:',
57
+ ' components:',
58
+ ' <component>:',
59
+ ' kubernetes:',
60
+ ' container: <container-name>',
61
+ ]);
62
+ }
63
+ const stdout = isInteractive ? process.stdout : (this.out ?? null);
64
+ const stderr = isInteractive ? process.stderr : (this.out ?? null);
65
+ const stdin = isInteractive ? process.stdin : null;
66
+ return new Promise((resolve, reject) => {
67
+ let exitCode = 0;
68
+ let ws;
69
+ let sigintHandler;
70
+ const cleanup = () => {
71
+ // Restore stdin raw mode
72
+ if (isInteractive && process.stdin.isTTY) {
73
+ process.stdin.setRawMode?.(false);
74
+ }
75
+ // Remove SIGINT handler
76
+ if (sigintHandler) {
77
+ process.off('SIGINT', sigintHandler);
78
+ }
79
+ };
80
+ const statusCallback = (status) => {
81
+ if (status.status === 'Success') {
82
+ exitCode = 0;
83
+ }
84
+ else if (status.status === 'Failure') {
85
+ // Try to extract exit code from the status message
86
+ const match = status.message?.match(/command terminated with exit code (\d+)/);
87
+ exitCode = match ? Number.parseInt(match[1], 10) : 1;
88
+ }
89
+ };
90
+ // Set raw mode for interactive sessions
91
+ if (isInteractive && process.stdin.isTTY) {
92
+ process.stdin.setRawMode?.(true);
93
+ // Handle Ctrl+C gracefully
94
+ sigintHandler = () => {
95
+ cleanup();
96
+ if (ws) {
97
+ ws.close();
98
+ }
99
+ reject(new Error('Interrupted'));
100
+ };
101
+ process.on('SIGINT', sigintHandler);
102
+ }
103
+ exec
104
+ .exec(input.namespace, input.podName, containerName, command, stdout, stderr, stdin, isInteractive ?? false, statusCallback)
105
+ .then((websocket) => {
106
+ ws = websocket;
107
+ websocket.on('close', () => {
108
+ cleanup();
109
+ if (exitCode === 0) {
110
+ resolve();
111
+ }
112
+ else {
113
+ reject(new Error(`command failed (exit ${exitCode})`));
114
+ }
115
+ });
116
+ websocket.on('error', (error) => {
117
+ cleanup();
118
+ reject(this.wrapError(error, input));
119
+ });
120
+ })
121
+ .catch((error) => {
122
+ cleanup();
123
+ reject(this.wrapError(error, input));
124
+ });
125
+ });
126
+ }
127
+ wrapError(error, input) {
128
+ const message = error.message || String(error);
129
+ // Handle common Kubernetes API errors
130
+ if (message.includes('not found') || message.includes('404')) {
131
+ return new CliError('K8S_POD_NOT_FOUND', `Pod "${input.podName}" not found in namespace "${input.namespace}"`, [
132
+ `Check pod exists: kubectl get pod ${input.podName} -n ${input.namespace}`,
133
+ `List pods: kubectl get pods -n ${input.namespace}`,
134
+ ]);
135
+ }
136
+ if (message.includes('Forbidden') || message.includes('403')) {
137
+ return new CliError('K8S_FORBIDDEN', `Access denied to pod "${input.podName}" in namespace "${input.namespace}"`, [
138
+ `Check permissions: kubectl auth can-i create pods/exec -n ${input.namespace}`,
139
+ 'Ensure your kubeconfig has the correct context and credentials',
140
+ ]);
141
+ }
142
+ if (message.includes('Unauthorized') || message.includes('401')) {
143
+ return new CliError('K8S_UNAUTHORIZED', 'Kubernetes authentication failed', [
144
+ 'Check your kubeconfig: kubectl config view',
145
+ 'Try re-authenticating: kubectl auth login',
146
+ ]);
147
+ }
148
+ if (message.includes('connection refused') ||
149
+ message.includes('ECONNREFUSED')) {
150
+ return new CliError('K8S_CONNECTION_REFUSED', 'Cannot connect to Kubernetes cluster', [
151
+ 'Check cluster is running: kubectl cluster-info',
152
+ 'Verify kubeconfig context: kubectl config current-context',
153
+ ]);
154
+ }
155
+ // Return original error if not a known type
156
+ return error;
157
+ }
158
+ }
@@ -1 +1,4 @@
1
+ export * from './GetComponentPodOperation.js';
2
+ export * from './GetDeploymentPodsOperation.js';
3
+ export * from './PodExecOperation.js';
1
4
  export * from './RestartPodsOperation.js';
@@ -1 +1,4 @@
1
+ export * from './GetComponentPodOperation.js';
2
+ export * from './GetDeploymentPodsOperation.js';
3
+ export * from './PodExecOperation.js';
1
4
  export * from './RestartPodsOperation.js';
@@ -0,0 +1 @@
1
+ export * from './resolveNamespace.js';
@@ -0,0 +1 @@
1
+ export * from './resolveNamespace.js';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Resolves the Kubernetes namespace to use for operations.
3
+ *
4
+ * Resolution order (first non-empty value wins):
5
+ * 1. CLI flag (--namespace)
6
+ * 2. Environment variable (K8S_NAMESPACE)
7
+ * 3. Config file (kubernetes.namespace in .emb.yml)
8
+ * 4. Default: "default"
9
+ */
10
+ export declare function resolveNamespace(options: {
11
+ cliFlag?: string;
12
+ config?: string;
13
+ }): string;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Resolves the Kubernetes namespace to use for operations.
3
+ *
4
+ * Resolution order (first non-empty value wins):
5
+ * 1. CLI flag (--namespace)
6
+ * 2. Environment variable (K8S_NAMESPACE)
7
+ * 3. Config file (kubernetes.namespace in .emb.yml)
8
+ * 4. Default: "default"
9
+ */
10
+ export function resolveNamespace(options) {
11
+ return (options.cliFlag || process.env.K8S_NAMESPACE || options.config || 'default');
12
+ }
@@ -3,6 +3,7 @@ import { TaskInfo } from '../../index.js';
3
3
  import { IOperation } from '../../../operations/index.js';
4
4
  export declare enum ExecutorType {
5
5
  container = "container",
6
+ kubernetes = "kubernetes",
6
7
  local = "local"
7
8
  }
8
9
  export type RunTasksOperationParams = {
@@ -22,6 +23,7 @@ export declare class RunTasksOperation implements IOperation<RunTasksOperationPa
22
23
  run(params: RunTasksOperationParams): Promise<Array<TaskInfo>>;
23
24
  protected runDocker(task: TaskWithScriptAndComponent, out?: Writable): Promise<void>;
24
25
  protected runLocal(task: TaskWithScript, _out: Writable): Promise<import("stream").Readable>;
26
+ protected runKubernetes(task: TaskWithScriptAndComponent, out?: Writable): Promise<void>;
25
27
  private defaultExecutorFor;
26
28
  private ensureExecutorValid;
27
29
  private availableExecutorsFor;
@@ -3,11 +3,14 @@ import { input } from '@inquirer/prompts';
3
3
  import { ListrInquirerPromptAdapter } from '@listr2/prompt-adapter-inquirer';
4
4
  import { PassThrough } from 'node:stream';
5
5
  import { ContainerExecOperation } from '../../../docker/index.js';
6
+ import { GetComponentPodOperation, PodExecOperation, } from '../../../kubernetes/operations/index.js';
7
+ import { resolveNamespace } from '../../../kubernetes/utils/index.js';
6
8
  import { EMBCollection, findRunOrder } from '../../index.js';
7
9
  import { ExecuteLocalCommandOperation } from '../index.js';
8
10
  export var ExecutorType;
9
11
  (function (ExecutorType) {
10
12
  ExecutorType["container"] = "container";
13
+ ExecutorType["kubernetes"] = "kubernetes";
11
14
  ExecutorType["local"] = "local";
12
15
  })(ExecutorType || (ExecutorType = {}));
13
16
  export class RunTasksOperation {
@@ -59,6 +62,9 @@ export class RunTasksOperation {
59
62
  case ExecutorType.container: {
60
63
  return this.runDocker(task, tee);
61
64
  }
65
+ case ExecutorType.kubernetes: {
66
+ return this.runKubernetes(task, tee);
67
+ }
62
68
  case ExecutorType.local: {
63
69
  return this.runLocal(task, tee);
64
70
  }
@@ -94,6 +100,30 @@ export class RunTasksOperation {
94
100
  env: await monorepo.expand(task.vars || {}),
95
101
  });
96
102
  }
103
+ async runKubernetes(task, out) {
104
+ const { monorepo } = getContext();
105
+ const component = monorepo.component(task.component);
106
+ const namespace = resolveNamespace({
107
+ config: monorepo.config.defaults?.kubernetes?.namespace,
108
+ });
109
+ // Resolve the pod and container for this component
110
+ const { pod, container } = await monorepo.run(new GetComponentPodOperation(), {
111
+ component,
112
+ namespace,
113
+ });
114
+ const podName = pod.metadata?.name;
115
+ if (!podName) {
116
+ throw new Error('Pod has no name');
117
+ }
118
+ return monorepo.run(new PodExecOperation(task.interactive ? undefined : out), {
119
+ namespace,
120
+ podName,
121
+ container,
122
+ script: task.script,
123
+ interactive: task.interactive || false,
124
+ env: await monorepo.expand(task.vars || {}),
125
+ });
126
+ }
97
127
  async defaultExecutorFor(task) {
98
128
  const available = await this.availableExecutorsFor(task);
99
129
  if (available.length === 0) {
@@ -112,8 +142,18 @@ export class RunTasksOperation {
112
142
  if (task.executors) {
113
143
  return task.executors;
114
144
  }
115
- return task.component && (await compose.isService(task.component))
116
- ? [ExecutorType.container, ExecutorType.local]
117
- : [ExecutorType.local];
145
+ // For tasks with a component, check what executors are available
146
+ if (task.component) {
147
+ const available = [ExecutorType.local];
148
+ // Container executor available if component is a docker-compose service
149
+ if (await compose.isService(task.component)) {
150
+ available.unshift(ExecutorType.container);
151
+ }
152
+ // Kubernetes executor is always available for component tasks
153
+ // (actual availability checked at runtime when --executor kubernetes is used)
154
+ available.push(ExecutorType.kubernetes);
155
+ return available;
156
+ }
157
+ return [ExecutorType.local];
118
158
  }
119
159
  }