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.
Files changed (337) hide show
  1. package/.claude/settings.local.json +111 -0
  2. package/.iec.yaml +5 -0
  3. package/CLAUDE.md +174 -0
  4. package/Dockerfile +34 -0
  5. package/README.md +84 -0
  6. package/catalog-info.yaml +11 -0
  7. package/dist/config/env.d.ts +219 -0
  8. package/dist/config/env.d.ts.map +1 -0
  9. package/dist/config/env.js +89 -0
  10. package/dist/config/env.js.map +1 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +148 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/middleware/auth.d.ts +43 -0
  16. package/dist/middleware/auth.d.ts.map +1 -0
  17. package/dist/middleware/auth.js +217 -0
  18. package/dist/middleware/auth.js.map +1 -0
  19. package/dist/middleware/org-access.d.ts +28 -0
  20. package/dist/middleware/org-access.d.ts.map +1 -0
  21. package/dist/middleware/org-access.js +102 -0
  22. package/dist/middleware/org-access.js.map +1 -0
  23. package/dist/models/types.d.ts +254 -0
  24. package/dist/models/types.d.ts.map +1 -0
  25. package/dist/models/types.js +2 -0
  26. package/dist/models/types.js.map +1 -0
  27. package/dist/routes/ai.d.ts +2 -0
  28. package/dist/routes/ai.d.ts.map +1 -0
  29. package/dist/routes/ai.js +77 -0
  30. package/dist/routes/ai.js.map +1 -0
  31. package/dist/routes/audit.d.ts +2 -0
  32. package/dist/routes/audit.d.ts.map +1 -0
  33. package/dist/routes/audit.js +102 -0
  34. package/dist/routes/audit.js.map +1 -0
  35. package/dist/routes/builds.d.ts +2 -0
  36. package/dist/routes/builds.d.ts.map +1 -0
  37. package/dist/routes/builds.js +262 -0
  38. package/dist/routes/builds.js.map +1 -0
  39. package/dist/routes/cluster.d.ts +2 -0
  40. package/dist/routes/cluster.d.ts.map +1 -0
  41. package/dist/routes/cluster.js +181 -0
  42. package/dist/routes/cluster.js.map +1 -0
  43. package/dist/routes/config.d.ts +2 -0
  44. package/dist/routes/config.d.ts.map +1 -0
  45. package/dist/routes/config.js +291 -0
  46. package/dist/routes/config.js.map +1 -0
  47. package/dist/routes/databases.d.ts +2 -0
  48. package/dist/routes/databases.d.ts.map +1 -0
  49. package/dist/routes/databases.js +161 -0
  50. package/dist/routes/databases.js.map +1 -0
  51. package/dist/routes/db-whitelist.d.ts +2 -0
  52. package/dist/routes/db-whitelist.d.ts.map +1 -0
  53. package/dist/routes/db-whitelist.js +148 -0
  54. package/dist/routes/db-whitelist.js.map +1 -0
  55. package/dist/routes/domains.d.ts +2 -0
  56. package/dist/routes/domains.d.ts.map +1 -0
  57. package/dist/routes/domains.js +449 -0
  58. package/dist/routes/domains.js.map +1 -0
  59. package/dist/routes/oauth.d.ts +2 -0
  60. package/dist/routes/oauth.d.ts.map +1 -0
  61. package/dist/routes/oauth.js +180 -0
  62. package/dist/routes/oauth.js.map +1 -0
  63. package/dist/routes/observability.d.ts +2 -0
  64. package/dist/routes/observability.d.ts.map +1 -0
  65. package/dist/routes/observability.js +167 -0
  66. package/dist/routes/observability.js.map +1 -0
  67. package/dist/routes/orgs.d.ts +2 -0
  68. package/dist/routes/orgs.d.ts.map +1 -0
  69. package/dist/routes/orgs.js +270 -0
  70. package/dist/routes/orgs.js.map +1 -0
  71. package/dist/routes/platform.d.ts +2 -0
  72. package/dist/routes/platform.d.ts.map +1 -0
  73. package/dist/routes/platform.js +107 -0
  74. package/dist/routes/platform.js.map +1 -0
  75. package/dist/routes/push.d.ts +2 -0
  76. package/dist/routes/push.d.ts.map +1 -0
  77. package/dist/routes/push.js +233 -0
  78. package/dist/routes/push.js.map +1 -0
  79. package/dist/routes/rotation.d.ts +3 -0
  80. package/dist/routes/rotation.d.ts.map +1 -0
  81. package/dist/routes/rotation.js +154 -0
  82. package/dist/routes/rotation.js.map +1 -0
  83. package/dist/routes/services.d.ts +2 -0
  84. package/dist/routes/services.d.ts.map +1 -0
  85. package/dist/routes/services.js +246 -0
  86. package/dist/routes/services.js.map +1 -0
  87. package/dist/routes/storage.d.ts +2 -0
  88. package/dist/routes/storage.d.ts.map +1 -0
  89. package/dist/routes/storage.js +118 -0
  90. package/dist/routes/storage.js.map +1 -0
  91. package/dist/routes/users.d.ts +2 -0
  92. package/dist/routes/users.d.ts.map +1 -0
  93. package/dist/routes/users.js +183 -0
  94. package/dist/routes/users.js.map +1 -0
  95. package/dist/routes/versions.d.ts +2 -0
  96. package/dist/routes/versions.d.ts.map +1 -0
  97. package/dist/routes/versions.js +195 -0
  98. package/dist/routes/versions.js.map +1 -0
  99. package/dist/routes/webhooks.d.ts +2 -0
  100. package/dist/routes/webhooks.d.ts.map +1 -0
  101. package/dist/routes/webhooks.js +334 -0
  102. package/dist/routes/webhooks.js.map +1 -0
  103. package/dist/services/__tests__/deploy-pipeline.integration.test.d.ts +2 -0
  104. package/dist/services/__tests__/deploy-pipeline.integration.test.d.ts.map +1 -0
  105. package/dist/services/__tests__/deploy-pipeline.integration.test.js +482 -0
  106. package/dist/services/__tests__/deploy-pipeline.integration.test.js.map +1 -0
  107. package/dist/services/bio-client.d.ts +68 -0
  108. package/dist/services/bio-client.d.ts.map +1 -0
  109. package/dist/services/bio-client.js +110 -0
  110. package/dist/services/bio-client.js.map +1 -0
  111. package/dist/services/build-queue.d.ts +7 -0
  112. package/dist/services/build-queue.d.ts.map +1 -0
  113. package/dist/services/build-queue.js +114 -0
  114. package/dist/services/build-queue.js.map +1 -0
  115. package/dist/services/builder.d.ts +7 -0
  116. package/dist/services/builder.d.ts.map +1 -0
  117. package/dist/services/builder.js +1384 -0
  118. package/dist/services/builder.js.map +1 -0
  119. package/dist/services/catalog.d.ts +177 -0
  120. package/dist/services/catalog.d.ts.map +1 -0
  121. package/dist/services/catalog.js +805 -0
  122. package/dist/services/catalog.js.map +1 -0
  123. package/dist/services/catalog.test.d.ts +2 -0
  124. package/dist/services/catalog.test.d.ts.map +1 -0
  125. package/dist/services/catalog.test.js +467 -0
  126. package/dist/services/catalog.test.js.map +1 -0
  127. package/dist/services/cloudflare.d.ts +43 -0
  128. package/dist/services/cloudflare.d.ts.map +1 -0
  129. package/dist/services/cloudflare.js +182 -0
  130. package/dist/services/cloudflare.js.map +1 -0
  131. package/dist/services/config-validator.d.ts +28 -0
  132. package/dist/services/config-validator.d.ts.map +1 -0
  133. package/dist/services/config-validator.js +68 -0
  134. package/dist/services/config-validator.js.map +1 -0
  135. package/dist/services/config-validator.test.d.ts +2 -0
  136. package/dist/services/config-validator.test.d.ts.map +1 -0
  137. package/dist/services/config-validator.test.js +151 -0
  138. package/dist/services/config-validator.test.js.map +1 -0
  139. package/dist/services/crypto.d.ts +19 -0
  140. package/dist/services/crypto.d.ts.map +1 -0
  141. package/dist/services/crypto.js +63 -0
  142. package/dist/services/crypto.js.map +1 -0
  143. package/dist/services/database.d.ts +26 -0
  144. package/dist/services/database.d.ts.map +1 -0
  145. package/dist/services/database.js +100 -0
  146. package/dist/services/database.js.map +1 -0
  147. package/dist/services/db-credential-manager.d.ts +73 -0
  148. package/dist/services/db-credential-manager.d.ts.map +1 -0
  149. package/dist/services/db-credential-manager.js +342 -0
  150. package/dist/services/db-credential-manager.js.map +1 -0
  151. package/dist/services/db-provisioner.d.ts +57 -0
  152. package/dist/services/db-provisioner.d.ts.map +1 -0
  153. package/dist/services/db-provisioner.js +400 -0
  154. package/dist/services/db-provisioner.js.map +1 -0
  155. package/dist/services/db-provisioner.test.d.ts +2 -0
  156. package/dist/services/db-provisioner.test.d.ts.map +1 -0
  157. package/dist/services/db-provisioner.test.js +141 -0
  158. package/dist/services/db-provisioner.test.js.map +1 -0
  159. package/dist/services/db-whitelist.d.ts +58 -0
  160. package/dist/services/db-whitelist.d.ts.map +1 -0
  161. package/dist/services/db-whitelist.js +379 -0
  162. package/dist/services/db-whitelist.js.map +1 -0
  163. package/dist/services/dependency-resolver.d.ts +58 -0
  164. package/dist/services/dependency-resolver.d.ts.map +1 -0
  165. package/dist/services/dependency-resolver.js +180 -0
  166. package/dist/services/dependency-resolver.js.map +1 -0
  167. package/dist/services/dependency-resolver.test.d.ts +2 -0
  168. package/dist/services/dependency-resolver.test.d.ts.map +1 -0
  169. package/dist/services/dependency-resolver.test.js +195 -0
  170. package/dist/services/dependency-resolver.test.js.map +1 -0
  171. package/dist/services/deploy-gate.d.ts +19 -0
  172. package/dist/services/deploy-gate.d.ts.map +1 -0
  173. package/dist/services/deploy-gate.js +56 -0
  174. package/dist/services/deploy-gate.js.map +1 -0
  175. package/dist/services/deploy-gate.test.d.ts +2 -0
  176. package/dist/services/deploy-gate.test.d.ts.map +1 -0
  177. package/dist/services/deploy-gate.test.js +199 -0
  178. package/dist/services/deploy-gate.test.js.map +1 -0
  179. package/dist/services/dockerfile-generator.d.ts +31 -0
  180. package/dist/services/dockerfile-generator.d.ts.map +1 -0
  181. package/dist/services/dockerfile-generator.js +544 -0
  182. package/dist/services/dockerfile-generator.js.map +1 -0
  183. package/dist/services/dockerfile-generator.test.d.ts +2 -0
  184. package/dist/services/dockerfile-generator.test.d.ts.map +1 -0
  185. package/dist/services/dockerfile-generator.test.js +144 -0
  186. package/dist/services/dockerfile-generator.test.js.map +1 -0
  187. package/dist/services/forgejo.d.ts +58 -0
  188. package/dist/services/forgejo.d.ts.map +1 -0
  189. package/dist/services/forgejo.js +131 -0
  190. package/dist/services/forgejo.js.map +1 -0
  191. package/dist/services/koko.d.ts +153 -0
  192. package/dist/services/koko.d.ts.map +1 -0
  193. package/dist/services/koko.js +260 -0
  194. package/dist/services/koko.js.map +1 -0
  195. package/dist/services/kubernetes.d.ts +16 -0
  196. package/dist/services/kubernetes.d.ts.map +1 -0
  197. package/dist/services/kubernetes.js +102 -0
  198. package/dist/services/kubernetes.js.map +1 -0
  199. package/dist/services/oauth-provisioner.d.ts +30 -0
  200. package/dist/services/oauth-provisioner.d.ts.map +1 -0
  201. package/dist/services/oauth-provisioner.js +182 -0
  202. package/dist/services/oauth-provisioner.js.map +1 -0
  203. package/dist/services/oauth-provisioner.test.d.ts +2 -0
  204. package/dist/services/oauth-provisioner.test.d.ts.map +1 -0
  205. package/dist/services/oauth-provisioner.test.js +349 -0
  206. package/dist/services/oauth-provisioner.test.js.map +1 -0
  207. package/dist/services/pod-diagnostics.d.ts +11 -0
  208. package/dist/services/pod-diagnostics.d.ts.map +1 -0
  209. package/dist/services/pod-diagnostics.js +201 -0
  210. package/dist/services/pod-diagnostics.js.map +1 -0
  211. package/dist/services/rotation-scheduler.d.ts +2 -0
  212. package/dist/services/rotation-scheduler.d.ts.map +1 -0
  213. package/dist/services/rotation-scheduler.js +215 -0
  214. package/dist/services/rotation-scheduler.js.map +1 -0
  215. package/dist/services/storage-credential-manager.d.ts +43 -0
  216. package/dist/services/storage-credential-manager.d.ts.map +1 -0
  217. package/dist/services/storage-credential-manager.js +159 -0
  218. package/dist/services/storage-credential-manager.js.map +1 -0
  219. package/dist/services/storage-provisioner.d.ts +32 -0
  220. package/dist/services/storage-provisioner.d.ts.map +1 -0
  221. package/dist/services/storage-provisioner.js +136 -0
  222. package/dist/services/storage-provisioner.js.map +1 -0
  223. package/dist/services/storage.d.ts +65 -0
  224. package/dist/services/storage.d.ts.map +1 -0
  225. package/dist/services/storage.js +204 -0
  226. package/dist/services/storage.js.map +1 -0
  227. package/dist/services/troubleshooter.d.ts +22 -0
  228. package/dist/services/troubleshooter.d.ts.map +1 -0
  229. package/dist/services/troubleshooter.js +168 -0
  230. package/dist/services/troubleshooter.js.map +1 -0
  231. package/dist/services/vault-client.d.ts +114 -0
  232. package/dist/services/vault-client.d.ts.map +1 -0
  233. package/dist/services/vault-client.js +411 -0
  234. package/dist/services/vault-client.js.map +1 -0
  235. package/dist/utils/logger.d.ts +2 -0
  236. package/dist/utils/logger.d.ts.map +1 -0
  237. package/dist/utils/logger.js +6 -0
  238. package/dist/utils/logger.js.map +1 -0
  239. package/dist/utils/response.d.ts +13 -0
  240. package/dist/utils/response.d.ts.map +1 -0
  241. package/dist/utils/response.js +12 -0
  242. package/dist/utils/response.js.map +1 -0
  243. package/docs/registry-migration.md +301 -0
  244. package/docs/registry-quickstart.md +169 -0
  245. package/ecosystem.config.cjs +14 -0
  246. package/findings.md +168 -0
  247. package/helm/default-service/Chart.yaml +6 -0
  248. package/helm/default-service/templates/deployment.yaml +97 -0
  249. package/helm/default-service/templates/ingress.yaml +43 -0
  250. package/helm/default-service/templates/service.yaml +17 -0
  251. package/helm/default-service/values.yaml +82 -0
  252. package/helm/services/iec-builder/Chart.yaml +6 -0
  253. package/helm/services/iec-builder/templates/_helpers.tpl +61 -0
  254. package/helm/services/iec-builder/templates/deployment.yaml +73 -0
  255. package/helm/services/iec-builder/templates/service.yaml +15 -0
  256. package/helm/services/iec-builder/templates/serviceaccount.yaml +12 -0
  257. package/helm/services/iec-builder/values.yaml +56 -0
  258. package/helm/vault-values.yaml +127 -0
  259. package/package.json +45 -0
  260. package/progress.md +156 -0
  261. package/scripts/.vault-init-keys.json +23 -0
  262. package/scripts/backfill-ownership.ts +113 -0
  263. package/scripts/finalize-mongo-auth.sh +212 -0
  264. package/scripts/setup-ipset.sh +107 -0
  265. package/scripts/setup-mongo-auth.sh +163 -0
  266. package/scripts/setup-neo4j-auth.sh +62 -0
  267. package/scripts/setup-redis-auth.sh +55 -0
  268. package/scripts/setup-registry-secret.sh +71 -0
  269. package/scripts/setup-vault.sh +308 -0
  270. package/src/config/env.ts +117 -0
  271. package/src/index.ts +153 -0
  272. package/src/middleware/auth.ts +294 -0
  273. package/src/middleware/org-access.ts +126 -0
  274. package/src/models/types.ts +288 -0
  275. package/src/routes/ai.ts +115 -0
  276. package/src/routes/audit.ts +121 -0
  277. package/src/routes/builds.ts +320 -0
  278. package/src/routes/cluster.ts +235 -0
  279. package/src/routes/config.ts +369 -0
  280. package/src/routes/databases.ts +201 -0
  281. package/src/routes/db-whitelist.ts +204 -0
  282. package/src/routes/domains.ts +547 -0
  283. package/src/routes/oauth.ts +195 -0
  284. package/src/routes/observability.ts +205 -0
  285. package/src/routes/orgs.ts +330 -0
  286. package/src/routes/platform.ts +134 -0
  287. package/src/routes/rotation.ts +191 -0
  288. package/src/routes/services.ts +290 -0
  289. package/src/routes/storage.ts +153 -0
  290. package/src/routes/users.ts +235 -0
  291. package/src/routes/webhooks.ts +384 -0
  292. package/src/services/__tests__/catalog-storage.test.ts +186 -0
  293. package/src/services/__tests__/deploy-pipeline.integration.test.ts +624 -0
  294. package/src/services/__tests__/pod-diagnostics.test.ts +332 -0
  295. package/src/services/__tests__/storage-credential-manager.test.ts +129 -0
  296. package/src/services/__tests__/storage-provisioner.test.ts +166 -0
  297. package/src/services/__tests__/troubleshooter.test.ts +329 -0
  298. package/src/services/bio-client.ts +189 -0
  299. package/src/services/build-queue.ts +137 -0
  300. package/src/services/builder.ts +1800 -0
  301. package/src/services/catalog.test.ts +1389 -0
  302. package/src/services/catalog.ts +1187 -0
  303. package/src/services/cloudflare.ts +259 -0
  304. package/src/services/config-validator.test.ts +190 -0
  305. package/src/services/config-validator.ts +108 -0
  306. package/src/services/crypto.ts +78 -0
  307. package/src/services/database.ts +122 -0
  308. package/src/services/db-credential-manager.test.ts +101 -0
  309. package/src/services/db-credential-manager.ts +447 -0
  310. package/src/services/db-provisioner.test.ts +602 -0
  311. package/src/services/db-provisioner.ts +589 -0
  312. package/src/services/db-whitelist.test.ts +671 -0
  313. package/src/services/db-whitelist.ts +496 -0
  314. package/src/services/dependency-resolver.test.ts +677 -0
  315. package/src/services/dependency-resolver.ts +319 -0
  316. package/src/services/deploy-gate.test.ts +247 -0
  317. package/src/services/deploy-gate.ts +75 -0
  318. package/src/services/dockerfile-generator.test.ts +401 -0
  319. package/src/services/dockerfile-generator.ts +606 -0
  320. package/src/services/forgejo.ts +212 -0
  321. package/src/services/koko.ts +492 -0
  322. package/src/services/kubernetes.ts +141 -0
  323. package/src/services/oauth-provisioner.test.ts +477 -0
  324. package/src/services/oauth-provisioner.ts +286 -0
  325. package/src/services/pod-diagnostics.ts +261 -0
  326. package/src/services/rotation-scheduler.ts +293 -0
  327. package/src/services/storage-credential-manager.ts +223 -0
  328. package/src/services/storage-provisioner.ts +216 -0
  329. package/src/services/storage.ts +274 -0
  330. package/src/services/troubleshooter.ts +208 -0
  331. package/src/services/vault-client.test.ts +272 -0
  332. package/src/services/vault-client.ts +587 -0
  333. package/src/utils/logger.ts +6 -0
  334. package/src/utils/response.ts +23 -0
  335. package/task_plan.md +171 -0
  336. package/tsconfig.json +20 -0
  337. 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
+ }