@oisincoveney/pipeline 2.0.0 → 2.0.1

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.
@@ -303,25 +303,6 @@ function runnerWorkflowStorage(options, tasks) {
303
303
  name: "runner-git-credentials",
304
304
  secret: {
305
305
  defaultMode: 256,
306
- items: [
307
- {
308
- key: "username",
309
- path: "username"
310
- },
311
- {
312
- key: "password",
313
- path: "password"
314
- },
315
- {
316
- key: "identity",
317
- path: "identity"
318
- },
319
- {
320
- key: "known_hosts",
321
- path: "known_hosts"
322
- }
323
- ],
324
- optional: true,
325
306
  secretName: options.gitCredentialsSecretName
326
307
  }
327
308
  });
@@ -26,6 +26,7 @@ interface DoctorResult {
26
26
  interface DoctorFlags {
27
27
  cluster?: boolean | string;
28
28
  kubeContext?: string;
29
+ kubeconfig?: string;
29
30
  }
30
31
  declare function createCliProgram(): Command;
31
32
  declare function runCli(argv: string[]): Promise<void>;
@@ -5,6 +5,7 @@ import { createOrchestratorLaunchPlan, createRunnerLaunchPlan } from "../runner.
5
5
  import { compileWorkflowPlan } from "../workflow-planner.js";
6
6
  import { compileScheduleArtifact, generateScheduleArtifact, parseScheduleArtifact } from "../schedule/planner.js";
7
7
  import "../schedule-planner.js";
8
+ import { loadMokaGlobalConfig } from "../moka-global-config.js";
8
9
  import { defaultClusterDoctorNamespace, runClusterDoctor } from "../cluster-doctor.js";
9
10
  import { formatCodexAuthSyncResult, syncLocalCodexAuth } from "../codex-auth-sync.js";
10
11
  import { registerConfiguredEntrypointCommands } from "../commands/pipeline-command.js";
@@ -133,7 +134,7 @@ function createCliProgram() {
133
134
  const config = loadPipelineConfig(cwd, { allowMissingLintFileReferences: true });
134
135
  console.log(formatSelectedWorkflowPlan(config, cwd, flags));
135
136
  });
136
- program.command("doctor").description("Check local prerequisites for pipeline init and execution").option("--cluster [namespace]", "also check runner-job Kubernetes prerequisites").option("--kube-context <context>", "kubectl context for cluster checks").action(async (flags) => {
137
+ program.command("doctor").description("Check local prerequisites for pipeline init and execution").option("--cluster [namespace]", "also check runner-job Kubernetes prerequisites").option("--kube-context <context>", "kubectl context for cluster checks").option("--kubeconfig <path>", "kubeconfig path for cluster checks").action(async (flags) => {
137
138
  const result = await runDoctor(process.env.PIPELINE_TARGET_PATH ?? process.cwd(), flags);
138
139
  console.log(formatDoctorResult(result));
139
140
  if (!result.passed) throw new Error("Doctor checks failed.");
@@ -241,9 +242,11 @@ async function runDoctor(cwd, options = {}) {
241
242
  checkCommand("fallow", ["--version"], cwd)
242
243
  ]);
243
244
  const configCheck = checkPipelineConfig(cwd);
245
+ const globalConfig = loadMokaGlobalConfig();
244
246
  const clusterResult = options.cluster ? await runClusterDoctor({
245
247
  kubeContext: options.kubeContext,
246
- namespace: clusterNamespace(options.cluster)
248
+ kubeconfigPath: options.kubeconfig ?? globalConfig?.momokaya.kubernetes.kubeconfig,
249
+ namespace: clusterNamespace(options.cluster, globalConfig?.momokaya.kubernetes.namespace)
247
250
  }) : { checks: [] };
248
251
  const checks = [
249
252
  ...commandChecks,
@@ -255,8 +258,8 @@ async function runDoctor(cwd, options = {}) {
255
258
  passed: checks.every((check) => check.passed)
256
259
  };
257
260
  }
258
- function clusterNamespace(value) {
259
- return typeof value === "string" && value.length > 0 ? value : defaultClusterDoctorNamespace();
261
+ function clusterNamespace(value, configuredNamespace) {
262
+ return typeof value === "string" && value.length > 0 ? value : configuredNamespace ?? defaultClusterDoctorNamespace();
260
263
  }
261
264
  function checkCommand(name, args, cwd) {
262
265
  return checkCommandWithRunner(name, name, args, cwd);
@@ -13,33 +13,38 @@ const DEFAULT_RESOURCES = {
13
13
  queueName: "pipeline-runner",
14
14
  serviceAccountName: "pipeline-runner"
15
15
  };
16
+ const FORBIDDEN_RE = /forbidden/i;
16
17
  async function runClusterDoctor(options = {}) {
17
18
  const resources = clusterResources();
18
19
  const namespace = options.namespace ?? DEFAULT_NAMESPACE;
20
+ const kubectlOptions = {
21
+ kubeContext: options.kubeContext,
22
+ kubeconfigPath: options.kubeconfigPath
23
+ };
19
24
  const checks = await Promise.all([
20
- checkKubectlNamespace(namespace, options.kubeContext),
21
- ...secretChecks(namespace, options.kubeContext, resources),
22
- checkExternalSecret(namespace, resources.eventAuthExternalSecretName, resources.externalSecretRemoteRef, options.kubeContext),
23
- checkClusterSecretStore("openbao", options.kubeContext),
24
- checkServiceAccount(namespace, resources.serviceAccountName, options.kubeContext),
25
- checkServiceAccountPermission(namespace, resources.serviceAccountName, {
26
- kubeContext: options.kubeContext,
25
+ checkKubectlNamespace(namespace, kubectlOptions),
26
+ ...secretChecks(namespace, kubectlOptions, resources),
27
+ checkExternalSecret(namespace, resources.eventAuthExternalSecretName, resources.externalSecretRemoteRef, kubectlOptions),
28
+ checkClusterSecretStore("openbao", kubectlOptions),
29
+ checkServiceAccount(namespace, resources.serviceAccountName, kubectlOptions),
30
+ checkWorkflowSubmitPermission(namespace, {
27
31
  resource: "workflows.argoproj.io",
28
- verb: "create"
32
+ verb: "create",
33
+ ...kubectlOptions
29
34
  }),
30
- checkLocalQueue(namespace, resources.queueName, options.kubeContext),
35
+ checkLocalQueue(namespace, resources.queueName, kubectlOptions),
31
36
  checkClusterResource("argo-workflow-crd", [
32
37
  "get",
33
38
  "crd",
34
39
  "workflows.argoproj.io"
35
- ], options.kubeContext),
40
+ ], kubectlOptions),
36
41
  checkClusterResource("argo-workflow-controller", [
37
42
  "get",
38
43
  "pods",
39
44
  "-A",
40
45
  "-l",
41
46
  "app=workflow-controller"
42
- ], options.kubeContext)
47
+ ], kubectlOptions)
43
48
  ]);
44
49
  return {
45
50
  checks,
@@ -62,7 +67,7 @@ function clusterResources() {
62
67
  serviceAccountName: configured.serviceAccountName
63
68
  } : DEFAULT_RESOURCES;
64
69
  }
65
- function secretChecks(namespace, kubeContext, resources) {
70
+ function secretChecks(namespace, kubectlOptions, resources) {
66
71
  return [
67
72
  [resources.eventAuthSecretName, eventAuthMissingDetail(namespace)],
68
73
  [resources.imagePullSecretName, `Secret ${resources.imagePullSecretName} missing in ${namespace}; expected imagePullSecret for ghcr.io/oisin-ee/pipeline-runner.`],
@@ -75,30 +80,31 @@ function secretChecks(namespace, kubeContext, resources) {
75
80
  name,
76
81
  "-n",
77
82
  namespace
78
- ], missingDetail, kubeContext));
83
+ ], missingDetail, kubectlOptions));
79
84
  }
80
85
  function eventAuthMissingDetail(namespace) {
81
86
  return `Secret pipeline-runner-event-auth missing in ${namespace}; expected ExternalSecret pipeline-runner-event-auth to sync it from agent-runtime/pipeline-runner/event-auth.`;
82
87
  }
83
- function checkKubectlNamespace(namespace, kubeContext) {
88
+ function checkKubectlNamespace(namespace, kubectlOptions) {
84
89
  return checkNamespacedResource(`namespace/${namespace}`, [
85
90
  "get",
86
91
  "namespace",
87
92
  namespace
88
- ], `Namespace ${namespace} missing or inaccessible.`, kubeContext);
93
+ ], `Namespace ${namespace} missing or inaccessible.`, kubectlOptions);
89
94
  }
90
- async function checkNamespacedResource(name, args, missingDetail, kubeContext) {
91
- return (await kubectl(args, kubeContext)).ok ? {
95
+ async function checkNamespacedResource(name, args, missingDetail, kubectlOptions) {
96
+ const result = await kubectl(args, kubectlOptions);
97
+ return result.ok ? {
92
98
  detail: "present",
93
99
  name,
94
100
  passed: true
95
101
  } : {
96
- detail: missingDetail,
102
+ detail: inaccessibleOrMissingDetail(name, missingDetail, result),
97
103
  name,
98
104
  passed: false
99
105
  };
100
106
  }
101
- async function checkExternalSecret(namespace, name, remoteRef, kubeContext) {
107
+ async function checkExternalSecret(namespace, name, remoteRef, kubectlOptions) {
102
108
  const result = await kubectl([
103
109
  "get",
104
110
  "externalsecret",
@@ -107,76 +113,79 @@ async function checkExternalSecret(namespace, name, remoteRef, kubeContext) {
107
113
  namespace,
108
114
  "-o",
109
115
  "json"
110
- ], kubeContext);
111
- if (!result.ok) return {
112
- detail: `ExternalSecret ${name} missing in ${namespace}; expected it to sync ${remoteRef}.`,
113
- name: `externalsecret/${name}`,
114
- passed: false
115
- };
116
+ ], kubectlOptions);
117
+ if (!result.ok) {
118
+ const missingDetail = `ExternalSecret ${name} missing in ${namespace}; expected it to sync ${remoteRef}.`;
119
+ return {
120
+ detail: inaccessibleOrMissingDetail(`externalsecret/${name}`, missingDetail, result),
121
+ name: `externalsecret/${name}`,
122
+ passed: false
123
+ };
124
+ }
116
125
  return readyConditionCheck(`externalsecret/${name}`, result.stdout);
117
126
  }
118
- async function checkClusterSecretStore(name, kubeContext) {
127
+ async function checkClusterSecretStore(name, kubectlOptions) {
119
128
  const result = await kubectl([
120
129
  "get",
121
130
  "clustersecretstore",
122
131
  name,
123
132
  "-o",
124
133
  "json"
125
- ], kubeContext);
126
- if (!result.ok) return {
127
- detail: `ClusterSecretStore/${name} missing or inaccessible; OpenBao/ESO readiness is an external prerequisite.`,
128
- name: `clustersecretstore/${name}`,
129
- passed: false
130
- };
134
+ ], kubectlOptions);
135
+ if (!result.ok) {
136
+ const missingDetail = `ClusterSecretStore/${name} missing or inaccessible; OpenBao/ESO readiness is an external prerequisite.`;
137
+ return {
138
+ detail: inaccessibleOrMissingDetail(`clustersecretstore/${name}`, missingDetail, result),
139
+ name: `clustersecretstore/${name}`,
140
+ passed: false
141
+ };
142
+ }
131
143
  return readyConditionCheck(`clustersecretstore/${name}`, result.stdout);
132
144
  }
133
- function checkServiceAccount(namespace, name, kubeContext) {
145
+ function checkServiceAccount(namespace, name, kubectlOptions) {
134
146
  return checkNamespacedResource(`serviceaccount/${name}`, [
135
147
  "get",
136
148
  "serviceaccount",
137
149
  name,
138
150
  "-n",
139
151
  namespace
140
- ], `ServiceAccount ${name} missing in ${namespace}; runner pods must use this account for workflow execution.`, kubeContext);
152
+ ], `ServiceAccount ${name} missing in ${namespace}; runner pods must use this account for workflow execution.`, kubectlOptions);
141
153
  }
142
- async function checkServiceAccountPermission(namespace, serviceAccountName, options) {
143
- const subject = `system:serviceaccount:${namespace}:${serviceAccountName}`;
154
+ async function checkWorkflowSubmitPermission(namespace, options) {
144
155
  return (await kubectl([
145
156
  "auth",
146
157
  "can-i",
147
158
  options.verb,
148
159
  options.resource,
149
- "--as",
150
- subject,
151
160
  "-n",
152
161
  namespace
153
- ], options.kubeContext)).stdout.trim() === "yes" ? {
154
- detail: `${subject} can ${options.verb} ${options.resource}`,
162
+ ], options)).stdout.trim() === "yes" ? {
163
+ detail: `current kube identity can ${options.verb} ${options.resource}`,
155
164
  name: "rbac/workflow-create",
156
165
  passed: true
157
166
  } : {
158
- detail: `${subject} cannot ${options.verb} ${options.resource}; check runner ServiceAccount RBAC.`,
167
+ detail: `current kube identity cannot ${options.verb} ${options.resource}; check submitter RBAC for Workflow creation.`,
159
168
  name: "rbac/workflow-create",
160
169
  passed: false
161
170
  };
162
171
  }
163
- function checkLocalQueue(namespace, queueName, kubeContext) {
172
+ function checkLocalQueue(namespace, queueName, kubectlOptions) {
164
173
  return checkNamespacedResource(`localqueue/${queueName}`, [
165
174
  "get",
166
175
  "localqueue",
167
176
  queueName,
168
177
  "-n",
169
178
  namespace
170
- ], `Kueue LocalQueue ${queueName} missing in ${namespace}; runner Workflow pods cannot be admitted to the expected queue.`, kubeContext);
179
+ ], `Kueue LocalQueue ${queueName} missing in ${namespace}; runner Workflow pods cannot be admitted to the expected queue.`, kubectlOptions);
171
180
  }
172
- async function checkClusterResource(name, args, kubeContext) {
173
- const result = await kubectl(args, kubeContext);
181
+ async function checkClusterResource(name, args, kubectlOptions) {
182
+ const result = await kubectl(args, kubectlOptions);
174
183
  return result.ok ? {
175
184
  detail: "present",
176
185
  name,
177
186
  passed: true
178
187
  } : {
179
- detail: result.stderr || "missing or inaccessible",
188
+ detail: isForbidden(result) ? inaccessibleDetail(name, result) : result.stderr || "missing or inaccessible",
180
189
  name,
181
190
  passed: false
182
191
  };
@@ -193,9 +202,12 @@ function readyConditionCheck(name, source) {
193
202
  passed: false
194
203
  };
195
204
  }
196
- async function kubectl(args, kubeContext) {
205
+ async function kubectl(args, options) {
197
206
  try {
198
- const result = await execa("kubectl", kubectlArgs(args, kubeContext), { stdin: "ignore" });
207
+ const result = await execa("kubectl", kubectlArgs(args, options.kubeContext), {
208
+ env: options.kubeconfigPath ? { KUBECONFIG: options.kubeconfigPath } : void 0,
209
+ stdin: "ignore"
210
+ });
199
211
  return {
200
212
  ok: true,
201
213
  stderr: result.stderr,
@@ -217,6 +229,15 @@ function kubectlArgs(args, kubeContext) {
217
229
  ...args
218
230
  ] : args;
219
231
  }
232
+ function inaccessibleOrMissingDetail(name, missingDetail, result) {
233
+ return isForbidden(result) ? inaccessibleDetail(name, result) : missingDetail;
234
+ }
235
+ function inaccessibleDetail(name, result) {
236
+ return `${name} inaccessible with the current kube identity: ${result.stderr}`;
237
+ }
238
+ function isForbidden(result) {
239
+ return FORBIDDEN_RE.test(result.stderr);
240
+ }
220
241
  function parseJson(source) {
221
242
  try {
222
243
  return JSON.parse(source);
@@ -310,6 +310,7 @@ declare const configSchema: z.ZodObject<{
310
310
  skills: z.ZodOptional<z.ZodArray<z.ZodString>>;
311
311
  timeout_ms: z.ZodOptional<z.ZodNumber>;
312
312
  tools: z.ZodOptional<z.ZodArray<z.ZodEnum<{
313
+ task: "task";
313
314
  read: "read";
314
315
  list: "list";
315
316
  grep: "grep";
@@ -317,7 +318,6 @@ declare const configSchema: z.ZodObject<{
317
318
  bash: "bash";
318
319
  edit: "edit";
319
320
  write: "write";
320
- task: "task";
321
321
  }>>>;
322
322
  }, z.core.$strict>>>;
323
323
  runner_command: z.ZodDefault<z.ZodObject<{
@@ -369,6 +369,7 @@ declare const configSchema: z.ZodObject<{
369
369
  rules: z.ZodOptional<z.ZodBoolean>;
370
370
  skills: z.ZodOptional<z.ZodBoolean>;
371
371
  tools: z.ZodOptional<z.ZodArray<z.ZodEnum<{
372
+ task: "task";
372
373
  read: "read";
373
374
  list: "list";
374
375
  grep: "grep";
@@ -376,7 +377,6 @@ declare const configSchema: z.ZodObject<{
376
377
  bash: "bash";
377
378
  edit: "edit";
378
379
  write: "write";
379
- task: "task";
380
380
  }>>>;
381
381
  }, z.core.$strict>;
382
382
  command: z.ZodOptional<z.ZodString>;