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,216 @@
|
|
|
1
|
+
import { env } from '../config/env.js'
|
|
2
|
+
import { logger } from '../utils/logger.js'
|
|
3
|
+
import {
|
|
4
|
+
buildBucketName,
|
|
5
|
+
buildBucketPolicy,
|
|
6
|
+
createMinioAdminClient,
|
|
7
|
+
ensureBucket,
|
|
8
|
+
setBucketQuota,
|
|
9
|
+
getStorageTierConfig,
|
|
10
|
+
DEFAULT_STORAGE_TIER,
|
|
11
|
+
} from './storage-credential-manager.js'
|
|
12
|
+
import {
|
|
13
|
+
isVaultEnabled,
|
|
14
|
+
getMinioAdminCreds,
|
|
15
|
+
configureMinioVaultRole,
|
|
16
|
+
buildMinioCredsPath,
|
|
17
|
+
type VaultAnnotations,
|
|
18
|
+
} from './vault-client.js'
|
|
19
|
+
import type { StorageTier, StorageCredential } from '../models/types.js'
|
|
20
|
+
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
// Types
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export interface StorageSpec {
|
|
26
|
+
readonly name?: string
|
|
27
|
+
readonly tier?: StorageTier
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface StorageProvisionResult {
|
|
31
|
+
readonly envVars: Record<string, string>
|
|
32
|
+
readonly vaultAnnotations: VaultAnnotations
|
|
33
|
+
readonly credential: StorageCredential | null
|
|
34
|
+
readonly gasPerMonth: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
// Provisioning
|
|
39
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Calculate total storage gas cost per month for deploy gate.
|
|
43
|
+
*/
|
|
44
|
+
export function calculateStorageGasPerMonth(
|
|
45
|
+
storageSpecs: ReadonlyArray<StorageSpec>,
|
|
46
|
+
): number {
|
|
47
|
+
return storageSpecs.reduce((sum, spec) => {
|
|
48
|
+
const tier = spec.tier || DEFAULT_STORAGE_TIER
|
|
49
|
+
const tierConfig = getStorageTierConfig(tier)
|
|
50
|
+
return sum + tierConfig.gasPerMonth
|
|
51
|
+
}, 0)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Provision object storage (MinIO) for a service.
|
|
56
|
+
*
|
|
57
|
+
* Requires Vault — throws if Vault is not enabled (no static credential fallback).
|
|
58
|
+
*
|
|
59
|
+
* Flow:
|
|
60
|
+
* 1. Read MinIO admin creds from Vault KV
|
|
61
|
+
* 2. Create buckets via MinIO SDK (idempotent)
|
|
62
|
+
* 3. Set hard quotas per bucket via mc CLI
|
|
63
|
+
* 4. Build scoped IAM policy for all declared buckets
|
|
64
|
+
* 5. Create MinIO user + policy via mc CLI (admin operations)
|
|
65
|
+
* 6. Store per-service creds in Vault KV
|
|
66
|
+
* 7. Return Vault Agent annotations for pod credential injection
|
|
67
|
+
*/
|
|
68
|
+
export async function provisionStorage(
|
|
69
|
+
serviceName: string,
|
|
70
|
+
environment: string,
|
|
71
|
+
namespace: string,
|
|
72
|
+
storageSpecs: ReadonlyArray<StorageSpec>,
|
|
73
|
+
): Promise<StorageProvisionResult> {
|
|
74
|
+
if (storageSpecs.length === 0) {
|
|
75
|
+
return { envVars: {}, vaultAnnotations: {}, credential: null, gasPerMonth: 0 }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Storage requires Vault — no static credential fallback
|
|
79
|
+
if (!isVaultEnabled()) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
'Storage provisioning requires Vault. Set VAULT_ENABLED=true and configure ' +
|
|
82
|
+
'the MinIO secrets engine in Vault before declaring spec.storage in catalog-info.yaml.'
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Read MinIO admin credentials from Vault KV (never stored on disk)
|
|
87
|
+
const adminCreds = await getMinioAdminCreds()
|
|
88
|
+
|
|
89
|
+
// Create MinIO S3 client for bucket management
|
|
90
|
+
const client = createMinioAdminClient(adminCreds.accessKey, adminCreds.secretKey)
|
|
91
|
+
|
|
92
|
+
// Build bucket names and tiers
|
|
93
|
+
const bucketEntries = storageSpecs.map((spec) => ({
|
|
94
|
+
name: buildBucketName(serviceName, environment, spec.name),
|
|
95
|
+
tier: spec.tier || DEFAULT_STORAGE_TIER,
|
|
96
|
+
suffix: spec.name,
|
|
97
|
+
}))
|
|
98
|
+
|
|
99
|
+
// Create buckets (via MinIO SDK) and set quotas (via mc CLI)
|
|
100
|
+
for (const entry of bucketEntries) {
|
|
101
|
+
await ensureBucket(client, entry.name)
|
|
102
|
+
await setBucketQuota(entry.name, entry.tier, adminCreds.accessKey, adminCreds.secretKey)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const bucketNames = bucketEntries.map((e) => e.name)
|
|
106
|
+
|
|
107
|
+
// Build scoped IAM policy for all declared buckets
|
|
108
|
+
const policyDocument = buildBucketPolicy(bucketNames)
|
|
109
|
+
|
|
110
|
+
// Configure Vault MinIO role with inline policy
|
|
111
|
+
// When pods read from minio/creds/{role}, the plugin creates an ephemeral
|
|
112
|
+
// MinIO user with this policy attached (dynamic credentials, lease-based)
|
|
113
|
+
await configureMinioVaultRole(serviceName, environment, JSON.stringify(policyDocument))
|
|
114
|
+
|
|
115
|
+
// Build Vault Agent annotations for pod injection
|
|
116
|
+
const vaultAnnotations = buildStorageVaultAnnotations(
|
|
117
|
+
serviceName,
|
|
118
|
+
environment,
|
|
119
|
+
bucketEntries,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
// Build credential record
|
|
123
|
+
const credsPath = buildMinioCredsPath(serviceName, environment)
|
|
124
|
+
const credential: StorageCredential = {
|
|
125
|
+
type: 'minio',
|
|
126
|
+
vaultLeasePath: credsPath,
|
|
127
|
+
buckets: bucketNames,
|
|
128
|
+
tiers: bucketEntries.map((e) => e.tier),
|
|
129
|
+
environment: environment as StorageCredential['environment'],
|
|
130
|
+
provisionedAt: new Date().toISOString(),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const gasPerMonth = calculateStorageGasPerMonth(storageSpecs)
|
|
134
|
+
|
|
135
|
+
logger.info(
|
|
136
|
+
{ serviceName, environment, buckets: bucketNames, gasPerMonth },
|
|
137
|
+
'Storage provisioned via Vault KV',
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
envVars: {}, // Credentials injected via Vault Agent, not env vars
|
|
142
|
+
vaultAnnotations,
|
|
143
|
+
credential,
|
|
144
|
+
gasPerMonth,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
149
|
+
// Vault Agent Annotations for Storage
|
|
150
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
interface BucketEntry {
|
|
153
|
+
readonly name: string
|
|
154
|
+
readonly tier: StorageTier
|
|
155
|
+
readonly suffix?: string
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build Vault Agent pod annotations that inject S3 credentials into the pod.
|
|
160
|
+
*
|
|
161
|
+
* The Vault Agent Injector reads these annotations and:
|
|
162
|
+
* 1. Injects an init container that fetches credentials from the MinIO secrets engine
|
|
163
|
+
* 2. Writes credentials to /vault/secrets/storage as env-file format
|
|
164
|
+
* 3. Sidecar renews the lease and rotates creds automatically
|
|
165
|
+
*
|
|
166
|
+
* Credentials are dynamic — Vault creates ephemeral MinIO users on each read,
|
|
167
|
+
* and revokes them when the lease expires.
|
|
168
|
+
*/
|
|
169
|
+
function buildStorageVaultAnnotations(
|
|
170
|
+
serviceName: string,
|
|
171
|
+
environment: string,
|
|
172
|
+
bucketEntries: readonly BucketEntry[],
|
|
173
|
+
): VaultAnnotations {
|
|
174
|
+
const endpoint = `http://${env.MINIO_HOST}:${env.MINIO_PORT}`
|
|
175
|
+
const credsPath = buildMinioCredsPath(serviceName, environment)
|
|
176
|
+
|
|
177
|
+
// Build the template that renders S3 credentials as env vars
|
|
178
|
+
// Always emit S3_{SUFFIX}_BUCKET (for createStorage('name')),
|
|
179
|
+
// plus S3_BUCKET alias for single-bucket services (for createStorage())
|
|
180
|
+
const namedLines = bucketEntries
|
|
181
|
+
.map((e) => {
|
|
182
|
+
const envSuffix = (e.suffix || 'default').toUpperCase().replace(/-/g, '_')
|
|
183
|
+
return `S3_${envSuffix}_BUCKET=${e.name}`
|
|
184
|
+
})
|
|
185
|
+
.join('\n')
|
|
186
|
+
const bucketLines = bucketEntries.length === 1
|
|
187
|
+
? `S3_BUCKET=${bucketEntries[0].name}\n${namedLines}`
|
|
188
|
+
: namedLines
|
|
189
|
+
|
|
190
|
+
// Secrets engine data is at .Data (not .Data.data like KV v2)
|
|
191
|
+
// Inject both split (S3_HOST/S3_PORT) and combined (S3_ENDPOINT) for SDK compatibility:
|
|
192
|
+
// MinIO SDK: new Client({ endPoint: S3_HOST, port: +S3_PORT, ... })
|
|
193
|
+
// AWS SDK: new S3Client({ endpoint: S3_ENDPOINT, ... })
|
|
194
|
+
const template = [
|
|
195
|
+
`{{- with secret "${credsPath}" -}}`,
|
|
196
|
+
`S3_HOST=${env.MINIO_HOST}`,
|
|
197
|
+
`S3_PORT=${env.MINIO_PORT}`,
|
|
198
|
+
`S3_ENDPOINT=${endpoint}`,
|
|
199
|
+
`S3_ACCESS_KEY_ID={{ .Data.access_key_id }}`,
|
|
200
|
+
`S3_SECRET_ACCESS_KEY={{ .Data.secret_access_key }}`,
|
|
201
|
+
`S3_REGION=us-east-1`,
|
|
202
|
+
bucketLines,
|
|
203
|
+
'{{- end -}}',
|
|
204
|
+
].join('\n')
|
|
205
|
+
|
|
206
|
+
// Storage annotations are keyed with "storage" suffix to avoid collisions
|
|
207
|
+
// with database annotations (which use "mongodb", "redis", "neo4j" suffixes).
|
|
208
|
+
// The command annotation triggers on credential rotation — it removes the
|
|
209
|
+
// liveness signal file so K8s restarts the pod with fresh creds (same
|
|
210
|
+
// pattern as database annotations in vault-client.ts).
|
|
211
|
+
return {
|
|
212
|
+
'vault.hashicorp.com/agent-inject-secret-storage': credsPath,
|
|
213
|
+
'vault.hashicorp.com/agent-inject-template-storage': template,
|
|
214
|
+
'vault.hashicorp.com/agent-inject-command-storage': 'rm -f /vault/signals/alive',
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { MongoClient, GridFSBucket, ObjectId, Db, GridFSFile } from 'mongodb'
|
|
2
|
+
import { Readable } from 'stream'
|
|
3
|
+
import { createHash } from 'crypto'
|
|
4
|
+
import { logger } from '../utils/logger.js'
|
|
5
|
+
|
|
6
|
+
let gridfs: GridFSBucket | null = null
|
|
7
|
+
let gridfsDb: Db | null = null
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Initialize GridFS bucket for tarball storage
|
|
11
|
+
*/
|
|
12
|
+
export function initGridFS(client: MongoClient, dbName?: string): void {
|
|
13
|
+
gridfsDb = client.db(dbName)
|
|
14
|
+
gridfs = new GridFSBucket(gridfsDb, {
|
|
15
|
+
bucketName: 'tarballs',
|
|
16
|
+
chunkSizeBytes: 1024 * 1024, // 1MB chunks for large files
|
|
17
|
+
})
|
|
18
|
+
logger.info('GridFS initialized for tarball storage')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the GridFS bucket instance
|
|
23
|
+
*/
|
|
24
|
+
export function getGridFS(): GridFSBucket {
|
|
25
|
+
if (!gridfs) {
|
|
26
|
+
throw new Error('GridFS not initialized. Call initGridFS first.')
|
|
27
|
+
}
|
|
28
|
+
return gridfs
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getGridFSDb(): Db {
|
|
32
|
+
if (!gridfsDb) {
|
|
33
|
+
throw new Error('GridFS not initialized. Call initGridFS first.')
|
|
34
|
+
}
|
|
35
|
+
return gridfsDb
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TarballMetadata {
|
|
39
|
+
serviceId: string
|
|
40
|
+
serviceName: string
|
|
41
|
+
version: string
|
|
42
|
+
commitSha: string
|
|
43
|
+
environment: string
|
|
44
|
+
uploadedBy: string
|
|
45
|
+
uploadedAt: string
|
|
46
|
+
size: number
|
|
47
|
+
checksum: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface StoredTarball {
|
|
51
|
+
id: string
|
|
52
|
+
filename: string
|
|
53
|
+
metadata: TarballMetadata
|
|
54
|
+
length: number
|
|
55
|
+
uploadDate: Date
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Upload a tarball to GridFS
|
|
60
|
+
*/
|
|
61
|
+
export async function uploadTarball(
|
|
62
|
+
stream: Readable,
|
|
63
|
+
filename: string,
|
|
64
|
+
metadata: Omit<TarballMetadata, 'uploadedAt' | 'size' | 'checksum'>
|
|
65
|
+
): Promise<StoredTarball> {
|
|
66
|
+
const bucket = getGridFS()
|
|
67
|
+
const db = getGridFSDb()
|
|
68
|
+
|
|
69
|
+
// Calculate checksum while uploading
|
|
70
|
+
const hash = createHash('sha256')
|
|
71
|
+
let size = 0
|
|
72
|
+
|
|
73
|
+
const checksumStream = new Readable({
|
|
74
|
+
read() {}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
stream.on('data', (chunk: Buffer) => {
|
|
78
|
+
hash.update(chunk)
|
|
79
|
+
size += chunk.length
|
|
80
|
+
checksumStream.push(chunk)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
stream.on('end', () => {
|
|
84
|
+
checksumStream.push(null)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
stream.on('error', (err) => {
|
|
88
|
+
checksumStream.destroy(err)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const fullMetadata: TarballMetadata = {
|
|
92
|
+
...metadata,
|
|
93
|
+
uploadedAt: new Date().toISOString(),
|
|
94
|
+
size: 0,
|
|
95
|
+
checksum: '',
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const uploadStream = bucket.openUploadStream(filename, {
|
|
99
|
+
metadata: fullMetadata,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
checksumStream.pipe(uploadStream)
|
|
104
|
+
|
|
105
|
+
uploadStream.on('finish', async () => {
|
|
106
|
+
// Update metadata with final size and checksum
|
|
107
|
+
const checksum = hash.digest('hex')
|
|
108
|
+
fullMetadata.size = size
|
|
109
|
+
fullMetadata.checksum = checksum
|
|
110
|
+
|
|
111
|
+
// Update the file's metadata
|
|
112
|
+
await db.collection('tarballs.files').updateOne(
|
|
113
|
+
{ _id: uploadStream.id },
|
|
114
|
+
{ $set: { 'metadata.size': size, 'metadata.checksum': checksum } }
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
logger.info({ filename, size, checksum }, 'Tarball uploaded to GridFS')
|
|
118
|
+
|
|
119
|
+
resolve({
|
|
120
|
+
id: uploadStream.id.toString(),
|
|
121
|
+
filename,
|
|
122
|
+
metadata: fullMetadata,
|
|
123
|
+
length: size,
|
|
124
|
+
uploadDate: new Date(),
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
uploadStream.on('error', (error) => {
|
|
129
|
+
logger.error({ filename, error }, 'Failed to upload tarball')
|
|
130
|
+
reject(error)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Download a tarball from GridFS
|
|
137
|
+
*/
|
|
138
|
+
export function downloadTarball(fileId: string): Readable {
|
|
139
|
+
const bucket = getGridFS()
|
|
140
|
+
return bucket.openDownloadStream(new ObjectId(fileId))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Download a tarball by filename
|
|
145
|
+
*/
|
|
146
|
+
export function downloadTarballByFilename(filename: string): Readable {
|
|
147
|
+
const bucket = getGridFS()
|
|
148
|
+
return bucket.openDownloadStreamByName(filename)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* List all tarballs for a service
|
|
153
|
+
*/
|
|
154
|
+
export async function listServiceTarballs(serviceId: string): Promise<StoredTarball[]> {
|
|
155
|
+
const db = getGridFSDb()
|
|
156
|
+
|
|
157
|
+
const files = await db
|
|
158
|
+
.collection<GridFSFile>('tarballs.files')
|
|
159
|
+
.find({ 'metadata.serviceId': serviceId })
|
|
160
|
+
.sort({ uploadDate: -1 })
|
|
161
|
+
.toArray()
|
|
162
|
+
|
|
163
|
+
return files.map((file) => ({
|
|
164
|
+
id: file._id.toString(),
|
|
165
|
+
filename: file.filename,
|
|
166
|
+
metadata: file.metadata as unknown as TarballMetadata,
|
|
167
|
+
length: file.length,
|
|
168
|
+
uploadDate: file.uploadDate,
|
|
169
|
+
}))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get a specific tarball by ID
|
|
174
|
+
*/
|
|
175
|
+
export async function getTarball(fileId: string): Promise<StoredTarball | null> {
|
|
176
|
+
const db = getGridFSDb()
|
|
177
|
+
|
|
178
|
+
const file = await db.collection<GridFSFile>('tarballs.files').findOne({ _id: new ObjectId(fileId) })
|
|
179
|
+
|
|
180
|
+
if (!file) return null
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
id: file._id.toString(),
|
|
184
|
+
filename: file.filename,
|
|
185
|
+
metadata: file.metadata as unknown as TarballMetadata,
|
|
186
|
+
length: file.length,
|
|
187
|
+
uploadDate: file.uploadDate,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get the latest tarball for a service
|
|
193
|
+
*/
|
|
194
|
+
export async function getLatestTarball(serviceId: string): Promise<StoredTarball | null> {
|
|
195
|
+
const db = getGridFSDb()
|
|
196
|
+
|
|
197
|
+
const file = await db
|
|
198
|
+
.collection<GridFSFile>('tarballs.files')
|
|
199
|
+
.findOne(
|
|
200
|
+
{ 'metadata.serviceId': serviceId },
|
|
201
|
+
{ sort: { uploadDate: -1 } }
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if (!file) return null
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
id: file._id.toString(),
|
|
208
|
+
filename: file.filename,
|
|
209
|
+
metadata: file.metadata as unknown as TarballMetadata,
|
|
210
|
+
length: file.length,
|
|
211
|
+
uploadDate: file.uploadDate,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Delete a tarball
|
|
217
|
+
*/
|
|
218
|
+
export async function deleteTarball(fileId: string): Promise<void> {
|
|
219
|
+
const bucket = getGridFS()
|
|
220
|
+
await bucket.delete(new ObjectId(fileId))
|
|
221
|
+
logger.info({ fileId }, 'Tarball deleted from GridFS')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Delete all tarballs for a service (cleanup)
|
|
226
|
+
*/
|
|
227
|
+
export async function deleteServiceTarballs(serviceId: string): Promise<number> {
|
|
228
|
+
const bucket = getGridFS()
|
|
229
|
+
const db = getGridFSDb()
|
|
230
|
+
|
|
231
|
+
const files = await db
|
|
232
|
+
.collection<GridFSFile>('tarballs.files')
|
|
233
|
+
.find({ 'metadata.serviceId': serviceId })
|
|
234
|
+
.toArray()
|
|
235
|
+
|
|
236
|
+
for (const file of files) {
|
|
237
|
+
await bucket.delete(file._id)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
logger.info({ serviceId, count: files.length }, 'Service tarballs deleted')
|
|
241
|
+
return files.length
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Extract tarball to a directory
|
|
246
|
+
*/
|
|
247
|
+
export async function extractTarball(fileId: string, destDir: string): Promise<void> {
|
|
248
|
+
const { pipeline } = await import('stream/promises')
|
|
249
|
+
const { createWriteStream } = await import('fs')
|
|
250
|
+
const { mkdir } = await import('fs/promises')
|
|
251
|
+
const { exec } = await import('child_process')
|
|
252
|
+
const { promisify } = await import('util')
|
|
253
|
+
const { join } = await import('path')
|
|
254
|
+
|
|
255
|
+
const execAsync = promisify(exec)
|
|
256
|
+
const tarPath = join(destDir, 'source.tar.gz')
|
|
257
|
+
|
|
258
|
+
// Ensure destination exists
|
|
259
|
+
await mkdir(destDir, { recursive: true })
|
|
260
|
+
|
|
261
|
+
// Download tarball to temp file
|
|
262
|
+
const downloadStream = downloadTarball(fileId)
|
|
263
|
+
const writeStream = createWriteStream(tarPath)
|
|
264
|
+
await pipeline(downloadStream, writeStream)
|
|
265
|
+
|
|
266
|
+
// Extract tarball
|
|
267
|
+
await execAsync(`tar -xzf "${tarPath}" -C "${destDir}"`)
|
|
268
|
+
|
|
269
|
+
// Remove tarball after extraction
|
|
270
|
+
const { unlink } = await import('fs/promises')
|
|
271
|
+
await unlink(tarPath)
|
|
272
|
+
|
|
273
|
+
logger.info({ fileId, destDir }, 'Tarball extracted')
|
|
274
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { getPods } from './pod-diagnostics.js'
|
|
2
|
+
import type { PodDiagnostic } from '../models/types.js'
|
|
3
|
+
|
|
4
|
+
export type IssueSeverity = 'critical' | 'warning' | 'info'
|
|
5
|
+
|
|
6
|
+
export interface TroubleshootIssue {
|
|
7
|
+
readonly severity: IssueSeverity
|
|
8
|
+
readonly category: string
|
|
9
|
+
readonly title: string
|
|
10
|
+
readonly detail: string
|
|
11
|
+
readonly suggestion: string
|
|
12
|
+
readonly podName?: string
|
|
13
|
+
readonly containerName?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TroubleshootResult {
|
|
17
|
+
readonly service: string
|
|
18
|
+
readonly namespace: string
|
|
19
|
+
readonly environment: string
|
|
20
|
+
readonly analyzedAt: string
|
|
21
|
+
readonly healthy: boolean
|
|
22
|
+
readonly issues: ReadonlyArray<TroubleshootIssue>
|
|
23
|
+
readonly pods: ReadonlyArray<PodDiagnostic>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface FailurePattern {
|
|
27
|
+
readonly name: string
|
|
28
|
+
readonly detect: (pod: PodDiagnostic) => TroubleshootIssue | null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const FAILURE_PATTERNS: ReadonlyArray<FailurePattern> = [
|
|
32
|
+
{
|
|
33
|
+
name: 'crash-loop',
|
|
34
|
+
detect: (pod) => {
|
|
35
|
+
const crashing = [...pod.containerStatuses, ...(pod.initContainerStatuses ?? [])]
|
|
36
|
+
.find((c) => c.reason === 'CrashLoopBackOff')
|
|
37
|
+
if (!crashing) return null
|
|
38
|
+
return {
|
|
39
|
+
severity: 'critical',
|
|
40
|
+
category: 'crash',
|
|
41
|
+
title: 'CrashLoopBackOff',
|
|
42
|
+
detail: `Container "${crashing.name}" is crash-looping (${pod.restartCount} restarts). Last exit code: ${crashing.exitCode ?? 'unknown'}.`,
|
|
43
|
+
suggestion: 'Check container logs for startup errors. Common causes: missing environment variable, failed database connection on startup, uncaught exception in entry point. Run `tawa logs <service>` to see the crash output.',
|
|
44
|
+
podName: pod.name,
|
|
45
|
+
containerName: crashing.name,
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'image-pull',
|
|
51
|
+
detect: (pod) => {
|
|
52
|
+
const failing = pod.containerStatuses.find(
|
|
53
|
+
(c) => c.reason === 'ImagePullBackOff' || c.reason === 'ErrImagePull'
|
|
54
|
+
)
|
|
55
|
+
if (!failing) return null
|
|
56
|
+
return {
|
|
57
|
+
severity: 'critical',
|
|
58
|
+
category: 'image',
|
|
59
|
+
title: 'ImagePullBackOff',
|
|
60
|
+
detail: `Container "${failing.name}" cannot pull its image. ${failing.message ?? ''}`.trim(),
|
|
61
|
+
suggestion: 'Verify the image exists in the registry. Check that imagePullSecrets are configured in the namespace. The builder pushes to registry.insureco.io/insureco.',
|
|
62
|
+
podName: pod.name,
|
|
63
|
+
containerName: failing.name,
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'oom-killed',
|
|
69
|
+
detect: (pod) => {
|
|
70
|
+
const oom = pod.containerStatuses.find((c) => c.reason === 'OOMKilled')
|
|
71
|
+
if (!oom) return null
|
|
72
|
+
return {
|
|
73
|
+
severity: 'critical',
|
|
74
|
+
category: 'resources',
|
|
75
|
+
title: 'OOMKilled',
|
|
76
|
+
detail: `Container "${oom.name}" was killed due to exceeding memory limits.`,
|
|
77
|
+
suggestion: 'Increase pod-tier in catalog-info.yaml (e.g., from "nano" to "small"). Each tier doubles the available memory.',
|
|
78
|
+
podName: pod.name,
|
|
79
|
+
containerName: oom.name,
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'vault-init-failure',
|
|
85
|
+
detect: (pod) => {
|
|
86
|
+
const vaultInit = (pod.initContainerStatuses ?? []).find(
|
|
87
|
+
(c) => c.name.includes('vault') && !c.ready
|
|
88
|
+
)
|
|
89
|
+
if (!vaultInit) return null
|
|
90
|
+
return {
|
|
91
|
+
severity: 'critical',
|
|
92
|
+
category: 'vault',
|
|
93
|
+
title: 'Vault Init Container Failed',
|
|
94
|
+
detail: `Init container "${vaultInit.name}" is not ready. State: ${vaultInit.state}. ${vaultInit.message ?? ''}`.trim(),
|
|
95
|
+
suggestion: 'Vault Agent could not authenticate or fetch secrets. Check: (1) ServiceAccount exists in namespace, (2) Vault K8s auth role is configured, (3) Vault is reachable from the cluster. Re-deploying usually fixes transient Vault issues.',
|
|
96
|
+
podName: pod.name,
|
|
97
|
+
containerName: vaultInit.name,
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'config-error',
|
|
103
|
+
detect: (pod) => {
|
|
104
|
+
const configErr = pod.containerStatuses.find(
|
|
105
|
+
(c) => c.reason === 'CreateContainerConfigError'
|
|
106
|
+
)
|
|
107
|
+
if (!configErr) return null
|
|
108
|
+
return {
|
|
109
|
+
severity: 'critical',
|
|
110
|
+
category: 'config',
|
|
111
|
+
title: 'Missing ConfigMap or Secret',
|
|
112
|
+
detail: `Container "${configErr.name}" cannot start because a required ConfigMap or Secret is missing. ${configErr.message ?? ''}`.trim(),
|
|
113
|
+
suggestion: 'Check that all referenced secrets exist in the namespace. If using managed secrets, ensure `tawa config set --secret` was run and a deploy has occurred since.',
|
|
114
|
+
podName: pod.name,
|
|
115
|
+
containerName: configErr.name,
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'pending-scheduling',
|
|
121
|
+
detect: (pod) => {
|
|
122
|
+
if (pod.phase !== 'Pending') return null
|
|
123
|
+
const schedEvent = pod.events.find((e) => e.reason === 'FailedScheduling')
|
|
124
|
+
if (!schedEvent) return null
|
|
125
|
+
return {
|
|
126
|
+
severity: 'warning',
|
|
127
|
+
category: 'scheduling',
|
|
128
|
+
title: 'Pod Pending (FailedScheduling)',
|
|
129
|
+
detail: `Pod is pending scheduling. ${schedEvent?.message ?? 'Cluster may lack resources.'}`,
|
|
130
|
+
suggestion: 'The cluster may not have enough CPU/memory to schedule this pod. Try a smaller pod-tier or wait for other pods to finish.',
|
|
131
|
+
podName: pod.name,
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'high-restarts',
|
|
137
|
+
detect: (pod) => {
|
|
138
|
+
if (pod.restartCount < 3) return null
|
|
139
|
+
const hasCrashLoop = pod.containerStatuses.some((c) => c.reason === 'CrashLoopBackOff')
|
|
140
|
+
if (hasCrashLoop) return null
|
|
141
|
+
return {
|
|
142
|
+
severity: 'warning',
|
|
143
|
+
category: 'stability',
|
|
144
|
+
title: 'High Restart Count',
|
|
145
|
+
detail: `Pod has restarted ${pod.restartCount} times. The container may be intermittently crashing.`,
|
|
146
|
+
suggestion: 'Check container logs (including --previous) for intermittent failures. Common causes: connection timeouts to databases, health check failures, OOM spikes.',
|
|
147
|
+
podName: pod.name,
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
function detectIssuesForPod(pod: PodDiagnostic): ReadonlyArray<TroubleshootIssue> {
|
|
154
|
+
const issues: TroubleshootIssue[] = []
|
|
155
|
+
for (const pattern of FAILURE_PATTERNS) {
|
|
156
|
+
const issue = pattern.detect(pod)
|
|
157
|
+
if (issue) {
|
|
158
|
+
issues.push(issue)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return issues
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const SEVERITY_ORDER: Record<IssueSeverity, number> = {
|
|
165
|
+
critical: 0,
|
|
166
|
+
warning: 1,
|
|
167
|
+
info: 2,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function troubleshoot(
|
|
171
|
+
serviceName: string,
|
|
172
|
+
namespace: string,
|
|
173
|
+
environment: string
|
|
174
|
+
): Promise<TroubleshootResult> {
|
|
175
|
+
const pods = await getPods(namespace, serviceName)
|
|
176
|
+
|
|
177
|
+
const issues: TroubleshootIssue[] = []
|
|
178
|
+
|
|
179
|
+
if (pods.length === 0) {
|
|
180
|
+
issues.push({
|
|
181
|
+
severity: 'critical',
|
|
182
|
+
category: 'deployment',
|
|
183
|
+
title: 'No Pods Found',
|
|
184
|
+
detail: `No pods found in namespace "${namespace}" for service "${serviceName}".`,
|
|
185
|
+
suggestion: 'The deployment may not have created any pods. Check that `tawa deploy` completed successfully and that the Helm release exists.',
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const pod of pods) {
|
|
190
|
+
issues.push(...detectIssuesForPod(pod))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const sorted = [...issues].sort(
|
|
194
|
+
(a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
const hasCritical = sorted.some((i) => i.severity === 'critical')
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
service: serviceName,
|
|
201
|
+
namespace,
|
|
202
|
+
environment,
|
|
203
|
+
analyzedAt: new Date().toISOString(),
|
|
204
|
+
healthy: !hasCritical,
|
|
205
|
+
issues: sorted,
|
|
206
|
+
pods,
|
|
207
|
+
}
|
|
208
|
+
}
|