@numa-tech/numa 1.12.11 → 1.13.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.
- package/README.md +111 -4
- package/dist/application-onboarding/client.d.ts +83 -8
- package/dist/application-onboarding/client.js +23 -1
- package/dist/application-onboarding/client.js.map +1 -1
- package/dist/application-onboarding/commands.js +72 -6
- package/dist/application-onboarding/commands.js.map +1 -1
- package/dist/application-onboarding/schemas.d.ts +301 -58
- package/dist/application-onboarding/schemas.js +134 -8
- package/dist/application-onboarding/schemas.js.map +1 -1
- package/dist/application-onboarding/tui-model.d.ts +20 -0
- package/dist/application-onboarding/tui-model.js +362 -20
- package/dist/application-onboarding/tui-model.js.map +1 -1
- package/dist/application-onboarding/tui.d.ts +6 -1
- package/dist/application-onboarding/tui.js +104 -7
- package/dist/application-onboarding/tui.js.map +1 -1
- package/dist/cli.js +50 -8
- package/dist/cli.js.map +1 -1
- package/dist/clusters/client.d.ts +436 -0
- package/dist/clusters/client.js +207 -0
- package/dist/clusters/client.js.map +1 -0
- package/dist/clusters/commands.d.ts +11 -0
- package/dist/clusters/commands.js +373 -0
- package/dist/clusters/commands.js.map +1 -0
- package/dist/clusters/errors.d.ts +10 -0
- package/dist/clusters/errors.js +61 -0
- package/dist/clusters/errors.js.map +1 -0
- package/dist/clusters/kubeconfig-discovery.d.ts +48 -0
- package/dist/clusters/kubeconfig-discovery.js +280 -0
- package/dist/clusters/kubeconfig-discovery.js.map +1 -0
- package/dist/clusters/schemas.d.ts +510 -0
- package/dist/clusters/schemas.js +201 -0
- package/dist/clusters/schemas.js.map +1 -0
- package/dist/command-catalog.js +370 -1
- package/dist/command-catalog.js.map +1 -1
- package/dist/gitops/client.d.ts +499 -0
- package/dist/gitops/client.js +163 -0
- package/dist/gitops/client.js.map +1 -0
- package/dist/gitops/commands.d.ts +11 -0
- package/dist/gitops/commands.js +367 -0
- package/dist/gitops/commands.js.map +1 -0
- package/dist/gitops/errors.d.ts +10 -0
- package/dist/gitops/errors.js +59 -0
- package/dist/gitops/errors.js.map +1 -0
- package/dist/gitops/schemas.d.ts +1712 -0
- package/dist/gitops/schemas.js +360 -0
- package/dist/gitops/schemas.js.map +1 -0
- package/dist/repositories/client.d.ts +558 -0
- package/dist/repositories/client.js +215 -0
- package/dist/repositories/client.js.map +1 -0
- package/dist/repositories/commands.d.ts +13 -0
- package/dist/repositories/commands.js +368 -0
- package/dist/repositories/commands.js.map +1 -0
- package/dist/repositories/errors.d.ts +10 -0
- package/dist/repositories/errors.js +60 -0
- package/dist/repositories/errors.js.map +1 -0
- package/dist/repositories/schemas.d.ts +1724 -0
- package/dist/repositories/schemas.js +261 -0
- package/dist/repositories/schemas.js.map +1 -0
- package/package.json +4 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OnboardingError } from "./errors.js";
|
|
2
|
-
import { CredentialFreeOnboardingValueSchema } from "./schemas.js";
|
|
2
|
+
import { CredentialFreeOnboardingValueSchema, OnboardingSpecSchema } from "./schemas.js";
|
|
3
3
|
export const ONBOARDING_STEPS = [
|
|
4
4
|
"MODE",
|
|
5
5
|
"BASIC_INFORMATION",
|
|
@@ -11,21 +11,54 @@ export const ONBOARDING_STEPS = [
|
|
|
11
11
|
"REVIEW",
|
|
12
12
|
"EXECUTION"
|
|
13
13
|
];
|
|
14
|
+
const DEFAULT_GITOPS_STRUCTURAL_SPEC = {
|
|
15
|
+
workloadKind: "SPRING",
|
|
16
|
+
environment: {},
|
|
17
|
+
secretEnvironment: []
|
|
18
|
+
};
|
|
19
|
+
const GITOPS_STRUCTURAL_FIELDS = new Set([
|
|
20
|
+
"workloadKind", "chart", "workload", "service", "probes", "ingress",
|
|
21
|
+
"environment", "secretEnvironment", "advancedValues"
|
|
22
|
+
]);
|
|
23
|
+
const GITOPS_PLATFORM_MANAGED_FIELDS = new Set([
|
|
24
|
+
"image", "appCode", "environmentId", "namespace", "releaseName", "targetKey",
|
|
25
|
+
"repository", "writeBranch", "reconcileBranch", "overlayPath", "imageRepository", "imageTag"
|
|
26
|
+
]);
|
|
27
|
+
const GITOPS_MANAGED_ADVANCED_VALUES = new Set([
|
|
28
|
+
"image", "replicaCount", "strategy", "service", "ingress", "startupProbe",
|
|
29
|
+
"livenessProbe", "readinessProbe", "resources", "env"
|
|
30
|
+
]);
|
|
31
|
+
const GITOPS_SENSITIVE_ENVIRONMENT_NAME = /(^|_)(PASSWORD|PASSWD|PWD|SECRET|TOKEN|API_KEY|PRIVATE_KEY|ACCESS_KEY|SECRET_KEY|CREDENTIAL|DATABASE_URL|REDIS_URL|MONGO_URI|JDBC_URL)($|_)/u;
|
|
32
|
+
const GITOPS_ENVIRONMENT_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
14
33
|
export function emptyWizardDraft(mode) {
|
|
15
34
|
return {
|
|
16
|
-
schemaVersion:
|
|
35
|
+
schemaVersion: 2,
|
|
17
36
|
mode,
|
|
18
37
|
application: { lifecycle: "EXPERIMENTAL", tier: "TIER_3" },
|
|
19
|
-
|
|
20
|
-
|
|
38
|
+
repositoryStrategy: mode === "EXISTING" ? "ATTACH_EXISTING" : "CREATE_NEW",
|
|
39
|
+
projectStrategy: mode === "EXISTING" ? "ASSESS_ONLY" : "BOOTSTRAP_TEMPLATE",
|
|
40
|
+
technology: { repoMode: mode === "EXISTING" ? "ASSESS_ONLY" : "STANDALONE", parameters: {} },
|
|
21
41
|
delivery: {
|
|
22
42
|
jenkinsRequired: true,
|
|
23
43
|
tiers: ["DEVELOP"],
|
|
24
44
|
gitopsRequired: false,
|
|
45
|
+
profileCode: "MCI_FLUX_V1",
|
|
46
|
+
profileVersion: "1.0.0",
|
|
47
|
+
structuralSpec: cloneValue(DEFAULT_GITOPS_STRUCTURAL_SPEC),
|
|
25
48
|
nacosRequired: false
|
|
26
49
|
}
|
|
27
50
|
};
|
|
28
51
|
}
|
|
52
|
+
function legacyWizardDraft(mode) {
|
|
53
|
+
return {
|
|
54
|
+
schemaVersion: 1,
|
|
55
|
+
mode,
|
|
56
|
+
application: { lifecycle: "EXPERIMENTAL", tier: "TIER_3" },
|
|
57
|
+
repository: { defaultBranch: "main" },
|
|
58
|
+
technology: { repoMode: "STANDALONE", parameters: {} },
|
|
59
|
+
delivery: cloneValue(emptyWizardDraft(mode).delivery)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
29
62
|
export function initialOnboardingTuiState() {
|
|
30
63
|
return {
|
|
31
64
|
phase: "LOADING",
|
|
@@ -174,20 +207,27 @@ function mergeCatalog(current, update) {
|
|
|
174
207
|
};
|
|
175
208
|
}
|
|
176
209
|
export function draftFromSession(session) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
210
|
+
if (!session.draft || typeof session.draft !== "object" || Array.isArray(session.draft)) {
|
|
211
|
+
return emptyWizardDraft(session.mode);
|
|
212
|
+
}
|
|
180
213
|
const incoming = cloneObject(session.draft);
|
|
181
|
-
|
|
214
|
+
const schemaVersion = incoming.schemaVersion === 2
|
|
215
|
+
? 2
|
|
216
|
+
: incoming.schemaVersion === 1 || Object.hasOwn(incoming, "repository") ? 1 : 2;
|
|
217
|
+
const base = schemaVersion === 2 ? emptyWizardDraft(session.mode) : legacyWizardDraft(session.mode);
|
|
218
|
+
const result = {
|
|
182
219
|
...base,
|
|
183
220
|
...incoming,
|
|
184
|
-
schemaVersion
|
|
221
|
+
schemaVersion,
|
|
185
222
|
mode: session.mode,
|
|
186
223
|
application: { ...asObject(base.application), ...asObject(incoming.application) },
|
|
187
|
-
repository: { ...asObject(base.repository), ...asObject(incoming.repository) },
|
|
188
224
|
technology: { ...asObject(base.technology), ...asObject(incoming.technology) },
|
|
189
225
|
delivery: { ...asObject(base.delivery), ...asObject(incoming.delivery) }
|
|
190
226
|
};
|
|
227
|
+
if (schemaVersion === 1) {
|
|
228
|
+
result.repository = { ...asObject(base.repository), ...asObject(incoming.repository) };
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
191
231
|
}
|
|
192
232
|
export function fieldsForState(state) {
|
|
193
233
|
const step = ONBOARDING_STEPS[state.stepIndex];
|
|
@@ -209,6 +249,39 @@ export function fieldsForState(state) {
|
|
|
209
249
|
{ path: "application.ownerUsername", label: "Owner username", kind: "text" }
|
|
210
250
|
];
|
|
211
251
|
if (step === "SOURCE_CONTROL") {
|
|
252
|
+
if (valueAtPath(state.draft, "schemaVersion") === 2) {
|
|
253
|
+
const strategy = stringAt(state.draft, "repositoryStrategy");
|
|
254
|
+
const repositoryChoices = state.catalog.repositories.map((item) => ({
|
|
255
|
+
value: text(item.repositoryKey),
|
|
256
|
+
label: [
|
|
257
|
+
text(item.fullName ?? item.name ?? item.repositoryKey),
|
|
258
|
+
text(item.connectionCode),
|
|
259
|
+
item.migrationRequired === true ? "MIGRATION_REQUIRED" : ""
|
|
260
|
+
].filter(Boolean).join(" · ")
|
|
261
|
+
})).filter((item) => item.value);
|
|
262
|
+
return [
|
|
263
|
+
{
|
|
264
|
+
path: "repositoryStrategy",
|
|
265
|
+
label: "Repository strategy",
|
|
266
|
+
kind: "select",
|
|
267
|
+
required: true,
|
|
268
|
+
choices: choices(mode === "EXISTING" ? ["ATTACH_EXISTING"] : ["CREATE_NEW", "ATTACH_EXISTING"])
|
|
269
|
+
},
|
|
270
|
+
...(strategy === "CREATE_NEW" ? [
|
|
271
|
+
{ path: "repositoryPlanRef.planNo", label: "Repository plan number", kind: "text", required: true, placeholder: "RP-1" },
|
|
272
|
+
{ path: "repositoryPlanRef.planHash", label: "Repository plan hash", kind: "text", required: true, placeholder: "64 lowercase hex" }
|
|
273
|
+
] : [
|
|
274
|
+
{
|
|
275
|
+
path: "repositoryRef.repositoryKey",
|
|
276
|
+
label: "Managed repository key",
|
|
277
|
+
kind: "choiceOrText",
|
|
278
|
+
required: true,
|
|
279
|
+
choices: includeCurrent(repositoryChoices, stringAt(state.draft, "repositoryRef.repositoryKey"))
|
|
280
|
+
},
|
|
281
|
+
{ path: "repositoryRef.expectedHead", label: "Expected repository HEAD", kind: "text", required: true, placeholder: "40-64 hex commit" }
|
|
282
|
+
])
|
|
283
|
+
];
|
|
284
|
+
}
|
|
212
285
|
const connectionChoices = state.catalog.connections.map((item) => ({
|
|
213
286
|
value: text(item.code),
|
|
214
287
|
label: [text(item.name), text(item.provider), text(item.health)].filter(Boolean).join(" · ")
|
|
@@ -258,7 +331,9 @@ export function fieldsForState(state) {
|
|
|
258
331
|
required: true,
|
|
259
332
|
choices: includeCurrent(stackChoices, stackCode)
|
|
260
333
|
},
|
|
261
|
-
|
|
334
|
+
...(valueAtPath(state.draft, "schemaVersion") === 2 ? [] : [
|
|
335
|
+
{ path: "technology.repoMode", label: "Repository mode", kind: "select", choices: choices(["STANDALONE", "ADD_MODULE"]) }
|
|
336
|
+
]),
|
|
262
337
|
...(mode === "NEW" ? [{
|
|
263
338
|
path: "technology.templateCode",
|
|
264
339
|
label: "Copier template",
|
|
@@ -274,6 +349,12 @@ export function fieldsForState(state) {
|
|
|
274
349
|
{ path: "delivery.jenkinsRequired", label: "Jenkins required", kind: "boolean" },
|
|
275
350
|
{ path: "delivery.tiers", label: "Jenkins tiers", kind: "tiers" },
|
|
276
351
|
{ path: "delivery.gitopsRequired", label: "GitOps required", kind: "boolean" },
|
|
352
|
+
...(valueAtPath(state.draft, "delivery.gitopsRequired") === true ? [
|
|
353
|
+
{ path: "delivery.targetKey", label: "GitOps deployment target key", kind: "text", required: true },
|
|
354
|
+
{ path: "delivery.profileCode", label: "GitOps layout profile", kind: "text", required: true, placeholder: "MCI_FLUX_V1" },
|
|
355
|
+
{ path: "delivery.profileVersion", label: "GitOps profile version", kind: "text", required: true, placeholder: "1.0.0" },
|
|
356
|
+
{ path: "delivery.structuralSpec", label: "GitOps structural spec JSON", kind: "json", required: true }
|
|
357
|
+
] : []),
|
|
277
358
|
{ path: "delivery.nacosRequired", label: "Nacos required", kind: "boolean" }
|
|
278
359
|
];
|
|
279
360
|
return [];
|
|
@@ -287,6 +368,33 @@ export function valueAtPath(root, path) {
|
|
|
287
368
|
}
|
|
288
369
|
return current;
|
|
289
370
|
}
|
|
371
|
+
export function gitopsExecutionDetails(events) {
|
|
372
|
+
for (const event of [...events].reverse()) {
|
|
373
|
+
const output = event.output;
|
|
374
|
+
if (!output || typeof output !== "object" || Array.isArray(output))
|
|
375
|
+
continue;
|
|
376
|
+
const value = output;
|
|
377
|
+
if (!value.gitopsPlanNumber && !value.gitopsRunNo && !value.gitopsState)
|
|
378
|
+
continue;
|
|
379
|
+
return {
|
|
380
|
+
planNumber: optionalText(value.gitopsPlanNumber),
|
|
381
|
+
planHash: optionalText(value.gitopsPlanHash),
|
|
382
|
+
files: textArray(value.gitopsPlanFiles),
|
|
383
|
+
warnings: textArray(value.gitopsWarnings),
|
|
384
|
+
sideEffects: textArray(value.gitopsSideEffects),
|
|
385
|
+
runNumber: optionalText(value.gitopsRunNo),
|
|
386
|
+
state: optionalText(value.gitopsState),
|
|
387
|
+
lastEventSequence: typeof value.gitopsLastEventSequence === "number"
|
|
388
|
+
? value.gitopsLastEventSequence : undefined,
|
|
389
|
+
bindingId: optionalText(value.gitopsBindingId),
|
|
390
|
+
targetKey: optionalText(value.gitopsTargetKey),
|
|
391
|
+
baseRevision: optionalText(value.gitopsBaseRevision),
|
|
392
|
+
resultRevision: optionalText(value.gitopsResultRevision),
|
|
393
|
+
reviewUrl: optionalText(value.gitopsReviewUrl)
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
290
398
|
export function stringAt(root, path) {
|
|
291
399
|
const value = valueAtPath(root, path);
|
|
292
400
|
return typeof value === "string" ? value : value == null ? "" : String(value);
|
|
@@ -329,6 +437,8 @@ export function parseFieldInput(field, input) {
|
|
|
329
437
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
330
438
|
throw new OnboardingError(`${field.label} must be a JSON object.`, "onboarding_input_invalid");
|
|
331
439
|
}
|
|
440
|
+
if (field.path === "delivery.structuralSpec")
|
|
441
|
+
validateGitopsStructuralSpec(parsed);
|
|
332
442
|
return parsed;
|
|
333
443
|
}
|
|
334
444
|
if (field.kind === "tiers") {
|
|
@@ -344,9 +454,26 @@ export function parseFieldInput(field, input) {
|
|
|
344
454
|
return trimmed || undefined;
|
|
345
455
|
}
|
|
346
456
|
export function assertCredentialFreeDraft(draft) {
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
457
|
+
const credentialScan = cloneObject(draft);
|
|
458
|
+
const delivery = asObject(credentialScan.delivery);
|
|
459
|
+
let structuralCredentialScan;
|
|
460
|
+
if (delivery.structuralSpec !== undefined) {
|
|
461
|
+
const structuralSpec = asObject(delivery.structuralSpec);
|
|
462
|
+
validateGitopsStructuralSpec(structuralSpec);
|
|
463
|
+
structuralCredentialScan = cloneObject(structuralSpec);
|
|
464
|
+
delete structuralCredentialScan.secretEnvironment;
|
|
465
|
+
delete delivery.structuralSpec;
|
|
466
|
+
credentialScan.delivery = delivery;
|
|
467
|
+
}
|
|
468
|
+
const parsed = CredentialFreeOnboardingValueSchema.safeParse(credentialScan);
|
|
469
|
+
const structuralParsed = structuralCredentialScan == null
|
|
470
|
+
? undefined
|
|
471
|
+
: CredentialFreeOnboardingValueSchema.safeParse({ controlledStructure: structuralCredentialScan });
|
|
472
|
+
if (!parsed.success || structuralParsed?.success === false) {
|
|
473
|
+
const issue = !parsed.success
|
|
474
|
+
? parsed.error.issues[0]
|
|
475
|
+
: structuralParsed && !structuralParsed.success ? structuralParsed.error.issues[0] : undefined;
|
|
476
|
+
throw new OnboardingError(`Draft contains credential-bearing input: ${issue?.path.join(".") || "unknown field"}.`, "onboarding_secret_not_allowed");
|
|
350
477
|
}
|
|
351
478
|
}
|
|
352
479
|
export function cycleFieldValue(state, field, direction) {
|
|
@@ -364,6 +491,102 @@ export function cycleFieldValue(state, field, direction) {
|
|
|
364
491
|
const selected = options[nextIndex].value;
|
|
365
492
|
return setDraftPath(state.draft, field.path, field.kind === "number" ? Number(selected) : selected);
|
|
366
493
|
}
|
|
494
|
+
export function enableGitopsDeliveryDefaults(draft) {
|
|
495
|
+
let result = setDraftPath(draft, "delivery.jenkinsRequired", true);
|
|
496
|
+
if (!stringAt(result, "delivery.profileCode")) {
|
|
497
|
+
result = setDraftPath(result, "delivery.profileCode", "MCI_FLUX_V1");
|
|
498
|
+
}
|
|
499
|
+
if (!stringAt(result, "delivery.profileVersion")) {
|
|
500
|
+
result = setDraftPath(result, "delivery.profileVersion", "1.0.0");
|
|
501
|
+
}
|
|
502
|
+
if (!valueAtPath(result, "delivery.structuralSpec")) {
|
|
503
|
+
result = setDraftPath(result, "delivery.structuralSpec", DEFAULT_GITOPS_STRUCTURAL_SPEC);
|
|
504
|
+
}
|
|
505
|
+
return result;
|
|
506
|
+
}
|
|
507
|
+
export function validateGitopsStructuralSpec(spec) {
|
|
508
|
+
for (const field of Object.keys(spec)) {
|
|
509
|
+
if (GITOPS_PLATFORM_MANAGED_FIELDS.has(field)) {
|
|
510
|
+
throw new OnboardingError(`delivery.structuralSpec.${field} is platform/Jenkins-managed and cannot be submitted.`, "GITOPS_MANAGED_FIELD_FORBIDDEN");
|
|
511
|
+
}
|
|
512
|
+
if (!GITOPS_STRUCTURAL_FIELDS.has(field)) {
|
|
513
|
+
throw new OnboardingError(`delivery.structuralSpec.${field} is not supported by MCI_FLUX_V1.`, "GITOPS_SPEC_INVALID");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
validateGitopsEnvironment(spec.environment);
|
|
517
|
+
validateGitopsSecretEnvironment(spec.secretEnvironment);
|
|
518
|
+
if (spec.advancedValues != null) {
|
|
519
|
+
const advancedValues = requireRecord(spec.advancedValues, "delivery.structuralSpec.advancedValues");
|
|
520
|
+
for (const field of Object.keys(advancedValues)) {
|
|
521
|
+
if (GITOPS_MANAGED_ADVANCED_VALUES.has(field)) {
|
|
522
|
+
throw new OnboardingError(`delivery.structuralSpec.advancedValues.${field} is managed by MCI_FLUX_V1.`, "GITOPS_MANAGED_FIELD_FORBIDDEN");
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
rejectRawGitopsSecret(spec, "delivery.structuralSpec");
|
|
527
|
+
}
|
|
528
|
+
function validateGitopsEnvironment(value) {
|
|
529
|
+
if (value == null)
|
|
530
|
+
return;
|
|
531
|
+
const environment = requireRecord(value, "delivery.structuralSpec.environment");
|
|
532
|
+
for (const [name, raw] of Object.entries(environment)) {
|
|
533
|
+
if (!GITOPS_ENVIRONMENT_NAME.test(name) || typeof raw !== "string") {
|
|
534
|
+
throw new OnboardingError(`delivery.structuralSpec.environment.${name} must have a valid environment name and string value.`, "GITOPS_SPEC_INVALID");
|
|
535
|
+
}
|
|
536
|
+
if (GITOPS_SENSITIVE_ENVIRONMENT_NAME.test(name.toUpperCase()) || looksLikeGitopsCredential(raw)) {
|
|
537
|
+
throw new OnboardingError(`delivery.structuralSpec.environment.${name} may contain plaintext credentials; use secretEnvironment SecretRef.`, "GITOPS_PLAINTEXT_CREDENTIAL_FORBIDDEN");
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function validateGitopsSecretEnvironment(value) {
|
|
542
|
+
if (value == null)
|
|
543
|
+
return;
|
|
544
|
+
if (!Array.isArray(value)) {
|
|
545
|
+
throw new OnboardingError("delivery.structuralSpec.secretEnvironment must be an array of SecretRef objects.", "GITOPS_SPEC_INVALID");
|
|
546
|
+
}
|
|
547
|
+
value.forEach((raw, index) => {
|
|
548
|
+
const item = requireRecord(raw, `delivery.structuralSpec.secretEnvironment[${index}]`);
|
|
549
|
+
const allowed = new Set(["name", "secretName", "secretKey", "optional"]);
|
|
550
|
+
const unknown = Object.keys(item).find((field) => !allowed.has(field));
|
|
551
|
+
if (unknown) {
|
|
552
|
+
throw new OnboardingError(`delivery.structuralSpec.secretEnvironment[${index}].${unknown} is not an allowed SecretRef field.`, "GITOPS_RAW_SECRET_FORBIDDEN");
|
|
553
|
+
}
|
|
554
|
+
if (!nonEmptyString(item.name) || !GITOPS_ENVIRONMENT_NAME.test(item.name)
|
|
555
|
+
|| !nonEmptyString(item.secretName) || !nonEmptyString(item.secretKey)
|
|
556
|
+
|| (item.optional != null && typeof item.optional !== "boolean")) {
|
|
557
|
+
throw new OnboardingError(`delivery.structuralSpec.secretEnvironment[${index}] requires name, secretName, secretKey, and optional boolean optional.`, "GITOPS_SPEC_INVALID");
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
function rejectRawGitopsSecret(value, path) {
|
|
562
|
+
if (Array.isArray(value)) {
|
|
563
|
+
value.forEach((item, index) => rejectRawGitopsSecret(item, `${path}[${index}]`));
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (!value || typeof value !== "object")
|
|
567
|
+
return;
|
|
568
|
+
const object = value;
|
|
569
|
+
if (String(object.kind ?? "").toLowerCase() === "secret"
|
|
570
|
+
&& (Object.hasOwn(object, "data") || Object.hasOwn(object, "stringData"))) {
|
|
571
|
+
throw new OnboardingError(`${path} cannot contain Kubernetes Secret.data/stringData; use an existing SecretRef.`, "GITOPS_RAW_SECRET_FORBIDDEN");
|
|
572
|
+
}
|
|
573
|
+
Object.entries(object).forEach(([key, item]) => rejectRawGitopsSecret(item, `${path}.${key}`));
|
|
574
|
+
}
|
|
575
|
+
function requireRecord(value, path) {
|
|
576
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
577
|
+
throw new OnboardingError(`${path} must be a JSON object.`, "GITOPS_SPEC_INVALID");
|
|
578
|
+
}
|
|
579
|
+
return value;
|
|
580
|
+
}
|
|
581
|
+
function nonEmptyString(value) {
|
|
582
|
+
return typeof value === "string" && Boolean(value.trim());
|
|
583
|
+
}
|
|
584
|
+
function looksLikeGitopsCredential(value) {
|
|
585
|
+
return /:\/\/[^/\s:@]+:[^@\s/]+@/u.test(value)
|
|
586
|
+
|| /(password|passwd|secret|token|api[_-]?key)\s*[:=]/iu.test(value)
|
|
587
|
+
|| value.includes("-----BEGIN PRIVATE KEY-----")
|
|
588
|
+
|| value.includes("-----BEGIN RSA PRIVATE KEY-----");
|
|
589
|
+
}
|
|
367
590
|
export function applyRepositorySelection(draft, repositories, externalId) {
|
|
368
591
|
let result = setDraftPath(draft, "repository.externalId", externalId || undefined);
|
|
369
592
|
const repository = repositories.find((item) => text(item.externalId ?? item.id) === externalId);
|
|
@@ -374,6 +597,22 @@ export function applyRepositorySelection(draft, repositories, externalId) {
|
|
|
374
597
|
result = setDraftPath(result, "repository.namespace", undefined);
|
|
375
598
|
return setDraftPath(result, "repository.defaultBranch", text(repository.defaultBranch) || "main");
|
|
376
599
|
}
|
|
600
|
+
export function applyRepositoryStrategy(draft, mode, strategy) {
|
|
601
|
+
const effective = mode === "EXISTING" ? "ATTACH_EXISTING" : strategy;
|
|
602
|
+
let result = setDraftPath(draft, "repositoryStrategy", effective);
|
|
603
|
+
result = setDraftPath(result, "projectStrategy", mode === "EXISTING" ? "ASSESS_ONLY" : effective === "CREATE_NEW" ? "BOOTSTRAP_TEMPLATE" : "ADD_MODULE_CHANGE");
|
|
604
|
+
result = setDraftPath(result, "technology.repoMode", mode === "EXISTING" ? "ASSESS_ONLY" : effective === "CREATE_NEW" ? "STANDALONE" : "ADD_MODULE");
|
|
605
|
+
result = setDraftPath(result, effective === "CREATE_NEW" ? "repositoryRef" : "repositoryPlanRef", undefined);
|
|
606
|
+
return result;
|
|
607
|
+
}
|
|
608
|
+
export function applyManagedRepositorySelection(draft, repositories, repositoryKey) {
|
|
609
|
+
let result = setDraftPath(draft, "repositoryRef.repositoryKey", repositoryKey || undefined);
|
|
610
|
+
const repository = repositories.find((item) => text(item.repositoryKey) === repositoryKey);
|
|
611
|
+
if (!repository)
|
|
612
|
+
return result;
|
|
613
|
+
const observedRevision = text(repository.observedRevision ?? repository.lastObservedRevision);
|
|
614
|
+
return setDraftPath(result, "repositoryRef.expectedHead", /^[0-9a-fA-F]{40,64}$/u.test(observedRevision) ? observedRevision : undefined);
|
|
615
|
+
}
|
|
377
616
|
export function applyTemplateDefaults(draft, template) {
|
|
378
617
|
const schema = asObject(template?.parameterSchema);
|
|
379
618
|
const defaults = defaultsFromSchema(schema);
|
|
@@ -403,15 +642,60 @@ export function validateWizardBeforeStep(state, targetStepIndex) {
|
|
|
403
642
|
errors.push("application.teamId must be a positive integer.");
|
|
404
643
|
if (!positiveIntegerAt(state.draft, "application.systemId"))
|
|
405
644
|
errors.push("application.systemId must be a positive integer.");
|
|
406
|
-
|
|
645
|
+
const schemaVersion = valueAtPath(state.draft, "schemaVersion");
|
|
407
646
|
const addModule = mode === "NEW" && stringAt(state.draft, "technology.repoMode") === "ADD_MODULE";
|
|
408
|
-
if (
|
|
409
|
-
|
|
410
|
-
|
|
647
|
+
if (schemaVersion === 2) {
|
|
648
|
+
if (Object.hasOwn(state.draft, "repository")) {
|
|
649
|
+
errors.push("schemaVersion=2 forbids legacy repository/provider fields; use repositoryPlanRef or repositoryRef.");
|
|
650
|
+
}
|
|
651
|
+
const strategy = stringAt(state.draft, "repositoryStrategy");
|
|
652
|
+
if (mode === "EXISTING" && strategy !== "ATTACH_EXISTING") {
|
|
653
|
+
errors.push("repositoryStrategy must be ATTACH_EXISTING for EXISTING onboarding.");
|
|
654
|
+
}
|
|
655
|
+
else if (!["CREATE_NEW", "ATTACH_EXISTING"].includes(strategy)) {
|
|
656
|
+
errors.push("repositoryStrategy must be CREATE_NEW or ATTACH_EXISTING.");
|
|
657
|
+
}
|
|
658
|
+
const expectedProject = mode === "EXISTING"
|
|
659
|
+
? "ASSESS_ONLY"
|
|
660
|
+
: strategy === "CREATE_NEW" ? "BOOTSTRAP_TEMPLATE" : "ADD_MODULE_CHANGE";
|
|
661
|
+
if (stringAt(state.draft, "projectStrategy") !== expectedProject) {
|
|
662
|
+
errors.push(`projectStrategy must be ${expectedProject}.`);
|
|
663
|
+
}
|
|
664
|
+
const expectedRepoMode = mode === "EXISTING"
|
|
665
|
+
? "ASSESS_ONLY"
|
|
666
|
+
: strategy === "CREATE_NEW" ? "STANDALONE" : "ADD_MODULE";
|
|
667
|
+
if (stringAt(state.draft, "technology.repoMode") !== expectedRepoMode) {
|
|
668
|
+
errors.push(`technology.repoMode must be ${expectedRepoMode}.`);
|
|
669
|
+
}
|
|
670
|
+
if (strategy === "CREATE_NEW") {
|
|
671
|
+
requireText("repositoryPlanRef.planNo");
|
|
672
|
+
if (!/^[0-9a-f]{64}$/u.test(stringAt(state.draft, "repositoryPlanRef.planHash"))) {
|
|
673
|
+
errors.push("repositoryPlanRef.planHash must be the exact 64-lowercase-hex server plan hash.");
|
|
674
|
+
}
|
|
675
|
+
if (valueAtPath(state.draft, "repositoryRef") != null) {
|
|
676
|
+
errors.push("CREATE_NEW cannot include repositoryRef.");
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
else if (strategy === "ATTACH_EXISTING") {
|
|
680
|
+
requireText("repositoryRef.repositoryKey");
|
|
681
|
+
if (!/^[0-9a-fA-F]{40,64}$/u.test(stringAt(state.draft, "repositoryRef.expectedHead"))) {
|
|
682
|
+
errors.push("repositoryRef.expectedHead must be an immutable 40-64 hex revision.");
|
|
683
|
+
}
|
|
684
|
+
if (valueAtPath(state.draft, "repositoryPlanRef") != null) {
|
|
685
|
+
errors.push("ATTACH_EXISTING cannot include repositoryPlanRef.");
|
|
686
|
+
}
|
|
687
|
+
}
|
|
411
688
|
}
|
|
412
689
|
else {
|
|
413
|
-
requireText("repository.
|
|
414
|
-
|
|
690
|
+
requireText("repository.connectionCode");
|
|
691
|
+
if (mode === "NEW" && !addModule) {
|
|
692
|
+
requireText("repository.namespace");
|
|
693
|
+
requireText("repository.name");
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
requireText("repository.externalId");
|
|
697
|
+
requireText("repository.expectedHead");
|
|
698
|
+
}
|
|
415
699
|
}
|
|
416
700
|
requireText("technology.stackCode");
|
|
417
701
|
if (mode === "NEW") {
|
|
@@ -420,6 +704,52 @@ export function validateWizardBeforeStep(state, targetStepIndex) {
|
|
|
420
704
|
if (selected)
|
|
421
705
|
errors.push(...validateTemplateParameters(state.draft, state.catalog.templates));
|
|
422
706
|
}
|
|
707
|
+
if (targetStepIndex >= ONBOARDING_STEPS.indexOf("REVIEW")
|
|
708
|
+
&& valueAtPath(state.draft, "delivery.gitopsRequired") === true) {
|
|
709
|
+
if (valueAtPath(state.draft, "delivery.jenkinsRequired") !== true) {
|
|
710
|
+
errors.push("delivery.jenkinsRequired must be enabled for GitOps image delivery evidence.");
|
|
711
|
+
}
|
|
712
|
+
const tiers = valueAtPath(state.draft, "delivery.tiers");
|
|
713
|
+
if (!Array.isArray(tiers) || tiers.length === 0) {
|
|
714
|
+
errors.push("delivery.tiers requires at least one Jenkins tier for GitOps onboarding.");
|
|
715
|
+
}
|
|
716
|
+
requireText("delivery.targetKey");
|
|
717
|
+
requireText("delivery.profileCode");
|
|
718
|
+
requireText("delivery.profileVersion");
|
|
719
|
+
const structuralSpec = valueAtPath(state.draft, "delivery.structuralSpec");
|
|
720
|
+
if (!structuralSpec || typeof structuralSpec !== "object" || Array.isArray(structuralSpec)
|
|
721
|
+
|| Object.keys(structuralSpec).length === 0) {
|
|
722
|
+
errors.push("delivery.structuralSpec must be a non-empty controlled JSON object.");
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
try {
|
|
726
|
+
validateGitopsStructuralSpec(structuralSpec);
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (targetStepIndex >= ONBOARDING_STEPS.indexOf("REVIEW") && schemaVersion !== 2) {
|
|
734
|
+
errors.push("Legacy schemaVersion=1 TUI sessions are recovery/read-only and cannot preflight or apply. "
|
|
735
|
+
+ "Start a v2 TUI session or use `numa app onboard --spec <v2.json> --plan`.");
|
|
736
|
+
}
|
|
737
|
+
else if (targetStepIndex >= ONBOARDING_STEPS.indexOf("REVIEW")) {
|
|
738
|
+
const schemaDraft = cloneObject(state.draft);
|
|
739
|
+
const delivery = asObject(schemaDraft.delivery);
|
|
740
|
+
const structuralSpec = asObject(delivery.structuralSpec);
|
|
741
|
+
if (Object.hasOwn(structuralSpec, "secretEnvironment")) {
|
|
742
|
+
delete structuralSpec.secretEnvironment;
|
|
743
|
+
delivery.structuralSpec = structuralSpec;
|
|
744
|
+
schemaDraft.delivery = delivery;
|
|
745
|
+
}
|
|
746
|
+
const parsed = OnboardingSpecSchema.safeParse(schemaDraft);
|
|
747
|
+
if (!parsed.success) {
|
|
748
|
+
for (const issue of parsed.error.issues) {
|
|
749
|
+
errors.push(`${issue.path.join(".") || "spec"}: ${issue.message}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
423
753
|
return [...new Set(errors)];
|
|
424
754
|
}
|
|
425
755
|
function validateSchemaObject(schema, value, path) {
|
|
@@ -513,6 +843,18 @@ function asObject(value) {
|
|
|
513
843
|
function text(value) {
|
|
514
844
|
return typeof value === "string" ? value : typeof value === "number" ? String(value) : "";
|
|
515
845
|
}
|
|
846
|
+
function optionalText(value) {
|
|
847
|
+
const result = text(value).trim();
|
|
848
|
+
return result || undefined;
|
|
849
|
+
}
|
|
850
|
+
function textArray(value) {
|
|
851
|
+
return Array.isArray(value)
|
|
852
|
+
? value.flatMap((item) => {
|
|
853
|
+
const result = optionalText(item);
|
|
854
|
+
return result == null ? [] : [result];
|
|
855
|
+
})
|
|
856
|
+
: [];
|
|
857
|
+
}
|
|
516
858
|
function choices(values) {
|
|
517
859
|
return values.map((value) => ({ value, label: value }));
|
|
518
860
|
}
|