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,329 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
const { mockGetPods } = vi.hoisted(() => ({
|
|
4
|
+
mockGetPods: vi.fn(),
|
|
5
|
+
}))
|
|
6
|
+
|
|
7
|
+
vi.mock('../pod-diagnostics.js', () => ({
|
|
8
|
+
getPods: (...args: unknown[]) => mockGetPods(...args),
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
vi.mock('../../utils/logger.js', () => ({
|
|
12
|
+
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
import { troubleshoot, type TroubleshootResult } from '../troubleshooter.js'
|
|
16
|
+
import type { PodDiagnostic } from '../../models/types.js'
|
|
17
|
+
|
|
18
|
+
function makePod(overrides: Partial<PodDiagnostic> = {}): PodDiagnostic {
|
|
19
|
+
return {
|
|
20
|
+
name: 'my-api-abc123',
|
|
21
|
+
phase: 'Running',
|
|
22
|
+
restartCount: 0,
|
|
23
|
+
containerStatuses: [{ name: 'my-api', ready: true, state: 'running' }],
|
|
24
|
+
events: [],
|
|
25
|
+
...overrides,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('troubleshooter', () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.clearAllMocks()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('healthy service', () => {
|
|
35
|
+
it('returns healthy=true with no issues for running pods', async () => {
|
|
36
|
+
mockGetPods.mockResolvedValue([makePod()])
|
|
37
|
+
|
|
38
|
+
const result = await troubleshoot('my-api', 'my-api-prod', 'prod')
|
|
39
|
+
expect(result.healthy).toBe(true)
|
|
40
|
+
expect(result.issues).toHaveLength(0)
|
|
41
|
+
expect(result.service).toBe('my-api')
|
|
42
|
+
expect(result.namespace).toBe('my-api-prod')
|
|
43
|
+
expect(result.environment).toBe('prod')
|
|
44
|
+
expect(result.pods).toHaveLength(1)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('no pods', () => {
|
|
49
|
+
it('returns critical issue when namespace has no pods', async () => {
|
|
50
|
+
mockGetPods.mockResolvedValue([])
|
|
51
|
+
|
|
52
|
+
const result = await troubleshoot('my-api', 'my-api-prod', 'prod')
|
|
53
|
+
expect(result.healthy).toBe(false)
|
|
54
|
+
expect(result.issues).toHaveLength(1)
|
|
55
|
+
expect(result.issues[0].severity).toBe('critical')
|
|
56
|
+
expect(result.issues[0].category).toBe('deployment')
|
|
57
|
+
expect(result.issues[0].title).toBe('No Pods Found')
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('crash-loop detection', () => {
|
|
62
|
+
it('detects CrashLoopBackOff in container', async () => {
|
|
63
|
+
mockGetPods.mockResolvedValue([
|
|
64
|
+
makePod({
|
|
65
|
+
restartCount: 5,
|
|
66
|
+
containerStatuses: [{
|
|
67
|
+
name: 'my-api',
|
|
68
|
+
ready: false,
|
|
69
|
+
state: 'waiting',
|
|
70
|
+
reason: 'CrashLoopBackOff',
|
|
71
|
+
exitCode: 1,
|
|
72
|
+
}],
|
|
73
|
+
}),
|
|
74
|
+
])
|
|
75
|
+
|
|
76
|
+
const result = await troubleshoot('my-api', 'my-api-prod', 'prod')
|
|
77
|
+
expect(result.healthy).toBe(false)
|
|
78
|
+
const issue = result.issues.find((i) => i.title === 'CrashLoopBackOff')
|
|
79
|
+
expect(issue).toBeDefined()
|
|
80
|
+
expect(issue!.severity).toBe('critical')
|
|
81
|
+
expect(issue!.category).toBe('crash')
|
|
82
|
+
expect(issue!.detail).toContain('5 restarts')
|
|
83
|
+
expect(issue!.containerName).toBe('my-api')
|
|
84
|
+
expect(issue!.suggestion).toContain('tawa logs')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('detects CrashLoopBackOff in init container', async () => {
|
|
88
|
+
mockGetPods.mockResolvedValue([
|
|
89
|
+
makePod({
|
|
90
|
+
phase: 'Init:Error',
|
|
91
|
+
containerStatuses: [{ name: 'my-api', ready: false, state: 'waiting' }],
|
|
92
|
+
initContainerStatuses: [{
|
|
93
|
+
name: 'vault-agent-init',
|
|
94
|
+
ready: false,
|
|
95
|
+
state: 'waiting',
|
|
96
|
+
reason: 'CrashLoopBackOff',
|
|
97
|
+
}],
|
|
98
|
+
}),
|
|
99
|
+
])
|
|
100
|
+
|
|
101
|
+
const result = await troubleshoot('my-api', 'my-api-prod', 'prod')
|
|
102
|
+
expect(result.healthy).toBe(false)
|
|
103
|
+
const crashIssue = result.issues.find((i) => i.title === 'CrashLoopBackOff')
|
|
104
|
+
expect(crashIssue).toBeDefined()
|
|
105
|
+
expect(crashIssue!.containerName).toBe('vault-agent-init')
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('image-pull detection', () => {
|
|
110
|
+
it('detects ImagePullBackOff', async () => {
|
|
111
|
+
mockGetPods.mockResolvedValue([
|
|
112
|
+
makePod({
|
|
113
|
+
phase: 'Pending',
|
|
114
|
+
containerStatuses: [{
|
|
115
|
+
name: 'my-api',
|
|
116
|
+
ready: false,
|
|
117
|
+
state: 'waiting',
|
|
118
|
+
reason: 'ImagePullBackOff',
|
|
119
|
+
message: 'Back-off pulling image',
|
|
120
|
+
}],
|
|
121
|
+
}),
|
|
122
|
+
])
|
|
123
|
+
|
|
124
|
+
const result = await troubleshoot('my-api', 'my-api-prod', 'prod')
|
|
125
|
+
expect(result.healthy).toBe(false)
|
|
126
|
+
const issue = result.issues.find((i) => i.title === 'ImagePullBackOff')
|
|
127
|
+
expect(issue).toBeDefined()
|
|
128
|
+
expect(issue!.severity).toBe('critical')
|
|
129
|
+
expect(issue!.suggestion).toContain('registry')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('detects ErrImagePull', async () => {
|
|
133
|
+
mockGetPods.mockResolvedValue([
|
|
134
|
+
makePod({
|
|
135
|
+
phase: 'Pending',
|
|
136
|
+
containerStatuses: [{
|
|
137
|
+
name: 'my-api',
|
|
138
|
+
ready: false,
|
|
139
|
+
state: 'waiting',
|
|
140
|
+
reason: 'ErrImagePull',
|
|
141
|
+
}],
|
|
142
|
+
}),
|
|
143
|
+
])
|
|
144
|
+
|
|
145
|
+
const result = await troubleshoot('my-api', 'my-api-prod', 'prod')
|
|
146
|
+
const issue = result.issues.find((i) => i.title === 'ImagePullBackOff')
|
|
147
|
+
expect(issue).toBeDefined()
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('OOMKilled detection', () => {
|
|
152
|
+
it('detects OOMKilled container', async () => {
|
|
153
|
+
mockGetPods.mockResolvedValue([
|
|
154
|
+
makePod({
|
|
155
|
+
restartCount: 1,
|
|
156
|
+
containerStatuses: [{
|
|
157
|
+
name: 'my-api',
|
|
158
|
+
ready: false,
|
|
159
|
+
state: 'terminated',
|
|
160
|
+
reason: 'OOMKilled',
|
|
161
|
+
}],
|
|
162
|
+
}),
|
|
163
|
+
])
|
|
164
|
+
|
|
165
|
+
const result = await troubleshoot('my-api', 'my-api-prod', 'prod')
|
|
166
|
+
expect(result.healthy).toBe(false)
|
|
167
|
+
const issue = result.issues.find((i) => i.title === 'OOMKilled')
|
|
168
|
+
expect(issue).toBeDefined()
|
|
169
|
+
expect(issue!.severity).toBe('critical')
|
|
170
|
+
expect(issue!.suggestion).toContain('pod-tier')
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
describe('vault-init-failure detection', () => {
|
|
175
|
+
it('detects failed Vault init container', async () => {
|
|
176
|
+
mockGetPods.mockResolvedValue([
|
|
177
|
+
makePod({
|
|
178
|
+
phase: 'Init:Error',
|
|
179
|
+
containerStatuses: [{ name: 'my-api', ready: false, state: 'waiting' }],
|
|
180
|
+
initContainerStatuses: [{
|
|
181
|
+
name: 'vault-agent-init',
|
|
182
|
+
ready: false,
|
|
183
|
+
state: 'waiting',
|
|
184
|
+
message: 'vault auth failed',
|
|
185
|
+
}],
|
|
186
|
+
}),
|
|
187
|
+
])
|
|
188
|
+
|
|
189
|
+
const result = await troubleshoot('my-api', 'my-api-prod', 'prod')
|
|
190
|
+
expect(result.healthy).toBe(false)
|
|
191
|
+
const issue = result.issues.find((i) => i.title === 'Vault Init Container Failed')
|
|
192
|
+
expect(issue).toBeDefined()
|
|
193
|
+
expect(issue!.severity).toBe('critical')
|
|
194
|
+
expect(issue!.category).toBe('vault')
|
|
195
|
+
expect(issue!.suggestion).toContain('Vault')
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
describe('config-error detection', () => {
|
|
200
|
+
it('detects CreateContainerConfigError', async () => {
|
|
201
|
+
mockGetPods.mockResolvedValue([
|
|
202
|
+
makePod({
|
|
203
|
+
containerStatuses: [{
|
|
204
|
+
name: 'my-api',
|
|
205
|
+
ready: false,
|
|
206
|
+
state: 'waiting',
|
|
207
|
+
reason: 'CreateContainerConfigError',
|
|
208
|
+
message: 'secret "my-api-managed-secrets" not found',
|
|
209
|
+
}],
|
|
210
|
+
}),
|
|
211
|
+
])
|
|
212
|
+
|
|
213
|
+
const result = await troubleshoot('my-api', 'my-api-prod', 'prod')
|
|
214
|
+
expect(result.healthy).toBe(false)
|
|
215
|
+
const issue = result.issues.find((i) => i.title === 'Missing ConfigMap or Secret')
|
|
216
|
+
expect(issue).toBeDefined()
|
|
217
|
+
expect(issue!.detail).toContain('my-api-managed-secrets')
|
|
218
|
+
expect(issue!.suggestion).toContain('tawa config set')
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
describe('pending-scheduling detection', () => {
|
|
223
|
+
it('detects FailedScheduling event on Pending pod', async () => {
|
|
224
|
+
mockGetPods.mockResolvedValue([
|
|
225
|
+
makePod({
|
|
226
|
+
phase: 'Pending',
|
|
227
|
+
containerStatuses: [],
|
|
228
|
+
events: [{
|
|
229
|
+
type: 'Warning',
|
|
230
|
+
reason: 'FailedScheduling',
|
|
231
|
+
message: '0/3 nodes are available: insufficient cpu.',
|
|
232
|
+
age: '2m',
|
|
233
|
+
}],
|
|
234
|
+
}),
|
|
235
|
+
])
|
|
236
|
+
|
|
237
|
+
const result = await troubleshoot('my-api', 'my-api-prod', 'prod')
|
|
238
|
+
const issue = result.issues.find((i) => i.title === 'Pod Pending (FailedScheduling)')
|
|
239
|
+
expect(issue).toBeDefined()
|
|
240
|
+
expect(issue!.severity).toBe('warning')
|
|
241
|
+
expect(issue!.detail).toContain('insufficient cpu')
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
describe('high-restarts detection', () => {
|
|
246
|
+
it('flags pods with 3+ restarts not in CrashLoopBackOff', async () => {
|
|
247
|
+
mockGetPods.mockResolvedValue([
|
|
248
|
+
makePod({
|
|
249
|
+
restartCount: 4,
|
|
250
|
+
containerStatuses: [{
|
|
251
|
+
name: 'my-api',
|
|
252
|
+
ready: true,
|
|
253
|
+
state: 'running',
|
|
254
|
+
}],
|
|
255
|
+
}),
|
|
256
|
+
])
|
|
257
|
+
|
|
258
|
+
const result = await troubleshoot('my-api', 'my-api-prod', 'prod')
|
|
259
|
+
const issue = result.issues.find((i) => i.title === 'High Restart Count')
|
|
260
|
+
expect(issue).toBeDefined()
|
|
261
|
+
expect(issue!.severity).toBe('warning')
|
|
262
|
+
expect(issue!.detail).toContain('4 times')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('does not flag high restarts when CrashLoopBackOff is already detected', async () => {
|
|
266
|
+
mockGetPods.mockResolvedValue([
|
|
267
|
+
makePod({
|
|
268
|
+
restartCount: 10,
|
|
269
|
+
containerStatuses: [{
|
|
270
|
+
name: 'my-api',
|
|
271
|
+
ready: false,
|
|
272
|
+
state: 'waiting',
|
|
273
|
+
reason: 'CrashLoopBackOff',
|
|
274
|
+
}],
|
|
275
|
+
}),
|
|
276
|
+
])
|
|
277
|
+
|
|
278
|
+
const result = await troubleshoot('my-api', 'my-api-prod', 'prod')
|
|
279
|
+
const highRestartsIssue = result.issues.find((i) => i.title === 'High Restart Count')
|
|
280
|
+
expect(highRestartsIssue).toBeUndefined()
|
|
281
|
+
const crashIssue = result.issues.find((i) => i.title === 'CrashLoopBackOff')
|
|
282
|
+
expect(crashIssue).toBeDefined()
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
describe('mixed status', () => {
|
|
287
|
+
it('reports multiple issues from different pods sorted by severity', async () => {
|
|
288
|
+
mockGetPods.mockResolvedValue([
|
|
289
|
+
makePod({ name: 'healthy-pod' }),
|
|
290
|
+
makePod({
|
|
291
|
+
name: 'crash-pod',
|
|
292
|
+
restartCount: 5,
|
|
293
|
+
containerStatuses: [{
|
|
294
|
+
name: 'app',
|
|
295
|
+
ready: false,
|
|
296
|
+
state: 'waiting',
|
|
297
|
+
reason: 'CrashLoopBackOff',
|
|
298
|
+
}],
|
|
299
|
+
}),
|
|
300
|
+
makePod({
|
|
301
|
+
name: 'restart-pod',
|
|
302
|
+
restartCount: 4,
|
|
303
|
+
containerStatuses: [{
|
|
304
|
+
name: 'app',
|
|
305
|
+
ready: true,
|
|
306
|
+
state: 'running',
|
|
307
|
+
}],
|
|
308
|
+
}),
|
|
309
|
+
])
|
|
310
|
+
|
|
311
|
+
const result = await troubleshoot('my-api', 'my-api-prod', 'prod')
|
|
312
|
+
expect(result.healthy).toBe(false)
|
|
313
|
+
expect(result.issues.length).toBeGreaterThanOrEqual(2)
|
|
314
|
+
// Critical issues should come first
|
|
315
|
+
expect(result.issues[0].severity).toBe('critical')
|
|
316
|
+
expect(result.pods).toHaveLength(3)
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
describe('result structure', () => {
|
|
321
|
+
it('includes analyzedAt timestamp', async () => {
|
|
322
|
+
mockGetPods.mockResolvedValue([makePod()])
|
|
323
|
+
|
|
324
|
+
const result = await troubleshoot('my-api', 'my-api-prod', 'prod')
|
|
325
|
+
expect(result.analyzedAt).toBeTruthy()
|
|
326
|
+
expect(new Date(result.analyzedAt).getTime()).not.toBeNaN()
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
})
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { env } from '../config/env.js'
|
|
2
|
+
import { logger } from '../utils/logger.js'
|
|
3
|
+
|
|
4
|
+
interface ApiResponse<T> {
|
|
5
|
+
success: boolean
|
|
6
|
+
data?: T
|
|
7
|
+
error?: {
|
|
8
|
+
code: string
|
|
9
|
+
message: string
|
|
10
|
+
details?: unknown
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface RequestOptions {
|
|
15
|
+
forwardedFor?: string
|
|
16
|
+
userAgent?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class BioClient {
|
|
20
|
+
private baseUrl: string
|
|
21
|
+
private internalKey: string
|
|
22
|
+
|
|
23
|
+
constructor(baseUrl: string, internalKey: string) {
|
|
24
|
+
this.baseUrl = baseUrl.replace(/\/$/, '')
|
|
25
|
+
this.internalKey = internalKey
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private async request<T>(
|
|
29
|
+
method: string,
|
|
30
|
+
path: string,
|
|
31
|
+
body?: unknown,
|
|
32
|
+
options?: RequestOptions
|
|
33
|
+
): Promise<ApiResponse<T>> {
|
|
34
|
+
const url = `${this.baseUrl}${path}`
|
|
35
|
+
const headers: Record<string, string> = {
|
|
36
|
+
'Content-Type': 'application/json',
|
|
37
|
+
'X-Internal-Key': this.internalKey,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (options?.forwardedFor) {
|
|
41
|
+
headers['X-Forwarded-For'] = options.forwardedFor
|
|
42
|
+
}
|
|
43
|
+
if (options?.userAgent) {
|
|
44
|
+
headers['User-Agent'] = options.userAgent
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch(url, {
|
|
49
|
+
method,
|
|
50
|
+
headers,
|
|
51
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const json = await response.json() as ApiResponse<T>
|
|
55
|
+
|
|
56
|
+
if (!response.ok && !json.error) {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
error: {
|
|
60
|
+
code: 'UPSTREAM_ERROR',
|
|
61
|
+
message: `Bio-ID returned ${response.status}`,
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return json
|
|
67
|
+
} catch (error) {
|
|
68
|
+
logger.error({ error, url, method }, 'Bio-ID request failed')
|
|
69
|
+
return {
|
|
70
|
+
success: false,
|
|
71
|
+
error: {
|
|
72
|
+
code: 'UPSTREAM_ERROR',
|
|
73
|
+
message: 'Bio-ID service unavailable',
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async listClients(options?: RequestOptions) {
|
|
80
|
+
return this.request<unknown[]>('GET', '/api/admin/oauth-clients', undefined, options)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async getClient(clientId: string, options?: RequestOptions) {
|
|
84
|
+
return this.request<unknown>('GET', `/api/admin/oauth-clients/${encodeURIComponent(clientId)}`, undefined, options)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async createClient(data: unknown, options?: RequestOptions) {
|
|
88
|
+
return this.request<unknown>('POST', '/api/admin/oauth-clients', data, options)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async updateClient(clientId: string, data: unknown, options?: RequestOptions) {
|
|
92
|
+
return this.request<unknown>('PATCH', `/api/admin/oauth-clients/${encodeURIComponent(clientId)}`, data, options)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async deleteClient(clientId: string, options?: RequestOptions) {
|
|
96
|
+
return this.request<unknown>('DELETE', `/api/admin/oauth-clients/${encodeURIComponent(clientId)}`, undefined, options)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async addRedirectUri(clientId: string, uri: string, options?: RequestOptions) {
|
|
100
|
+
return this.request<unknown>(
|
|
101
|
+
'POST',
|
|
102
|
+
`/api/admin/oauth-clients/${encodeURIComponent(clientId)}/redirect-uris`,
|
|
103
|
+
{ uri },
|
|
104
|
+
options
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async removeRedirectUri(clientId: string, uri: string, options?: RequestOptions) {
|
|
109
|
+
return this.request<unknown>(
|
|
110
|
+
'DELETE',
|
|
111
|
+
`/api/admin/oauth-clients/${encodeURIComponent(clientId)}/redirect-uris`,
|
|
112
|
+
{ uri },
|
|
113
|
+
options
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async regenerateSecret(clientId: string, options?: RequestOptions) {
|
|
118
|
+
return this.request<unknown>(
|
|
119
|
+
'POST',
|
|
120
|
+
`/api/admin/oauth-clients/${encodeURIComponent(clientId)}/regenerate-secret`,
|
|
121
|
+
undefined,
|
|
122
|
+
options
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async listScopeGrants(filters: {
|
|
127
|
+
requestingServiceId?: string
|
|
128
|
+
targetServiceId?: string
|
|
129
|
+
credentialId?: string
|
|
130
|
+
status?: string
|
|
131
|
+
}, options?: RequestOptions) {
|
|
132
|
+
const params = new URLSearchParams()
|
|
133
|
+
if (filters.requestingServiceId) params.set('requestingServiceId', filters.requestingServiceId)
|
|
134
|
+
if (filters.targetServiceId) params.set('targetServiceId', filters.targetServiceId)
|
|
135
|
+
if (filters.credentialId) params.set('credentialId', filters.credentialId)
|
|
136
|
+
if (filters.status) params.set('status', filters.status)
|
|
137
|
+
const qs = params.toString()
|
|
138
|
+
return this.request<unknown[]>('GET', `/api/admin/scope-grants${qs ? `?${qs}` : ''}`, undefined, options)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async createScopeGrant(data: {
|
|
142
|
+
requestingServiceId: string
|
|
143
|
+
targetServiceId: string
|
|
144
|
+
requestedScopes: string[]
|
|
145
|
+
requestNote?: string
|
|
146
|
+
credentialType: 'oauth_client' | 'api_key'
|
|
147
|
+
credentialId: string
|
|
148
|
+
}, options?: RequestOptions) {
|
|
149
|
+
return this.request<unknown>('POST', '/api/admin/scope-grants', data, options)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async registerModule(data: {
|
|
153
|
+
moduleId: string
|
|
154
|
+
name: string
|
|
155
|
+
description: string
|
|
156
|
+
serviceId: string
|
|
157
|
+
owner?: string
|
|
158
|
+
homepage?: string
|
|
159
|
+
scopes: ReadonlyArray<{ code: string; name: string; description: string }>
|
|
160
|
+
defaultScopes?: readonly string[]
|
|
161
|
+
onboarding?: {
|
|
162
|
+
requirePasswordSetup?: boolean
|
|
163
|
+
requireProfileCompletion?: boolean
|
|
164
|
+
podOnboardingUrl?: string
|
|
165
|
+
welcomeMessage?: string
|
|
166
|
+
skipForExistingUsers?: boolean
|
|
167
|
+
}
|
|
168
|
+
}, options?: RequestOptions) {
|
|
169
|
+
return this.request<unknown>('POST', '/api/admin/modules', data, options)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let bioClientInstance: BioClient | null = null
|
|
174
|
+
|
|
175
|
+
export function getBioClient(): BioClient {
|
|
176
|
+
if (!bioClientInstance) {
|
|
177
|
+
if (!env.BIO_INTERNAL_KEY) {
|
|
178
|
+
throw new Error('BIO_INTERNAL_KEY is not configured')
|
|
179
|
+
}
|
|
180
|
+
bioClientInstance = new BioClient(env.BIO_ID_URL, env.BIO_INTERNAL_KEY)
|
|
181
|
+
}
|
|
182
|
+
return bioClientInstance
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function extractRequestMeta(req: { headers: Record<string, string | string[] | undefined> }): RequestOptions {
|
|
186
|
+
const forwardedFor = (req.headers['x-forwarded-for'] as string) || (req.headers['x-real-ip'] as string)
|
|
187
|
+
const userAgent = req.headers['user-agent'] as string
|
|
188
|
+
return { forwardedFor, userAgent }
|
|
189
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Queue, Worker, type Job } from 'bullmq'
|
|
2
|
+
import { env } from '../config/env.js'
|
|
3
|
+
import { logger } from '../utils/logger.js'
|
|
4
|
+
import { executeBuild } from './builder.js'
|
|
5
|
+
import { getBuildsCollection, getRedis } from './database.js'
|
|
6
|
+
|
|
7
|
+
const QUEUE_NAME = 'iec-builds'
|
|
8
|
+
const LEGACY_QUEUE_KEY = 'iec:build:queue'
|
|
9
|
+
|
|
10
|
+
let buildQueue: Queue | null = null
|
|
11
|
+
let buildWorker: Worker | null = null
|
|
12
|
+
|
|
13
|
+
function parseRedisConnection() {
|
|
14
|
+
const url = new URL(env.REDIS_URL)
|
|
15
|
+
return {
|
|
16
|
+
host: url.hostname,
|
|
17
|
+
port: Number(url.port) || 6379,
|
|
18
|
+
...(url.username ? { username: url.username } : {}),
|
|
19
|
+
...(url.password ? { password: url.password } : {}),
|
|
20
|
+
maxRetriesPerRequest: null as null, // Required for BullMQ blocking commands
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function initBuildQueue(): Promise<void> {
|
|
25
|
+
const connection = parseRedisConnection()
|
|
26
|
+
|
|
27
|
+
buildQueue = new Queue(QUEUE_NAME, {
|
|
28
|
+
connection,
|
|
29
|
+
defaultJobOptions: {
|
|
30
|
+
attempts: 1,
|
|
31
|
+
removeOnComplete: 100,
|
|
32
|
+
removeOnFail: 200,
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
await drainLegacyQueue()
|
|
37
|
+
|
|
38
|
+
logger.info({ queue: QUEUE_NAME }, 'BullMQ build queue initialized')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* One-time migration: drain any buildIds remaining in the legacy
|
|
43
|
+
* Redis list into BullMQ. After first deploy this is always a no-op.
|
|
44
|
+
*/
|
|
45
|
+
async function drainLegacyQueue(): Promise<void> {
|
|
46
|
+
try {
|
|
47
|
+
const redis = getRedis()
|
|
48
|
+
let migrated = 0
|
|
49
|
+
|
|
50
|
+
let buildId = await redis.rpop(LEGACY_QUEUE_KEY)
|
|
51
|
+
while (buildId) {
|
|
52
|
+
await enqueueBuild(buildId)
|
|
53
|
+
migrated++
|
|
54
|
+
buildId = await redis.rpop(LEGACY_QUEUE_KEY)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (migrated > 0) {
|
|
58
|
+
logger.info({ migrated }, 'Drained legacy Redis list queue into BullMQ')
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
// Redis is connected at this point (connectRedis ran first); a failure here is unexpected
|
|
62
|
+
logger.error({ err }, 'Failed to drain legacy Redis queue — some builds may have been lost')
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function enqueueBuild(buildId: string): Promise<void> {
|
|
67
|
+
if (!buildQueue) throw new Error('Build queue not initialized')
|
|
68
|
+
await buildQueue.add('build', { buildId }, { jobId: buildId })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function initBuildWorker(): void {
|
|
72
|
+
if (!buildQueue) throw new Error('Build queue must be initialized before worker')
|
|
73
|
+
|
|
74
|
+
const connection = parseRedisConnection()
|
|
75
|
+
const concurrency = env.BUILD_CONCURRENCY
|
|
76
|
+
|
|
77
|
+
buildWorker = new Worker(
|
|
78
|
+
QUEUE_NAME,
|
|
79
|
+
async (job: Job<{ buildId: string }>) => {
|
|
80
|
+
const { buildId } = job.data
|
|
81
|
+
|
|
82
|
+
// Guard: if the build was already marked failed (e.g. by recoverStaleBuilds
|
|
83
|
+
// after a crash) or cancelled, skip re-execution. BullMQ marks this job as
|
|
84
|
+
// completed — intentional, since the build state is managed in MongoDB.
|
|
85
|
+
const builds = getBuildsCollection()
|
|
86
|
+
const build = await builds.findOne({ id: buildId })
|
|
87
|
+
if (!build || build.status === 'failed' || build.status === 'cancelled' || build.status === 'completed') {
|
|
88
|
+
logger.info({ buildId, status: build?.status }, 'Skipping build — already in terminal state')
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
logger.info({ buildId, jobId: job.id, concurrency }, 'Processing build')
|
|
93
|
+
await executeBuild(buildId)
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
connection,
|
|
97
|
+
concurrency,
|
|
98
|
+
lockDuration: 20 * 60 * 1000, // 20 min — generous for slow Docker builds
|
|
99
|
+
lockRenewTime: 5 * 60 * 1000, // renew every 5 min (4x before expiry)
|
|
100
|
+
stalledInterval: 5 * 60 * 1000,
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
buildWorker.on('completed', (job) => {
|
|
105
|
+
logger.info({ jobId: job.id, buildId: job.data.buildId }, 'Build job completed')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
buildWorker.on('failed', (job, err) => {
|
|
109
|
+
logger.error({ jobId: job?.id, buildId: job?.data.buildId, err }, 'Build job failed')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
buildWorker.on('stalled', (jobId) => {
|
|
113
|
+
logger.warn({ jobId }, 'Build job stalled — BullMQ will attempt recovery')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
buildWorker.on('error', (err) => {
|
|
117
|
+
logger.error({ err }, 'BullMQ worker error')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
logger.info({ concurrency, queue: QUEUE_NAME }, 'BullMQ build worker started')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function getBuildQueue(): Queue | null {
|
|
124
|
+
return buildQueue
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function closeBuildQueue(): Promise<void> {
|
|
128
|
+
if (buildWorker) {
|
|
129
|
+
logger.info('Closing BullMQ worker — waiting for active builds to finish...')
|
|
130
|
+
await buildWorker.close()
|
|
131
|
+
logger.info('BullMQ worker closed')
|
|
132
|
+
}
|
|
133
|
+
if (buildQueue) {
|
|
134
|
+
await buildQueue.close()
|
|
135
|
+
logger.info('BullMQ queue closed')
|
|
136
|
+
}
|
|
137
|
+
}
|