@enspirit/emb 0.23.0 → 0.25.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 (42) hide show
  1. package/README.md +84 -45
  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/images/push.d.ts +3 -0
  5. package/dist/src/cli/commands/images/push.js +4 -1
  6. package/dist/src/cli/commands/kubernetes/logs.js +7 -12
  7. package/dist/src/cli/commands/kubernetes/ps.js +2 -1
  8. package/dist/src/cli/commands/kubernetes/restart.js +2 -1
  9. package/dist/src/cli/commands/kubernetes/shell.js +7 -10
  10. package/dist/src/cli/commands/resources/build.d.ts +1 -0
  11. package/dist/src/cli/commands/resources/build.js +17 -3
  12. package/dist/src/cli/commands/resources/index.d.ts +3 -1
  13. package/dist/src/cli/commands/resources/index.js +23 -5
  14. package/dist/src/cli/commands/resources/publish.d.ts +15 -0
  15. package/dist/src/cli/commands/resources/publish.js +35 -0
  16. package/dist/src/config/schema.d.ts +51 -1
  17. package/dist/src/config/schema.json +68 -1
  18. package/dist/src/docker/compose/client.js +2 -2
  19. package/dist/src/docker/compose/operations/ComposeLogsArchiveOperation.d.ts +1 -1
  20. package/dist/src/docker/credentials.d.ts +20 -0
  21. package/dist/src/docker/credentials.js +157 -0
  22. package/dist/src/docker/operations/images/PushImagesOperation.js +5 -7
  23. package/dist/src/docker/resources/DockerImageResource.js +79 -0
  24. package/dist/src/kubernetes/index.d.ts +1 -0
  25. package/dist/src/kubernetes/index.js +1 -0
  26. package/dist/src/kubernetes/operations/GetComponentPodOperation.d.ts +17 -0
  27. package/dist/src/kubernetes/operations/GetComponentPodOperation.js +77 -0
  28. package/dist/src/kubernetes/operations/GetDeploymentPodsOperation.js +4 -2
  29. package/dist/src/kubernetes/operations/PodExecOperation.d.ts +20 -0
  30. package/dist/src/kubernetes/operations/PodExecOperation.js +158 -0
  31. package/dist/src/kubernetes/operations/index.d.ts +3 -0
  32. package/dist/src/kubernetes/operations/index.js +3 -0
  33. package/dist/src/kubernetes/utils/index.d.ts +1 -0
  34. package/dist/src/kubernetes/utils/index.js +1 -0
  35. package/dist/src/kubernetes/utils/resolveNamespace.d.ts +13 -0
  36. package/dist/src/kubernetes/utils/resolveNamespace.js +12 -0
  37. package/dist/src/monorepo/operations/resources/PublishResourcesOperation.d.ts +19 -0
  38. package/dist/src/monorepo/operations/resources/PublishResourcesOperation.js +120 -0
  39. package/dist/src/monorepo/operations/tasks/RunTasksOperation.d.ts +2 -0
  40. package/dist/src/monorepo/operations/tasks/RunTasksOperation.js +43 -3
  41. package/oclif.manifest.json +98 -12
  42. package/package.json +1 -1
@@ -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,12 @@ 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 ??
15
+ 'app.kubernetes.io/component';
14
16
  const res = await kubernetes.core.listNamespacedPod({
15
17
  namespace: input.namespace,
16
- labelSelector: `component=${input.deployment}`,
18
+ labelSelector: `${selectorLabel}=${input.deployment}`,
17
19
  });
18
20
  return res.items;
19
21
  }
@@ -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
+ }
@@ -0,0 +1,19 @@
1
+ import * as z from 'zod';
2
+ import { ResourceInfo } from '../../index.js';
3
+ import { AbstractOperation } from '../../../operations/index.js';
4
+ declare const schema: z.ZodObject<{
5
+ resources: z.ZodOptional<z.ZodArray<z.ZodString>>;
6
+ dryRun: z.ZodOptional<z.ZodBoolean>;
7
+ silent: z.ZodOptional<z.ZodBoolean>;
8
+ }, z.core.$strip>;
9
+ export type PublishResourceMeta = {
10
+ resource: ResourceInfo;
11
+ reference: string;
12
+ skipped?: boolean;
13
+ skipReason?: string;
14
+ };
15
+ export declare class PublishResourcesOperation extends AbstractOperation<typeof schema, Record<string, PublishResourceMeta>> {
16
+ constructor();
17
+ protected _run(input: z.input<typeof schema>): Promise<Record<string, PublishResourceMeta>>;
18
+ }
19
+ export {};
@@ -0,0 +1,120 @@
1
+ import { PRESET_TIMER } from 'listr2';
2
+ import * as z from 'zod';
3
+ import { CliError } from '../../../errors.js';
4
+ import { EMBCollection, findRunOrder, ResourceFactory, } from '../../index.js';
5
+ import { AbstractOperation } from '../../../operations/index.js';
6
+ const schema = z.object({
7
+ resources: z
8
+ .array(z.string())
9
+ .describe('The list of resources to publish')
10
+ .optional(),
11
+ dryRun: z
12
+ .boolean()
13
+ .optional()
14
+ .describe('Do not publish, just show what would be published'),
15
+ silent: z
16
+ .boolean()
17
+ .optional()
18
+ .describe('Do not produce any output on the terminal'),
19
+ });
20
+ export class PublishResourcesOperation extends AbstractOperation {
21
+ constructor() {
22
+ super(schema);
23
+ }
24
+ async _run(input) {
25
+ const { monorepo } = this.context;
26
+ const manager = monorepo.taskManager();
27
+ // Filter to only publishable resources (publish: true)
28
+ const publishableResources = monorepo.resources.filter((r) => r.publish === true);
29
+ // Return early if no publishable resources
30
+ if (publishableResources.length === 0) {
31
+ return {};
32
+ }
33
+ // If specific resources requested, filter to those
34
+ let targetResources;
35
+ if (input.resources && input.resources.length > 0) {
36
+ const collection = new EMBCollection(publishableResources, {
37
+ idField: 'id',
38
+ depField: 'dependencies',
39
+ });
40
+ targetResources = findRunOrder(input.resources, collection);
41
+ }
42
+ else {
43
+ // All publishable resources
44
+ const collection = new EMBCollection(publishableResources, {
45
+ idField: 'id',
46
+ depField: 'dependencies',
47
+ });
48
+ targetResources = findRunOrder(publishableResources.map((r) => r.id), collection);
49
+ }
50
+ // Verify each resource's builder supports publish
51
+ for (const resource of targetResources) {
52
+ const component = monorepo.component(resource.component);
53
+ const builder = ResourceFactory.factor(resource.type, {
54
+ config: resource,
55
+ monorepo,
56
+ component,
57
+ });
58
+ if (typeof builder.publish !== 'function') {
59
+ throw new CliError('PUBLISH_NOT_SUPPORTED', `Resource "${resource.id}" has publish: true but resource type "${resource.type}" does not support publishing.`, [
60
+ `Remove "publish: true" from the resource configuration`,
61
+ `Use a different resource type that supports publishing (e.g., docker/image)`,
62
+ ]);
63
+ }
64
+ }
65
+ const tasks = targetResources.map((resource) => {
66
+ return {
67
+ title: `Publish ${resource.id}`,
68
+ async task(ctx, task) {
69
+ const component = monorepo.component(resource.component);
70
+ const builder = ResourceFactory.factor(resource.type, {
71
+ config: resource,
72
+ monorepo,
73
+ component,
74
+ });
75
+ const reference = await builder.getReference();
76
+ ctx[resource.id] = {
77
+ resource,
78
+ reference,
79
+ };
80
+ if (input.dryRun) {
81
+ ctx[resource.id].skipped = true;
82
+ ctx[resource.id].skipReason = 'dry run';
83
+ task.title = `[dry run] ${resource.id} → ${reference}`;
84
+ return task.skip();
85
+ }
86
+ task.title = `Publishing ${resource.id} → ${reference}`;
87
+ await builder.publish(resource, task.stdout());
88
+ },
89
+ };
90
+ });
91
+ if (tasks.length === 0) {
92
+ return {};
93
+ }
94
+ return manager.run([
95
+ {
96
+ title: 'Publish resources',
97
+ async task(_ctx, task) {
98
+ return task.newListr(tasks, {
99
+ rendererOptions: {
100
+ collapseSubtasks: false,
101
+ collapseSkips: true,
102
+ },
103
+ });
104
+ },
105
+ },
106
+ ], {
107
+ silentRendererCondition() {
108
+ return Boolean(input.silent);
109
+ },
110
+ rendererOptions: {
111
+ collapseSkips: true,
112
+ collapseSubtasks: true,
113
+ timer: {
114
+ ...PRESET_TIMER,
115
+ },
116
+ },
117
+ ctx: {},
118
+ });
119
+ }
120
+ }
@@ -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
  }