iec-builder 0.1.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.
- package/.claude/settings.local.json +111 -0
- package/.iec.yaml +5 -0
- package/CLAUDE.md +174 -0
- package/Dockerfile +34 -0
- package/README.md +84 -0
- package/catalog-info.yaml +11 -0
- package/dist/config/env.d.ts +219 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +89 -0
- package/dist/config/env.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +148 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +43 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +217 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/org-access.d.ts +28 -0
- package/dist/middleware/org-access.d.ts.map +1 -0
- package/dist/middleware/org-access.js +102 -0
- package/dist/middleware/org-access.js.map +1 -0
- package/dist/models/types.d.ts +254 -0
- package/dist/models/types.d.ts.map +1 -0
- package/dist/models/types.js +2 -0
- package/dist/models/types.js.map +1 -0
- package/dist/routes/ai.d.ts +2 -0
- package/dist/routes/ai.d.ts.map +1 -0
- package/dist/routes/ai.js +77 -0
- package/dist/routes/ai.js.map +1 -0
- package/dist/routes/audit.d.ts +2 -0
- package/dist/routes/audit.d.ts.map +1 -0
- package/dist/routes/audit.js +102 -0
- package/dist/routes/audit.js.map +1 -0
- package/dist/routes/builds.d.ts +2 -0
- package/dist/routes/builds.d.ts.map +1 -0
- package/dist/routes/builds.js +262 -0
- package/dist/routes/builds.js.map +1 -0
- package/dist/routes/cluster.d.ts +2 -0
- package/dist/routes/cluster.d.ts.map +1 -0
- package/dist/routes/cluster.js +181 -0
- package/dist/routes/cluster.js.map +1 -0
- package/dist/routes/config.d.ts +2 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +291 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/databases.d.ts +2 -0
- package/dist/routes/databases.d.ts.map +1 -0
- package/dist/routes/databases.js +161 -0
- package/dist/routes/databases.js.map +1 -0
- package/dist/routes/db-whitelist.d.ts +2 -0
- package/dist/routes/db-whitelist.d.ts.map +1 -0
- package/dist/routes/db-whitelist.js +148 -0
- package/dist/routes/db-whitelist.js.map +1 -0
- package/dist/routes/domains.d.ts +2 -0
- package/dist/routes/domains.d.ts.map +1 -0
- package/dist/routes/domains.js +449 -0
- package/dist/routes/domains.js.map +1 -0
- package/dist/routes/oauth.d.ts +2 -0
- package/dist/routes/oauth.d.ts.map +1 -0
- package/dist/routes/oauth.js +180 -0
- package/dist/routes/oauth.js.map +1 -0
- package/dist/routes/observability.d.ts +2 -0
- package/dist/routes/observability.d.ts.map +1 -0
- package/dist/routes/observability.js +167 -0
- package/dist/routes/observability.js.map +1 -0
- package/dist/routes/orgs.d.ts +2 -0
- package/dist/routes/orgs.d.ts.map +1 -0
- package/dist/routes/orgs.js +270 -0
- package/dist/routes/orgs.js.map +1 -0
- package/dist/routes/platform.d.ts +2 -0
- package/dist/routes/platform.d.ts.map +1 -0
- package/dist/routes/platform.js +107 -0
- package/dist/routes/platform.js.map +1 -0
- package/dist/routes/push.d.ts +2 -0
- package/dist/routes/push.d.ts.map +1 -0
- package/dist/routes/push.js +233 -0
- package/dist/routes/push.js.map +1 -0
- package/dist/routes/rotation.d.ts +3 -0
- package/dist/routes/rotation.d.ts.map +1 -0
- package/dist/routes/rotation.js +154 -0
- package/dist/routes/rotation.js.map +1 -0
- package/dist/routes/services.d.ts +2 -0
- package/dist/routes/services.d.ts.map +1 -0
- package/dist/routes/services.js +246 -0
- package/dist/routes/services.js.map +1 -0
- package/dist/routes/storage.d.ts +2 -0
- package/dist/routes/storage.d.ts.map +1 -0
- package/dist/routes/storage.js +118 -0
- package/dist/routes/storage.js.map +1 -0
- package/dist/routes/users.d.ts +2 -0
- package/dist/routes/users.d.ts.map +1 -0
- package/dist/routes/users.js +183 -0
- package/dist/routes/users.js.map +1 -0
- package/dist/routes/versions.d.ts +2 -0
- package/dist/routes/versions.d.ts.map +1 -0
- package/dist/routes/versions.js +195 -0
- package/dist/routes/versions.js.map +1 -0
- package/dist/routes/webhooks.d.ts +2 -0
- package/dist/routes/webhooks.d.ts.map +1 -0
- package/dist/routes/webhooks.js +334 -0
- package/dist/routes/webhooks.js.map +1 -0
- package/dist/services/__tests__/deploy-pipeline.integration.test.d.ts +2 -0
- package/dist/services/__tests__/deploy-pipeline.integration.test.d.ts.map +1 -0
- package/dist/services/__tests__/deploy-pipeline.integration.test.js +482 -0
- package/dist/services/__tests__/deploy-pipeline.integration.test.js.map +1 -0
- package/dist/services/bio-client.d.ts +68 -0
- package/dist/services/bio-client.d.ts.map +1 -0
- package/dist/services/bio-client.js +110 -0
- package/dist/services/bio-client.js.map +1 -0
- package/dist/services/build-queue.d.ts +7 -0
- package/dist/services/build-queue.d.ts.map +1 -0
- package/dist/services/build-queue.js +114 -0
- package/dist/services/build-queue.js.map +1 -0
- package/dist/services/builder.d.ts +7 -0
- package/dist/services/builder.d.ts.map +1 -0
- package/dist/services/builder.js +1384 -0
- package/dist/services/builder.js.map +1 -0
- package/dist/services/catalog.d.ts +177 -0
- package/dist/services/catalog.d.ts.map +1 -0
- package/dist/services/catalog.js +805 -0
- package/dist/services/catalog.js.map +1 -0
- package/dist/services/catalog.test.d.ts +2 -0
- package/dist/services/catalog.test.d.ts.map +1 -0
- package/dist/services/catalog.test.js +467 -0
- package/dist/services/catalog.test.js.map +1 -0
- package/dist/services/cloudflare.d.ts +43 -0
- package/dist/services/cloudflare.d.ts.map +1 -0
- package/dist/services/cloudflare.js +182 -0
- package/dist/services/cloudflare.js.map +1 -0
- package/dist/services/config-validator.d.ts +28 -0
- package/dist/services/config-validator.d.ts.map +1 -0
- package/dist/services/config-validator.js +68 -0
- package/dist/services/config-validator.js.map +1 -0
- package/dist/services/config-validator.test.d.ts +2 -0
- package/dist/services/config-validator.test.d.ts.map +1 -0
- package/dist/services/config-validator.test.js +151 -0
- package/dist/services/config-validator.test.js.map +1 -0
- package/dist/services/crypto.d.ts +19 -0
- package/dist/services/crypto.d.ts.map +1 -0
- package/dist/services/crypto.js +63 -0
- package/dist/services/crypto.js.map +1 -0
- package/dist/services/database.d.ts +26 -0
- package/dist/services/database.d.ts.map +1 -0
- package/dist/services/database.js +100 -0
- package/dist/services/database.js.map +1 -0
- package/dist/services/db-credential-manager.d.ts +73 -0
- package/dist/services/db-credential-manager.d.ts.map +1 -0
- package/dist/services/db-credential-manager.js +342 -0
- package/dist/services/db-credential-manager.js.map +1 -0
- package/dist/services/db-provisioner.d.ts +57 -0
- package/dist/services/db-provisioner.d.ts.map +1 -0
- package/dist/services/db-provisioner.js +400 -0
- package/dist/services/db-provisioner.js.map +1 -0
- package/dist/services/db-provisioner.test.d.ts +2 -0
- package/dist/services/db-provisioner.test.d.ts.map +1 -0
- package/dist/services/db-provisioner.test.js +141 -0
- package/dist/services/db-provisioner.test.js.map +1 -0
- package/dist/services/db-whitelist.d.ts +58 -0
- package/dist/services/db-whitelist.d.ts.map +1 -0
- package/dist/services/db-whitelist.js +379 -0
- package/dist/services/db-whitelist.js.map +1 -0
- package/dist/services/dependency-resolver.d.ts +58 -0
- package/dist/services/dependency-resolver.d.ts.map +1 -0
- package/dist/services/dependency-resolver.js +180 -0
- package/dist/services/dependency-resolver.js.map +1 -0
- package/dist/services/dependency-resolver.test.d.ts +2 -0
- package/dist/services/dependency-resolver.test.d.ts.map +1 -0
- package/dist/services/dependency-resolver.test.js +195 -0
- package/dist/services/dependency-resolver.test.js.map +1 -0
- package/dist/services/deploy-gate.d.ts +19 -0
- package/dist/services/deploy-gate.d.ts.map +1 -0
- package/dist/services/deploy-gate.js +56 -0
- package/dist/services/deploy-gate.js.map +1 -0
- package/dist/services/deploy-gate.test.d.ts +2 -0
- package/dist/services/deploy-gate.test.d.ts.map +1 -0
- package/dist/services/deploy-gate.test.js +199 -0
- package/dist/services/deploy-gate.test.js.map +1 -0
- package/dist/services/dockerfile-generator.d.ts +31 -0
- package/dist/services/dockerfile-generator.d.ts.map +1 -0
- package/dist/services/dockerfile-generator.js +544 -0
- package/dist/services/dockerfile-generator.js.map +1 -0
- package/dist/services/dockerfile-generator.test.d.ts +2 -0
- package/dist/services/dockerfile-generator.test.d.ts.map +1 -0
- package/dist/services/dockerfile-generator.test.js +144 -0
- package/dist/services/dockerfile-generator.test.js.map +1 -0
- package/dist/services/forgejo.d.ts +58 -0
- package/dist/services/forgejo.d.ts.map +1 -0
- package/dist/services/forgejo.js +131 -0
- package/dist/services/forgejo.js.map +1 -0
- package/dist/services/koko.d.ts +153 -0
- package/dist/services/koko.d.ts.map +1 -0
- package/dist/services/koko.js +260 -0
- package/dist/services/koko.js.map +1 -0
- package/dist/services/kubernetes.d.ts +16 -0
- package/dist/services/kubernetes.d.ts.map +1 -0
- package/dist/services/kubernetes.js +102 -0
- package/dist/services/kubernetes.js.map +1 -0
- package/dist/services/oauth-provisioner.d.ts +30 -0
- package/dist/services/oauth-provisioner.d.ts.map +1 -0
- package/dist/services/oauth-provisioner.js +182 -0
- package/dist/services/oauth-provisioner.js.map +1 -0
- package/dist/services/oauth-provisioner.test.d.ts +2 -0
- package/dist/services/oauth-provisioner.test.d.ts.map +1 -0
- package/dist/services/oauth-provisioner.test.js +349 -0
- package/dist/services/oauth-provisioner.test.js.map +1 -0
- package/dist/services/pod-diagnostics.d.ts +11 -0
- package/dist/services/pod-diagnostics.d.ts.map +1 -0
- package/dist/services/pod-diagnostics.js +201 -0
- package/dist/services/pod-diagnostics.js.map +1 -0
- package/dist/services/rotation-scheduler.d.ts +2 -0
- package/dist/services/rotation-scheduler.d.ts.map +1 -0
- package/dist/services/rotation-scheduler.js +215 -0
- package/dist/services/rotation-scheduler.js.map +1 -0
- package/dist/services/storage-credential-manager.d.ts +43 -0
- package/dist/services/storage-credential-manager.d.ts.map +1 -0
- package/dist/services/storage-credential-manager.js +159 -0
- package/dist/services/storage-credential-manager.js.map +1 -0
- package/dist/services/storage-provisioner.d.ts +32 -0
- package/dist/services/storage-provisioner.d.ts.map +1 -0
- package/dist/services/storage-provisioner.js +136 -0
- package/dist/services/storage-provisioner.js.map +1 -0
- package/dist/services/storage.d.ts +65 -0
- package/dist/services/storage.d.ts.map +1 -0
- package/dist/services/storage.js +204 -0
- package/dist/services/storage.js.map +1 -0
- package/dist/services/troubleshooter.d.ts +22 -0
- package/dist/services/troubleshooter.d.ts.map +1 -0
- package/dist/services/troubleshooter.js +168 -0
- package/dist/services/troubleshooter.js.map +1 -0
- package/dist/services/vault-client.d.ts +114 -0
- package/dist/services/vault-client.d.ts.map +1 -0
- package/dist/services/vault-client.js +411 -0
- package/dist/services/vault-client.js.map +1 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +6 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/response.d.ts +13 -0
- package/dist/utils/response.d.ts.map +1 -0
- package/dist/utils/response.js +12 -0
- package/dist/utils/response.js.map +1 -0
- package/docs/registry-migration.md +301 -0
- package/docs/registry-quickstart.md +169 -0
- package/ecosystem.config.cjs +14 -0
- package/findings.md +168 -0
- package/helm/default-service/Chart.yaml +6 -0
- package/helm/default-service/templates/deployment.yaml +97 -0
- package/helm/default-service/templates/ingress.yaml +43 -0
- package/helm/default-service/templates/service.yaml +17 -0
- package/helm/default-service/values.yaml +82 -0
- package/helm/services/iec-builder/Chart.yaml +6 -0
- package/helm/services/iec-builder/templates/_helpers.tpl +61 -0
- package/helm/services/iec-builder/templates/deployment.yaml +73 -0
- package/helm/services/iec-builder/templates/service.yaml +15 -0
- package/helm/services/iec-builder/templates/serviceaccount.yaml +12 -0
- package/helm/services/iec-builder/values.yaml +56 -0
- package/helm/vault-values.yaml +127 -0
- package/package.json +45 -0
- package/progress.md +156 -0
- package/scripts/.vault-init-keys.json +23 -0
- package/scripts/backfill-ownership.ts +113 -0
- package/scripts/finalize-mongo-auth.sh +212 -0
- package/scripts/setup-ipset.sh +107 -0
- package/scripts/setup-mongo-auth.sh +163 -0
- package/scripts/setup-neo4j-auth.sh +62 -0
- package/scripts/setup-redis-auth.sh +55 -0
- package/scripts/setup-registry-secret.sh +71 -0
- package/scripts/setup-vault.sh +308 -0
- package/src/config/env.ts +117 -0
- package/src/index.ts +153 -0
- package/src/middleware/auth.ts +294 -0
- package/src/middleware/org-access.ts +126 -0
- package/src/models/types.ts +288 -0
- package/src/routes/ai.ts +115 -0
- package/src/routes/audit.ts +121 -0
- package/src/routes/builds.ts +320 -0
- package/src/routes/cluster.ts +235 -0
- package/src/routes/config.ts +369 -0
- package/src/routes/databases.ts +201 -0
- package/src/routes/db-whitelist.ts +204 -0
- package/src/routes/domains.ts +547 -0
- package/src/routes/oauth.ts +195 -0
- package/src/routes/observability.ts +205 -0
- package/src/routes/orgs.ts +330 -0
- package/src/routes/platform.ts +134 -0
- package/src/routes/rotation.ts +191 -0
- package/src/routes/services.ts +290 -0
- package/src/routes/storage.ts +153 -0
- package/src/routes/users.ts +235 -0
- package/src/routes/webhooks.ts +384 -0
- package/src/services/__tests__/catalog-storage.test.ts +186 -0
- package/src/services/__tests__/deploy-pipeline.integration.test.ts +624 -0
- package/src/services/__tests__/pod-diagnostics.test.ts +332 -0
- package/src/services/__tests__/storage-credential-manager.test.ts +129 -0
- package/src/services/__tests__/storage-provisioner.test.ts +166 -0
- package/src/services/__tests__/troubleshooter.test.ts +329 -0
- package/src/services/bio-client.ts +189 -0
- package/src/services/build-queue.ts +137 -0
- package/src/services/builder.ts +1800 -0
- package/src/services/catalog.test.ts +1389 -0
- package/src/services/catalog.ts +1187 -0
- package/src/services/cloudflare.ts +259 -0
- package/src/services/config-validator.test.ts +190 -0
- package/src/services/config-validator.ts +108 -0
- package/src/services/crypto.ts +78 -0
- package/src/services/database.ts +122 -0
- package/src/services/db-credential-manager.test.ts +101 -0
- package/src/services/db-credential-manager.ts +447 -0
- package/src/services/db-provisioner.test.ts +602 -0
- package/src/services/db-provisioner.ts +589 -0
- package/src/services/db-whitelist.test.ts +671 -0
- package/src/services/db-whitelist.ts +496 -0
- package/src/services/dependency-resolver.test.ts +677 -0
- package/src/services/dependency-resolver.ts +319 -0
- package/src/services/deploy-gate.test.ts +247 -0
- package/src/services/deploy-gate.ts +75 -0
- package/src/services/dockerfile-generator.test.ts +401 -0
- package/src/services/dockerfile-generator.ts +606 -0
- package/src/services/forgejo.ts +212 -0
- package/src/services/koko.ts +492 -0
- package/src/services/kubernetes.ts +141 -0
- package/src/services/oauth-provisioner.test.ts +477 -0
- package/src/services/oauth-provisioner.ts +286 -0
- package/src/services/pod-diagnostics.ts +261 -0
- package/src/services/rotation-scheduler.ts +293 -0
- package/src/services/storage-credential-manager.ts +223 -0
- package/src/services/storage-provisioner.ts +216 -0
- package/src/services/storage.ts +274 -0
- package/src/services/troubleshooter.ts +208 -0
- package/src/services/vault-client.test.ts +272 -0
- package/src/services/vault-client.ts +587 -0
- package/src/utils/logger.ts +6 -0
- package/src/utils/response.ts +23 -0
- package/task_plan.md +171 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +19 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { getBioClient } from './bio-client.js'
|
|
2
|
+
import { getServicesCollection } from './database.js'
|
|
3
|
+
import { encrypt, decrypt } from './crypto.js'
|
|
4
|
+
import { logger } from '../utils/logger.js'
|
|
5
|
+
import type { CustomDomainRecord, OAuthCredentialRecord } from '../models/types.js'
|
|
6
|
+
import type { AuthMode } from './catalog.js'
|
|
7
|
+
|
|
8
|
+
export interface OAuthCredentials {
|
|
9
|
+
readonly clientId: string
|
|
10
|
+
readonly clientSecret: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface BioClientData {
|
|
14
|
+
clientId: string
|
|
15
|
+
clientSecret?: string
|
|
16
|
+
redirectUris?: string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build the OAuth client ID from service name and environment.
|
|
21
|
+
*/
|
|
22
|
+
export function buildOAuthClientId(serviceName: string, environment: string): string {
|
|
23
|
+
return `${serviceName}-${environment}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DOMAIN_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build the set of redirect URIs for a service based on naming convention.
|
|
30
|
+
* Includes the platform hostname plus any verified custom domains for the environment.
|
|
31
|
+
*/
|
|
32
|
+
export function buildRedirectUris(
|
|
33
|
+
serviceName: string,
|
|
34
|
+
environment: string,
|
|
35
|
+
customDomains?: ReadonlyArray<CustomDomainRecord>
|
|
36
|
+
): ReadonlyArray<string> {
|
|
37
|
+
const hostname = environment === 'prod'
|
|
38
|
+
? `${serviceName}.tawa.insureco.io`
|
|
39
|
+
: `${serviceName}.${environment}.tawa.insureco.io`
|
|
40
|
+
|
|
41
|
+
const platformUri = `https://${hostname}/api/auth/callback`
|
|
42
|
+
|
|
43
|
+
const customUris = (customDomains ?? [])
|
|
44
|
+
.filter(d => d.dnsVerified && d.environment === environment && DOMAIN_RE.test(d.domain))
|
|
45
|
+
.map(d => `https://${d.domain}/api/auth/callback`)
|
|
46
|
+
|
|
47
|
+
return [platformUri, ...customUris]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read stored OAuth credentials from the service's MongoDB document.
|
|
52
|
+
*/
|
|
53
|
+
function readStoredCredentials(
|
|
54
|
+
storedCreds: ReadonlyArray<OAuthCredentialRecord> | undefined,
|
|
55
|
+
clientId: string,
|
|
56
|
+
environment: string
|
|
57
|
+
): OAuthCredentials | null {
|
|
58
|
+
const record = (storedCreds ?? []).find(
|
|
59
|
+
c => c.clientId === clientId && c.environment === environment
|
|
60
|
+
)
|
|
61
|
+
if (!record) return null
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const clientSecret = decrypt(record.clientSecretEncrypted)
|
|
65
|
+
return { clientId: record.clientId, clientSecret }
|
|
66
|
+
} catch (error) {
|
|
67
|
+
logger.warn({ clientId, error }, 'Failed to decrypt stored OAuth credentials')
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Persist OAuth credentials (encrypted) to the service's MongoDB document.
|
|
74
|
+
*/
|
|
75
|
+
async function storeCredentials(
|
|
76
|
+
serviceId: string,
|
|
77
|
+
credentials: OAuthCredentials,
|
|
78
|
+
environment: string,
|
|
79
|
+
existingCreds: ReadonlyArray<OAuthCredentialRecord> | undefined
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
const record: OAuthCredentialRecord = {
|
|
82
|
+
clientId: credentials.clientId,
|
|
83
|
+
clientSecretEncrypted: encrypt(credentials.clientSecret),
|
|
84
|
+
environment: environment as OAuthCredentialRecord['environment'],
|
|
85
|
+
provisionedAt: new Date().toISOString(),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Replace existing record for this environment, or append
|
|
89
|
+
const filtered = (existingCreds ?? []).filter(
|
|
90
|
+
c => !(c.clientId === credentials.clientId && c.environment === environment)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
await getServicesCollection().updateOne(
|
|
94
|
+
{ id: serviceId },
|
|
95
|
+
{ $set: { oauthCredentials: [...filtered, record], updatedAt: new Date().toISOString() } }
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
logger.info({ clientId: credentials.clientId, environment }, 'OAuth credentials stored in service record')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Try to create a new OAuth client in Bio-id.
|
|
103
|
+
* Returns the credentials if successful, null otherwise.
|
|
104
|
+
*/
|
|
105
|
+
async function createNewOAuthClient(
|
|
106
|
+
clientId: string,
|
|
107
|
+
serviceName: string,
|
|
108
|
+
environment: string,
|
|
109
|
+
redirectUris: ReadonlyArray<string>,
|
|
110
|
+
createdBy?: string,
|
|
111
|
+
grants?: ReadonlyArray<string>,
|
|
112
|
+
scopes?: ReadonlyArray<string>,
|
|
113
|
+
orgSlug?: string
|
|
114
|
+
): Promise<OAuthCredentials | null> {
|
|
115
|
+
const bioClient = getBioClient()
|
|
116
|
+
|
|
117
|
+
const createResult = await bioClient.createClient({
|
|
118
|
+
clientId,
|
|
119
|
+
name: `${serviceName} (${environment})`,
|
|
120
|
+
redirectUris: [...redirectUris],
|
|
121
|
+
allowedGrantTypes: grants ? [...grants] : ['authorization_code', 'refresh_token'],
|
|
122
|
+
...(scopes ? { allowedScopes: [...scopes] } : {}),
|
|
123
|
+
...(createdBy ? { createdBy } : {}),
|
|
124
|
+
...(orgSlug ? { orgSlug } : {}),
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
if (!createResult.success) {
|
|
128
|
+
logger.error(
|
|
129
|
+
{ clientId, error: createResult.error },
|
|
130
|
+
'Failed to create OAuth client in Bio-id'
|
|
131
|
+
)
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const data = createResult.data as BioClientData
|
|
136
|
+
if (!data.clientSecret) {
|
|
137
|
+
logger.error({ clientId }, 'Bio-ID did not return clientSecret on create')
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
clientId: data.clientId,
|
|
142
|
+
clientSecret: data.clientSecret,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Update an existing OAuth client's redirect URIs in Bio-ID.
|
|
148
|
+
* Does NOT attempt to retrieve or regenerate the secret — the caller
|
|
149
|
+
* must already have the secret from the stored credentials.
|
|
150
|
+
*/
|
|
151
|
+
async function syncRedirectUris(
|
|
152
|
+
clientId: string,
|
|
153
|
+
redirectUris: ReadonlyArray<string>,
|
|
154
|
+
createdBy?: string,
|
|
155
|
+
grants?: ReadonlyArray<string>,
|
|
156
|
+
scopes?: ReadonlyArray<string>,
|
|
157
|
+
orgSlug?: string
|
|
158
|
+
): Promise<void> {
|
|
159
|
+
const bioClient = getBioClient()
|
|
160
|
+
|
|
161
|
+
const updateResult = await bioClient.updateClient(clientId, {
|
|
162
|
+
redirectUris: [...redirectUris],
|
|
163
|
+
...(grants ? { allowedGrantTypes: [...grants] } : {}),
|
|
164
|
+
...(scopes ? { allowedScopes: [...scopes] } : {}),
|
|
165
|
+
...(createdBy ? { createdBy } : {}),
|
|
166
|
+
...(orgSlug ? { orgSlug } : {}),
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
if (!updateResult.success) {
|
|
170
|
+
logger.warn(
|
|
171
|
+
{ clientId, error: updateResult.error },
|
|
172
|
+
'Failed to update OAuth client redirect URIs'
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Provision an OAuth client in Bio-id for a deployed service.
|
|
179
|
+
*
|
|
180
|
+
* Priority order for the client secret:
|
|
181
|
+
* 1. Stored credentials in MongoDB (encrypted) — normal path for subsequent deploys
|
|
182
|
+
* 2. Create new client in Bio-ID — first deploy
|
|
183
|
+
* 3. Regenerate secret — client exists in Bio-ID but no stored credentials (migration)
|
|
184
|
+
*
|
|
185
|
+
* Credentials are persisted (encrypted) in the service document for future deploys.
|
|
186
|
+
* Redirect URIs are synced on every deploy to pick up custom domain changes.
|
|
187
|
+
*
|
|
188
|
+
* Returns null if Bio-id is unavailable or provisioning fails.
|
|
189
|
+
*/
|
|
190
|
+
export async function provisionOAuthClient(
|
|
191
|
+
serviceName: string,
|
|
192
|
+
environment: string,
|
|
193
|
+
namespace: string,
|
|
194
|
+
customDomains?: ReadonlyArray<CustomDomainRecord>,
|
|
195
|
+
createdBy?: string,
|
|
196
|
+
authMode?: AuthMode,
|
|
197
|
+
serviceId?: string,
|
|
198
|
+
storedOAuthCreds?: ReadonlyArray<OAuthCredentialRecord>,
|
|
199
|
+
hasExternalDeps?: boolean,
|
|
200
|
+
dependencyScopes?: ReadonlyArray<string>,
|
|
201
|
+
orgSlug?: string
|
|
202
|
+
): Promise<OAuthCredentials | null> {
|
|
203
|
+
const clientId = buildOAuthClientId(serviceName, environment)
|
|
204
|
+
|
|
205
|
+
const grants = authMode === 'service-only'
|
|
206
|
+
? ['client_credentials'] as const
|
|
207
|
+
: hasExternalDeps
|
|
208
|
+
? ['authorization_code', 'refresh_token', 'client_credentials'] as const
|
|
209
|
+
: ['authorization_code', 'refresh_token'] as const
|
|
210
|
+
|
|
211
|
+
const redirectUris = authMode === 'service-only'
|
|
212
|
+
? []
|
|
213
|
+
: buildRedirectUris(serviceName, environment, customDomains)
|
|
214
|
+
|
|
215
|
+
// Merge SSO scopes with dependency scopes (e.g. relay:send)
|
|
216
|
+
const ssoScopes = ['openid', 'profile', 'email']
|
|
217
|
+
const allScopes = Array.from(new Set([...ssoScopes, ...(dependencyScopes || [])]))
|
|
218
|
+
|
|
219
|
+
logger.info(
|
|
220
|
+
{ clientId, serviceName, environment, authMode, redirectUris, grants, allowedScopes: allScopes },
|
|
221
|
+
'Provisioning OAuth client'
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
// 1. Check stored credentials first (fastest, no network call to Bio-ID)
|
|
226
|
+
const stored = readStoredCredentials(storedOAuthCreds, clientId, environment)
|
|
227
|
+
if (stored) {
|
|
228
|
+
logger.info({ clientId }, 'Using stored OAuth credentials, syncing redirect URIs')
|
|
229
|
+
await syncRedirectUris(clientId, redirectUris, createdBy, [...grants], allScopes, orgSlug)
|
|
230
|
+
return stored
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 2. No stored credentials — check Bio-ID
|
|
234
|
+
const bioClient = getBioClient()
|
|
235
|
+
const existingResult = await bioClient.getClient(clientId)
|
|
236
|
+
|
|
237
|
+
let credentials: OAuthCredentials | null
|
|
238
|
+
|
|
239
|
+
if (existingResult.success && existingResult.data) {
|
|
240
|
+
// Client exists in Bio-ID but we have no stored secret.
|
|
241
|
+
// Must regenerate to get a secret we can store.
|
|
242
|
+
logger.warn({ clientId }, 'OAuth client exists in Bio-ID but no stored credentials, regenerating secret')
|
|
243
|
+
await syncRedirectUris(clientId, redirectUris, createdBy, [...grants], allScopes, orgSlug)
|
|
244
|
+
const regenResult = await bioClient.regenerateSecret(clientId)
|
|
245
|
+
const regenSecret = (regenResult.data as BioClientData | undefined)?.clientSecret
|
|
246
|
+
|
|
247
|
+
if (!regenSecret) {
|
|
248
|
+
logger.error({ clientId, error: regenResult.error }, 'Failed to regenerate OAuth client secret')
|
|
249
|
+
return null
|
|
250
|
+
}
|
|
251
|
+
credentials = { clientId, clientSecret: regenSecret }
|
|
252
|
+
} else if (
|
|
253
|
+
existingResult.error?.code === 'NOT_FOUND' ||
|
|
254
|
+
existingResult.error?.code === 'RESOURCE_NOT_FOUND'
|
|
255
|
+
) {
|
|
256
|
+
// Client does not exist — create new
|
|
257
|
+
logger.info({ clientId }, 'OAuth client not found, creating new')
|
|
258
|
+
credentials = await createNewOAuthClient(clientId, serviceName, environment, redirectUris, createdBy, [...grants], allScopes, orgSlug)
|
|
259
|
+
} else {
|
|
260
|
+
// Bio-id returned an unexpected error (likely unavailable)
|
|
261
|
+
logger.error(
|
|
262
|
+
{ clientId, error: existingResult.error },
|
|
263
|
+
'Bio-id returned unexpected error, skipping OAuth provisioning'
|
|
264
|
+
)
|
|
265
|
+
return null
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!credentials) {
|
|
269
|
+
return null
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 3. Store credentials for future deploys
|
|
273
|
+
if (serviceId) {
|
|
274
|
+
await storeCredentials(serviceId, credentials, environment, storedOAuthCreds)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return credentials
|
|
278
|
+
} catch (error) {
|
|
279
|
+
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
280
|
+
logger.error(
|
|
281
|
+
{ clientId, error: message },
|
|
282
|
+
'OAuth provisioning failed'
|
|
283
|
+
)
|
|
284
|
+
return null
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { spawn } from 'child_process'
|
|
2
|
+
import { logger } from '../utils/logger.js'
|
|
3
|
+
import type { PodDiagnostic, ContainerStatusInfo, PodEventInfo, DeployDiagnostics } from '../models/types.js'
|
|
4
|
+
|
|
5
|
+
const KUBECTL_TIMEOUT = 15_000
|
|
6
|
+
|
|
7
|
+
// Strict K8s name validation to prevent command injection
|
|
8
|
+
const K8S_NAME_REGEX = /^[a-z0-9][a-z0-9._-]{0,252}$/
|
|
9
|
+
|
|
10
|
+
function validateK8sName(value: string, label: string): string {
|
|
11
|
+
if (!K8S_NAME_REGEX.test(value)) {
|
|
12
|
+
throw new Error(`Invalid ${label}: "${value}"`)
|
|
13
|
+
}
|
|
14
|
+
return value
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function kubectlArgs(args: ReadonlyArray<string>): Promise<string> {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const proc = spawn('kubectl', args as string[])
|
|
20
|
+
let out = ''
|
|
21
|
+
let errOut = ''
|
|
22
|
+
proc.stdout.on('data', (chunk: Buffer) => { out += chunk.toString() })
|
|
23
|
+
proc.stderr.on('data', (chunk: Buffer) => { errOut += chunk.toString() })
|
|
24
|
+
proc.on('close', (code) => {
|
|
25
|
+
if (code !== 0 && errOut) {
|
|
26
|
+
logger.warn({ args: args.slice(0, 6), code }, 'kubectl command failed (diagnostic)')
|
|
27
|
+
}
|
|
28
|
+
resolve(out.trim())
|
|
29
|
+
})
|
|
30
|
+
proc.on('error', (err) => {
|
|
31
|
+
logger.warn({ args: args.slice(0, 6), err }, 'kubectl spawn failed (diagnostic)')
|
|
32
|
+
resolve('')
|
|
33
|
+
})
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
proc.kill('SIGTERM')
|
|
36
|
+
resolve('')
|
|
37
|
+
}, KUBECTL_TIMEOUT)
|
|
38
|
+
proc.on('close', () => clearTimeout(timer))
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface K8sContainerStatus {
|
|
43
|
+
name: string
|
|
44
|
+
ready: boolean
|
|
45
|
+
restartCount: number
|
|
46
|
+
state: Record<string, { reason?: string; message?: string; exitCode?: number; startedAt?: string }>
|
|
47
|
+
lastState?: Record<string, { reason?: string; message?: string; exitCode?: number }>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface K8sPodItem {
|
|
51
|
+
metadata: { name: string; namespace: string }
|
|
52
|
+
status: {
|
|
53
|
+
phase: string
|
|
54
|
+
reason?: string
|
|
55
|
+
containerStatuses?: K8sContainerStatus[]
|
|
56
|
+
initContainerStatuses?: K8sContainerStatus[]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface K8sEvent {
|
|
61
|
+
type: string
|
|
62
|
+
reason: string
|
|
63
|
+
message: string
|
|
64
|
+
lastTimestamp?: string
|
|
65
|
+
eventTime?: string
|
|
66
|
+
metadata?: { creationTimestamp?: string }
|
|
67
|
+
involvedObject?: { name: string }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseContainerStatus(cs: K8sContainerStatus): ContainerStatusInfo {
|
|
71
|
+
const stateKey = Object.keys(cs.state)[0] ?? 'unknown'
|
|
72
|
+
const stateDetail = cs.state[stateKey] ?? {}
|
|
73
|
+
const lastStateKey = cs.lastState ? Object.keys(cs.lastState)[0] : undefined
|
|
74
|
+
const lastDetail = lastStateKey && cs.lastState ? cs.lastState[lastStateKey] : undefined
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
name: cs.name,
|
|
78
|
+
ready: cs.ready,
|
|
79
|
+
state: stateKey,
|
|
80
|
+
reason: stateDetail.reason ?? lastDetail?.reason,
|
|
81
|
+
message: stateDetail.message ?? lastDetail?.message,
|
|
82
|
+
exitCode: stateDetail.exitCode ?? lastDetail?.exitCode,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function computeRestartCount(containerStatuses: K8sContainerStatus[] | undefined): number {
|
|
87
|
+
if (!containerStatuses) return 0
|
|
88
|
+
return containerStatuses.reduce((sum, cs) => sum + (cs.restartCount || 0), 0)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function getPodEvents(namespace: string, podName: string): Promise<ReadonlyArray<PodEventInfo>> {
|
|
92
|
+
const ns = validateK8sName(namespace, 'namespace')
|
|
93
|
+
const pod = validateK8sName(podName, 'podName')
|
|
94
|
+
const raw = await kubectlArgs([
|
|
95
|
+
'get', 'events', '-n', ns,
|
|
96
|
+
'--field-selector', `involvedObject.name=${pod}`,
|
|
97
|
+
'--sort-by=.lastTimestamp', '-o', 'json',
|
|
98
|
+
])
|
|
99
|
+
if (!raw) return []
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(raw)
|
|
103
|
+
const items: K8sEvent[] = parsed.items ?? []
|
|
104
|
+
return items.slice(-20).map((e) => ({
|
|
105
|
+
type: e.type ?? 'Normal',
|
|
106
|
+
reason: e.reason ?? 'Unknown',
|
|
107
|
+
message: e.message ?? '',
|
|
108
|
+
age: e.lastTimestamp ?? e.eventTime ?? e.metadata?.creationTimestamp ?? '',
|
|
109
|
+
}))
|
|
110
|
+
} catch {
|
|
111
|
+
return []
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function getContainerLogs(
|
|
116
|
+
namespace: string,
|
|
117
|
+
podName: string,
|
|
118
|
+
options?: { previous?: boolean; tail?: number; container?: string }
|
|
119
|
+
): Promise<string> {
|
|
120
|
+
const ns = validateK8sName(namespace, 'namespace')
|
|
121
|
+
const pod = validateK8sName(podName, 'podName')
|
|
122
|
+
const tail = options?.tail ?? 50
|
|
123
|
+
const args = ['logs', '-n', ns, pod, `--tail=${tail}`]
|
|
124
|
+
if (options?.previous) args.push('--previous')
|
|
125
|
+
if (options?.container) {
|
|
126
|
+
args.push('-c', validateK8sName(options.container, 'container'))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return kubectlArgs(args)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function getPods(namespace: string, serviceName?: string): Promise<ReadonlyArray<PodDiagnostic>> {
|
|
133
|
+
const ns = validateK8sName(namespace, 'namespace')
|
|
134
|
+
const baseArgs = ['get', 'pods', '-n', ns, '-o', 'json']
|
|
135
|
+
const labelArgs = serviceName
|
|
136
|
+
? [...baseArgs, '-l', `app=${validateK8sName(serviceName, 'serviceName')}`]
|
|
137
|
+
: baseArgs
|
|
138
|
+
const raw = await kubectlArgs(labelArgs)
|
|
139
|
+
if (!raw) {
|
|
140
|
+
// Try alternative label selector for custom charts
|
|
141
|
+
if (serviceName) {
|
|
142
|
+
const altRaw = await kubectlArgs([
|
|
143
|
+
...baseArgs, '-l', `app.kubernetes.io/name=${validateK8sName(serviceName, 'serviceName')}`,
|
|
144
|
+
])
|
|
145
|
+
if (altRaw) return parsePodList(altRaw, ns)
|
|
146
|
+
}
|
|
147
|
+
return []
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return parsePodList(raw, ns)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function parsePodList(raw: string, namespace: string): Promise<ReadonlyArray<PodDiagnostic>> {
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(raw)
|
|
156
|
+
const items: K8sPodItem[] = parsed.items ?? []
|
|
157
|
+
|
|
158
|
+
const results = await Promise.all(
|
|
159
|
+
items.map(async (pod) => {
|
|
160
|
+
const containerStatuses = (pod.status.containerStatuses ?? []).map(parseContainerStatus)
|
|
161
|
+
const initContainerStatuses = (pod.status.initContainerStatuses ?? []).map(parseContainerStatus)
|
|
162
|
+
const restartCount = computeRestartCount(pod.status.containerStatuses)
|
|
163
|
+
const events = await getPodEvents(namespace, pod.metadata.name)
|
|
164
|
+
|
|
165
|
+
const isUnhealthy = pod.status.phase !== 'Running' && pod.status.phase !== 'Succeeded'
|
|
166
|
+
const hasCrashes = restartCount > 0
|
|
167
|
+
const logs = isUnhealthy || hasCrashes
|
|
168
|
+
? await getContainerLogs(namespace, pod.metadata.name, { tail: 50 })
|
|
169
|
+
: undefined
|
|
170
|
+
|
|
171
|
+
const previousLogs = hasCrashes
|
|
172
|
+
? await getContainerLogs(namespace, pod.metadata.name, { tail: 30, previous: true })
|
|
173
|
+
: undefined
|
|
174
|
+
|
|
175
|
+
const combinedLogs = [previousLogs ? `--- Previous container ---\n${previousLogs}` : '', logs]
|
|
176
|
+
.filter(Boolean)
|
|
177
|
+
.join('\n--- Current container ---\n')
|
|
178
|
+
|
|
179
|
+
const diagnostic: PodDiagnostic = {
|
|
180
|
+
name: pod.metadata.name,
|
|
181
|
+
phase: pod.status.phase,
|
|
182
|
+
reason: pod.status.reason,
|
|
183
|
+
restartCount,
|
|
184
|
+
containerStatuses,
|
|
185
|
+
...(initContainerStatuses.length > 0 ? { initContainerStatuses } : {}),
|
|
186
|
+
events,
|
|
187
|
+
...(combinedLogs ? { logs: combinedLogs } : {}),
|
|
188
|
+
}
|
|
189
|
+
return diagnostic
|
|
190
|
+
})
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return results
|
|
194
|
+
} catch {
|
|
195
|
+
return []
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function summarizeDiagnostics(pods: ReadonlyArray<PodDiagnostic>): string {
|
|
200
|
+
if (pods.length === 0) return 'No pods found in namespace'
|
|
201
|
+
|
|
202
|
+
const issues: string[] = []
|
|
203
|
+
|
|
204
|
+
for (const pod of pods) {
|
|
205
|
+
const allStatuses = [...pod.containerStatuses, ...(pod.initContainerStatuses ?? [])]
|
|
206
|
+
|
|
207
|
+
for (const cs of allStatuses) {
|
|
208
|
+
if (cs.reason === 'CrashLoopBackOff') {
|
|
209
|
+
issues.push(`CrashLoopBackOff: ${cs.name} in ${pod.name} (${pod.restartCount} restarts)`)
|
|
210
|
+
}
|
|
211
|
+
if (cs.reason === 'ImagePullBackOff' || cs.reason === 'ErrImagePull') {
|
|
212
|
+
issues.push(`ImagePullBackOff: ${cs.name} in ${pod.name}`)
|
|
213
|
+
}
|
|
214
|
+
if (cs.reason === 'OOMKilled') {
|
|
215
|
+
issues.push(`OOMKilled: ${cs.name} in ${pod.name}`)
|
|
216
|
+
}
|
|
217
|
+
if (cs.reason === 'CreateContainerConfigError') {
|
|
218
|
+
issues.push(`CreateContainerConfigError: ${cs.name} in ${pod.name}`)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const vaultInit = (pod.initContainerStatuses ?? []).find(
|
|
223
|
+
(cs) => cs.name.includes('vault') && !cs.ready
|
|
224
|
+
)
|
|
225
|
+
if (vaultInit) {
|
|
226
|
+
issues.push(`Vault init failed: ${vaultInit.name} in ${pod.name}`)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (pod.phase === 'Pending') {
|
|
230
|
+
const schedEvent = pod.events.find((e) => e.reason === 'FailedScheduling')
|
|
231
|
+
if (schedEvent) {
|
|
232
|
+
issues.push(`FailedScheduling: ${pod.name} — ${schedEvent.message.slice(0, 100)}`)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (issues.length === 0) {
|
|
238
|
+
const allRunning = pods.every((p) => p.phase === 'Running' || p.phase === 'Succeeded')
|
|
239
|
+
const totalRestarts = pods.reduce((sum, p) => sum + p.restartCount, 0)
|
|
240
|
+
if (allRunning && totalRestarts === 0) return 'All pods healthy'
|
|
241
|
+
if (allRunning) return `All pods running (${totalRestarts} total restarts)`
|
|
242
|
+
return `${pods.length} pod(s) — mixed status`
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return issues.join('; ')
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function captureDeployDiagnostics(
|
|
249
|
+
serviceName: string,
|
|
250
|
+
namespace: string
|
|
251
|
+
): Promise<DeployDiagnostics> {
|
|
252
|
+
const pods = await getPods(namespace, serviceName)
|
|
253
|
+
const summary = summarizeDiagnostics(pods)
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
capturedAt: new Date().toISOString(),
|
|
257
|
+
namespace,
|
|
258
|
+
pods,
|
|
259
|
+
summary,
|
|
260
|
+
}
|
|
261
|
+
}
|