typekro 0.8.0 → 0.9.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 (510) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/alchemy/deployers.d.ts +16 -1
  3. package/dist/alchemy/deployers.d.ts.map +1 -1
  4. package/dist/alchemy/deployers.js +131 -18
  5. package/dist/alchemy/deployers.js.map +1 -1
  6. package/dist/alchemy/deployment.d.ts +1 -1
  7. package/dist/alchemy/deployment.d.ts.map +1 -1
  8. package/dist/alchemy/deployment.js +1 -1
  9. package/dist/alchemy/deployment.js.map +1 -1
  10. package/dist/alchemy/kro-delete.d.ts +66 -0
  11. package/dist/alchemy/kro-delete.d.ts.map +1 -0
  12. package/dist/alchemy/kro-delete.js +183 -0
  13. package/dist/alchemy/kro-delete.js.map +1 -0
  14. package/dist/alchemy/resource-registration.d.ts +16 -0
  15. package/dist/alchemy/resource-registration.d.ts.map +1 -1
  16. package/dist/alchemy/resource-registration.js +138 -24
  17. package/dist/alchemy/resource-registration.js.map +1 -1
  18. package/dist/alchemy/types.d.ts +8 -4
  19. package/dist/alchemy/types.d.ts.map +1 -1
  20. package/dist/compositions/typekro-runtime/typekro-runtime.d.ts +1 -1
  21. package/dist/compositions/typekro-runtime/typekro-runtime.d.ts.map +1 -1
  22. package/dist/compositions/typekro-runtime/typekro-runtime.js +27 -9
  23. package/dist/compositions/typekro-runtime/typekro-runtime.js.map +1 -1
  24. package/dist/core/composition/context.d.ts +58 -2
  25. package/dist/core/composition/context.d.ts.map +1 -1
  26. package/dist/core/composition/context.js +4 -0
  27. package/dist/core/composition/context.js.map +1 -1
  28. package/dist/core/composition/imperative.d.ts +9 -0
  29. package/dist/core/composition/imperative.d.ts.map +1 -1
  30. package/dist/core/composition/imperative.js +538 -54
  31. package/dist/core/composition/imperative.js.map +1 -1
  32. package/dist/core/composition/nested-status-cel.d.ts +34 -1
  33. package/dist/core/composition/nested-status-cel.d.ts.map +1 -1
  34. package/dist/core/composition/nested-status-cel.js +379 -41
  35. package/dist/core/composition/nested-status-cel.js.map +1 -1
  36. package/dist/core/composition-debugger.d.ts +1 -1
  37. package/dist/core/composition-debugger.d.ts.map +1 -1
  38. package/dist/core/composition-debugger.js.map +1 -1
  39. package/dist/core/constants/brands.d.ts +1 -1
  40. package/dist/core/constants/brands.d.ts.map +1 -1
  41. package/dist/core/constants/brands.js +1 -1
  42. package/dist/core/constants/brands.js.map +1 -1
  43. package/dist/core/containers/build.d.ts.map +1 -1
  44. package/dist/core/containers/build.js +40 -12
  45. package/dist/core/containers/build.js.map +1 -1
  46. package/dist/core/dependencies/resolver.d.ts +19 -0
  47. package/dist/core/dependencies/resolver.d.ts.map +1 -1
  48. package/dist/core/dependencies/resolver.js +261 -1
  49. package/dist/core/dependencies/resolver.js.map +1 -1
  50. package/dist/core/deployment/client-provider-manager.d.ts +9 -3
  51. package/dist/core/deployment/client-provider-manager.d.ts.map +1 -1
  52. package/dist/core/deployment/client-provider-manager.js +12 -0
  53. package/dist/core/deployment/client-provider-manager.js.map +1 -1
  54. package/dist/core/deployment/crd-manager.d.ts +24 -0
  55. package/dist/core/deployment/crd-manager.d.ts.map +1 -1
  56. package/dist/core/deployment/crd-manager.js +79 -1
  57. package/dist/core/deployment/crd-manager.js.map +1 -1
  58. package/dist/core/deployment/deployment-state-discovery.d.ts +116 -0
  59. package/dist/core/deployment/deployment-state-discovery.d.ts.map +1 -0
  60. package/dist/core/deployment/deployment-state-discovery.js +400 -0
  61. package/dist/core/deployment/deployment-state-discovery.js.map +1 -0
  62. package/dist/core/deployment/direct-factory.d.ts +65 -6
  63. package/dist/core/deployment/direct-factory.d.ts.map +1 -1
  64. package/dist/core/deployment/direct-factory.js +551 -56
  65. package/dist/core/deployment/direct-factory.js.map +1 -1
  66. package/dist/core/deployment/engine.d.ts +64 -3
  67. package/dist/core/deployment/engine.d.ts.map +1 -1
  68. package/dist/core/deployment/engine.js +194 -27
  69. package/dist/core/deployment/engine.js.map +1 -1
  70. package/dist/core/deployment/event-filter.js +1 -1
  71. package/dist/core/deployment/event-filter.js.map +1 -1
  72. package/dist/core/deployment/event-monitor.d.ts +11 -0
  73. package/dist/core/deployment/event-monitor.d.ts.map +1 -1
  74. package/dist/core/deployment/event-monitor.js +14 -0
  75. package/dist/core/deployment/event-monitor.js.map +1 -1
  76. package/dist/core/deployment/handle-tracing.d.ts +14 -0
  77. package/dist/core/deployment/handle-tracing.d.ts.map +1 -0
  78. package/dist/core/deployment/handle-tracing.js +38 -0
  79. package/dist/core/deployment/handle-tracing.js.map +1 -0
  80. package/dist/core/deployment/kro-factory.d.ts +115 -3
  81. package/dist/core/deployment/kro-factory.d.ts.map +1 -1
  82. package/dist/core/deployment/kro-factory.js +906 -265
  83. package/dist/core/deployment/kro-factory.js.map +1 -1
  84. package/dist/core/deployment/kro-readiness.d.ts.map +1 -1
  85. package/dist/core/deployment/kro-readiness.js +21 -12
  86. package/dist/core/deployment/kro-readiness.js.map +1 -1
  87. package/dist/core/deployment/nested-composition-status.d.ts +1 -1
  88. package/dist/core/deployment/nested-composition-status.d.ts.map +1 -1
  89. package/dist/core/deployment/nested-composition-status.js +96 -53
  90. package/dist/core/deployment/nested-composition-status.js.map +1 -1
  91. package/dist/core/deployment/resource-applier.d.ts +15 -2
  92. package/dist/core/deployment/resource-applier.d.ts.map +1 -1
  93. package/dist/core/deployment/resource-applier.js +75 -25
  94. package/dist/core/deployment/resource-applier.js.map +1 -1
  95. package/dist/core/deployment/resource-tagging.d.ts +220 -0
  96. package/dist/core/deployment/resource-tagging.d.ts.map +1 -0
  97. package/dist/core/deployment/resource-tagging.js +292 -0
  98. package/dist/core/deployment/resource-tagging.js.map +1 -0
  99. package/dist/core/deployment/rollback-manager.d.ts +25 -4
  100. package/dist/core/deployment/rollback-manager.d.ts.map +1 -1
  101. package/dist/core/deployment/rollback-manager.js +70 -57
  102. package/dist/core/deployment/rollback-manager.js.map +1 -1
  103. package/dist/core/deployment/shared-utilities.d.ts +6 -0
  104. package/dist/core/deployment/shared-utilities.d.ts.map +1 -1
  105. package/dist/core/deployment/shared-utilities.js +32 -2
  106. package/dist/core/deployment/shared-utilities.js.map +1 -1
  107. package/dist/core/deployment/singleton-owner-drift.d.ts +16 -0
  108. package/dist/core/deployment/singleton-owner-drift.d.ts.map +1 -0
  109. package/dist/core/deployment/singleton-owner-drift.js +54 -0
  110. package/dist/core/deployment/singleton-owner-drift.js.map +1 -0
  111. package/dist/core/deployment/strategies/alchemy-strategy.d.ts +3 -1
  112. package/dist/core/deployment/strategies/alchemy-strategy.d.ts.map +1 -1
  113. package/dist/core/deployment/strategies/alchemy-strategy.js +121 -18
  114. package/dist/core/deployment/strategies/alchemy-strategy.js.map +1 -1
  115. package/dist/core/deployment/strategies/base-strategy.d.ts +9 -3
  116. package/dist/core/deployment/strategies/base-strategy.d.ts.map +1 -1
  117. package/dist/core/deployment/strategies/base-strategy.js +32 -4
  118. package/dist/core/deployment/strategies/base-strategy.js.map +1 -1
  119. package/dist/core/deployment/strategies/direct-strategy.d.ts +12 -4
  120. package/dist/core/deployment/strategies/direct-strategy.d.ts.map +1 -1
  121. package/dist/core/deployment/strategies/direct-strategy.js +112 -8
  122. package/dist/core/deployment/strategies/direct-strategy.js.map +1 -1
  123. package/dist/core/errors.d.ts +2 -2
  124. package/dist/core/errors.d.ts.map +1 -1
  125. package/dist/core/errors.js.map +1 -1
  126. package/dist/core/expressions/analysis/cache.d.ts +2 -2
  127. package/dist/core/expressions/analysis/cache.d.ts.map +1 -1
  128. package/dist/core/expressions/analysis/cache.js +4 -0
  129. package/dist/core/expressions/analysis/cache.js.map +1 -1
  130. package/dist/core/expressions/analysis/fn-toString-self-test.d.ts.map +1 -1
  131. package/dist/core/expressions/analysis/fn-toString-self-test.js +0 -1
  132. package/dist/core/expressions/analysis/fn-toString-self-test.js.map +1 -1
  133. package/dist/core/expressions/analysis/shared-types.d.ts +2 -2
  134. package/dist/core/expressions/analysis/shared-types.d.ts.map +1 -1
  135. package/dist/core/expressions/analysis/source-map.d.ts.map +1 -1
  136. package/dist/core/expressions/analysis/source-map.js +1 -0
  137. package/dist/core/expressions/analysis/source-map.js.map +1 -1
  138. package/dist/core/expressions/analysis/types.d.ts +7 -7
  139. package/dist/core/expressions/analysis/types.d.ts.map +1 -1
  140. package/dist/core/expressions/composition/composition-analyzer-helpers.d.ts +29 -1
  141. package/dist/core/expressions/composition/composition-analyzer-helpers.d.ts.map +1 -1
  142. package/dist/core/expressions/composition/composition-analyzer-helpers.js +348 -17
  143. package/dist/core/expressions/composition/composition-analyzer-helpers.js.map +1 -1
  144. package/dist/core/expressions/composition/composition-analyzer-ternary.d.ts +2 -2
  145. package/dist/core/expressions/composition/composition-analyzer-ternary.d.ts.map +1 -1
  146. package/dist/core/expressions/composition/composition-analyzer-ternary.js +221 -10
  147. package/dist/core/expressions/composition/composition-analyzer-ternary.js.map +1 -1
  148. package/dist/core/expressions/composition/composition-analyzer-traversal.d.ts.map +1 -1
  149. package/dist/core/expressions/composition/composition-analyzer-traversal.js +67 -2
  150. package/dist/core/expressions/composition/composition-analyzer-traversal.js.map +1 -1
  151. package/dist/core/expressions/composition/composition-analyzer-types.d.ts +52 -0
  152. package/dist/core/expressions/composition/composition-analyzer-types.d.ts.map +1 -1
  153. package/dist/core/expressions/composition/composition-analyzer.d.ts.map +1 -1
  154. package/dist/core/expressions/composition/composition-analyzer.js +150 -6
  155. package/dist/core/expressions/composition/composition-analyzer.js.map +1 -1
  156. package/dist/core/expressions/composition/expression-analyzer.d.ts.map +1 -1
  157. package/dist/core/expressions/composition/expression-analyzer.js +21 -4
  158. package/dist/core/expressions/composition/expression-analyzer.js.map +1 -1
  159. package/dist/core/expressions/composition/imperative-analyzer.d.ts +1 -1
  160. package/dist/core/expressions/composition/imperative-analyzer.d.ts.map +1 -1
  161. package/dist/core/expressions/composition/imperative-analyzer.js +31 -7
  162. package/dist/core/expressions/composition/imperative-analyzer.js.map +1 -1
  163. package/dist/core/expressions/composition/scope-manager.d.ts +2 -2
  164. package/dist/core/expressions/composition/scope-manager.d.ts.map +1 -1
  165. package/dist/core/expressions/composition/scope-manager.js.map +1 -1
  166. package/dist/core/expressions/conditional/conditional-expression-processor.d.ts +3 -3
  167. package/dist/core/expressions/conditional/conditional-expression-processor.d.ts.map +1 -1
  168. package/dist/core/expressions/conditional/conditional-expression-processor.js.map +1 -1
  169. package/dist/core/expressions/conditional/conditional-integration.d.ts.map +1 -1
  170. package/dist/core/expressions/conditional/conditional-integration.js.map +1 -1
  171. package/dist/core/expressions/context/context-aware-generator.d.ts +5 -5
  172. package/dist/core/expressions/context/context-aware-generator.d.ts.map +1 -1
  173. package/dist/core/expressions/context/context-aware-generator.js.map +1 -1
  174. package/dist/core/expressions/context/context-detector.d.ts +3 -3
  175. package/dist/core/expressions/context/context-detector.d.ts.map +1 -1
  176. package/dist/core/expressions/context/context-detector.js.map +1 -1
  177. package/dist/core/expressions/context/context-validator.d.ts +6 -6
  178. package/dist/core/expressions/context/context-validator.d.ts.map +1 -1
  179. package/dist/core/expressions/context/context-validator.js +2 -2
  180. package/dist/core/expressions/context/context-validator.js.map +1 -1
  181. package/dist/core/expressions/factory/cel-conversion-engine.d.ts +4 -4
  182. package/dist/core/expressions/factory/cel-conversion-engine.d.ts.map +1 -1
  183. package/dist/core/expressions/factory/cel-conversion-engine.js.map +1 -1
  184. package/dist/core/expressions/factory/dependency-tracker.d.ts +2 -2
  185. package/dist/core/expressions/factory/dependency-tracker.d.ts.map +1 -1
  186. package/dist/core/expressions/factory/dependency-tracker.js +21 -5
  187. package/dist/core/expressions/factory/dependency-tracker.js.map +1 -1
  188. package/dist/core/expressions/factory/factory-integration.d.ts +2 -4
  189. package/dist/core/expressions/factory/factory-integration.d.ts.map +1 -1
  190. package/dist/core/expressions/factory/factory-integration.js +0 -6
  191. package/dist/core/expressions/factory/factory-integration.js.map +1 -1
  192. package/dist/core/expressions/factory/factory-pattern-handler.d.ts +3 -3
  193. package/dist/core/expressions/factory/factory-pattern-handler.d.ts.map +1 -1
  194. package/dist/core/expressions/factory/factory-pattern-handler.js +1 -0
  195. package/dist/core/expressions/factory/factory-pattern-handler.js.map +1 -1
  196. package/dist/core/expressions/factory/migration-helpers.js.map +1 -1
  197. package/dist/core/expressions/factory/resource-analyzer.d.ts +4 -4
  198. package/dist/core/expressions/factory/resource-analyzer.d.ts.map +1 -1
  199. package/dist/core/expressions/factory/resource-analyzer.js.map +1 -1
  200. package/dist/core/expressions/factory/resource-type-validator.d.ts +5 -5
  201. package/dist/core/expressions/factory/resource-type-validator.d.ts.map +1 -1
  202. package/dist/core/expressions/factory/resource-type-validator.js.map +1 -1
  203. package/dist/core/expressions/factory/status-builder-analyzer.d.ts +6 -7
  204. package/dist/core/expressions/factory/status-builder-analyzer.d.ts.map +1 -1
  205. package/dist/core/expressions/factory/status-builder-analyzer.js +0 -3
  206. package/dist/core/expressions/factory/status-builder-analyzer.js.map +1 -1
  207. package/dist/core/expressions/factory/status-builder-types.d.ts +1 -1
  208. package/dist/core/expressions/factory/status-builder-types.d.ts.map +1 -1
  209. package/dist/core/expressions/factory/status-cel-generation.d.ts +1 -1
  210. package/dist/core/expressions/factory/status-cel-generation.d.ts.map +1 -1
  211. package/dist/core/expressions/factory/status-cel-generation.js +1 -1
  212. package/dist/core/expressions/factory/status-cel-generation.js.map +1 -1
  213. package/dist/core/expressions/factory/status-field-analysis.d.ts +3 -3
  214. package/dist/core/expressions/factory/status-field-analysis.d.ts.map +1 -1
  215. package/dist/core/expressions/factory/status-field-analysis.js.map +1 -1
  216. package/dist/core/expressions/magic-proxy/magic-assignable-analyzer.d.ts +5 -5
  217. package/dist/core/expressions/magic-proxy/magic-assignable-analyzer.d.ts.map +1 -1
  218. package/dist/core/expressions/magic-proxy/magic-assignable-analyzer.js +1 -1
  219. package/dist/core/expressions/magic-proxy/magic-assignable-analyzer.js.map +1 -1
  220. package/dist/core/expressions/magic-proxy/magic-proxy-analyzer.d.ts +10 -10
  221. package/dist/core/expressions/magic-proxy/magic-proxy-analyzer.d.ts.map +1 -1
  222. package/dist/core/expressions/magic-proxy/magic-proxy-analyzer.js +5 -1
  223. package/dist/core/expressions/magic-proxy/magic-proxy-analyzer.js.map +1 -1
  224. package/dist/core/expressions/magic-proxy/magic-proxy-ast.d.ts +2 -2
  225. package/dist/core/expressions/magic-proxy/magic-proxy-ast.d.ts.map +1 -1
  226. package/dist/core/expressions/magic-proxy/magic-proxy-ast.js.map +1 -1
  227. package/dist/core/expressions/magic-proxy/magic-proxy-detector.d.ts +5 -5
  228. package/dist/core/expressions/magic-proxy/magic-proxy-detector.d.ts.map +1 -1
  229. package/dist/core/expressions/magic-proxy/magic-proxy-detector.js.map +1 -1
  230. package/dist/core/expressions/magic-proxy/magic-proxy-types.d.ts +2 -2
  231. package/dist/core/expressions/magic-proxy/magic-proxy-types.d.ts.map +1 -1
  232. package/dist/core/expressions/magic-proxy/optionality-handler.d.ts +1 -2
  233. package/dist/core/expressions/magic-proxy/optionality-handler.d.ts.map +1 -1
  234. package/dist/core/expressions/magic-proxy/optionality-handler.js +2 -15
  235. package/dist/core/expressions/magic-proxy/optionality-handler.js.map +1 -1
  236. package/dist/core/expressions/validation/compile-time-checker.d.ts +1 -1
  237. package/dist/core/expressions/validation/compile-time-checker.d.ts.map +1 -1
  238. package/dist/core/expressions/validation/compile-time-checker.js.map +1 -1
  239. package/dist/core/expressions/validation/compile-time-types.d.ts +2 -2
  240. package/dist/core/expressions/validation/compile-time-types.d.ts.map +1 -1
  241. package/dist/core/expressions/validation/kubernetes-field-types.d.ts +4 -4
  242. package/dist/core/expressions/validation/kubernetes-field-types.d.ts.map +1 -1
  243. package/dist/core/expressions/validation/kubernetes-field-types.js.map +1 -1
  244. package/dist/core/expressions/validation/resource-field-utils.d.ts +10 -10
  245. package/dist/core/expressions/validation/resource-field-utils.d.ts.map +1 -1
  246. package/dist/core/expressions/validation/resource-field-utils.js.map +1 -1
  247. package/dist/core/expressions/validation/resource-validation.d.ts +3 -3
  248. package/dist/core/expressions/validation/resource-validation.d.ts.map +1 -1
  249. package/dist/core/expressions/validation/resource-validation.js.map +1 -1
  250. package/dist/core/expressions/validation/type-inference-types.d.ts +2 -2
  251. package/dist/core/expressions/validation/type-inference-types.d.ts.map +1 -1
  252. package/dist/core/expressions/validation/type-safety.d.ts +2 -2
  253. package/dist/core/expressions/validation/type-safety.d.ts.map +1 -1
  254. package/dist/core/expressions/validation/type-safety.js +1 -0
  255. package/dist/core/expressions/validation/type-safety.js.map +1 -1
  256. package/dist/core/kubernetes/bun-api-client.js.map +1 -1
  257. package/dist/core/kubernetes/bun-http-library.d.ts.map +1 -1
  258. package/dist/core/kubernetes/bun-http-library.js +29 -3
  259. package/dist/core/kubernetes/bun-http-library.js.map +1 -1
  260. package/dist/core/kubernetes/client-provider.d.ts +12 -0
  261. package/dist/core/kubernetes/client-provider.d.ts.map +1 -1
  262. package/dist/core/kubernetes/client-provider.js +35 -0
  263. package/dist/core/kubernetes/client-provider.js.map +1 -1
  264. package/dist/core/metadata/resource-metadata.d.ts +46 -1
  265. package/dist/core/metadata/resource-metadata.d.ts.map +1 -1
  266. package/dist/core/metadata/resource-metadata.js.map +1 -1
  267. package/dist/core/proxy/create-resource.d.ts +15 -0
  268. package/dist/core/proxy/create-resource.d.ts.map +1 -1
  269. package/dist/core/proxy/create-resource.js +56 -2
  270. package/dist/core/proxy/create-resource.js.map +1 -1
  271. package/dist/core/readiness/registry.js +2 -2
  272. package/dist/core/readiness/registry.js.map +1 -1
  273. package/dist/core/references/cel-evaluator.d.ts +1 -4
  274. package/dist/core/references/cel-evaluator.d.ts.map +1 -1
  275. package/dist/core/references/cel-evaluator.js +3 -7
  276. package/dist/core/references/cel-evaluator.js.map +1 -1
  277. package/dist/core/references/cel.d.ts +22 -0
  278. package/dist/core/references/cel.d.ts.map +1 -1
  279. package/dist/core/references/cel.js +106 -8
  280. package/dist/core/references/cel.js.map +1 -1
  281. package/dist/core/references/external-refs.d.ts.map +1 -1
  282. package/dist/core/references/external-refs.js +3 -0
  283. package/dist/core/references/external-refs.js.map +1 -1
  284. package/dist/core/references/resolver.d.ts.map +1 -1
  285. package/dist/core/references/resolver.js +28 -17
  286. package/dist/core/references/resolver.js.map +1 -1
  287. package/dist/core/references/schema-proxy.d.ts +18 -10
  288. package/dist/core/references/schema-proxy.d.ts.map +1 -1
  289. package/dist/core/references/schema-proxy.js +174 -23
  290. package/dist/core/references/schema-proxy.js.map +1 -1
  291. package/dist/core/runtime-patches/crd-schema-fix.d.ts.map +1 -1
  292. package/dist/core/runtime-patches/crd-schema-fix.js +4 -1
  293. package/dist/core/runtime-patches/crd-schema-fix.js.map +1 -1
  294. package/dist/core/serialization/cel-optimizer.d.ts.map +1 -1
  295. package/dist/core/serialization/cel-optimizer.js +2 -0
  296. package/dist/core/serialization/cel-optimizer.js.map +1 -1
  297. package/dist/core/serialization/cel-references.d.ts +75 -1
  298. package/dist/core/serialization/cel-references.d.ts.map +1 -1
  299. package/dist/core/serialization/cel-references.js +692 -156
  300. package/dist/core/serialization/cel-references.js.map +1 -1
  301. package/dist/core/serialization/core.d.ts +8 -8
  302. package/dist/core/serialization/core.d.ts.map +1 -1
  303. package/dist/core/serialization/core.js +638 -90
  304. package/dist/core/serialization/core.js.map +1 -1
  305. package/dist/core/serialization/kro-post-processing.d.ts +1 -1
  306. package/dist/core/serialization/kro-post-processing.d.ts.map +1 -1
  307. package/dist/core/serialization/kro-post-processing.js +69 -22
  308. package/dist/core/serialization/kro-post-processing.js.map +1 -1
  309. package/dist/core/serialization/schema.d.ts +1 -0
  310. package/dist/core/serialization/schema.d.ts.map +1 -1
  311. package/dist/core/serialization/schema.js +378 -50
  312. package/dist/core/serialization/schema.js.map +1 -1
  313. package/dist/core/serialization/status-analysis-pipeline.d.ts +1 -1
  314. package/dist/core/serialization/status-analysis-pipeline.d.ts.map +1 -1
  315. package/dist/core/serialization/status-analysis-pipeline.js.map +1 -1
  316. package/dist/core/serialization/yaml.d.ts +3 -2
  317. package/dist/core/serialization/yaml.d.ts.map +1 -1
  318. package/dist/core/serialization/yaml.js +385 -55
  319. package/dist/core/serialization/yaml.js.map +1 -1
  320. package/dist/core/singleton/singleton.d.ts +16 -0
  321. package/dist/core/singleton/singleton.d.ts.map +1 -0
  322. package/dist/core/singleton/singleton.js +135 -0
  323. package/dist/core/singleton/singleton.js.map +1 -0
  324. package/dist/core/types/common.d.ts +2 -2
  325. package/dist/core/types/common.d.ts.map +1 -1
  326. package/dist/core/types/composable.d.ts +1 -1
  327. package/dist/core/types/composable.d.ts.map +1 -1
  328. package/dist/core/types/deployment.d.ts +126 -6
  329. package/dist/core/types/deployment.d.ts.map +1 -1
  330. package/dist/core/types/deployment.js +1 -1
  331. package/dist/core/types/deployment.js.map +1 -1
  332. package/dist/core/types/kubernetes.d.ts +25 -17
  333. package/dist/core/types/kubernetes.d.ts.map +1 -1
  334. package/dist/core/types/references.d.ts +1 -1
  335. package/dist/core/types/references.d.ts.map +1 -1
  336. package/dist/core/types/references.js.map +1 -1
  337. package/dist/core/types/resource-graph.d.ts +1 -1
  338. package/dist/core/types/resource-graph.d.ts.map +1 -1
  339. package/dist/core/types/schema.d.ts +1 -1
  340. package/dist/core/types/schema.d.ts.map +1 -1
  341. package/dist/core/types/serialization.d.ts +24 -6
  342. package/dist/core/types/serialization.d.ts.map +1 -1
  343. package/dist/core/validation/cel-validator.d.ts +15 -2
  344. package/dist/core/validation/cel-validator.d.ts.map +1 -1
  345. package/dist/core/validation/cel-validator.js +144 -63
  346. package/dist/core/validation/cel-validator.js.map +1 -1
  347. package/dist/factories/apisix/compositions/apisix-bootstrap.d.ts +2 -41
  348. package/dist/factories/apisix/compositions/apisix-bootstrap.d.ts.map +1 -1
  349. package/dist/factories/apisix/compositions/apisix-bootstrap.js +262 -217
  350. package/dist/factories/apisix/compositions/apisix-bootstrap.js.map +1 -1
  351. package/dist/factories/apisix/index.d.ts +2 -2
  352. package/dist/factories/apisix/index.js +2 -2
  353. package/dist/factories/apisix/resources/helm.d.ts +2 -2
  354. package/dist/factories/apisix/resources/helm.d.ts.map +1 -1
  355. package/dist/factories/apisix/resources/helm.js.map +1 -1
  356. package/dist/factories/apisix/types.d.ts +21 -11
  357. package/dist/factories/apisix/types.d.ts.map +1 -1
  358. package/dist/factories/apisix/types.js +106 -4
  359. package/dist/factories/apisix/types.js.map +1 -1
  360. package/dist/factories/apisix/utils/admin-credentials.d.ts +5 -3
  361. package/dist/factories/apisix/utils/admin-credentials.d.ts.map +1 -1
  362. package/dist/factories/apisix/utils/admin-credentials.js +14 -10
  363. package/dist/factories/apisix/utils/admin-credentials.js.map +1 -1
  364. package/dist/factories/apisix/utils/helm-values-mapper.d.ts.map +1 -1
  365. package/dist/factories/apisix/utils/helm-values-mapper.js +4 -2
  366. package/dist/factories/apisix/utils/helm-values-mapper.js.map +1 -1
  367. package/dist/factories/cert-manager/resources/challenges.js.map +1 -1
  368. package/dist/factories/cert-manager/types.d.ts +3 -3
  369. package/dist/factories/cert-manager/types.d.ts.map +1 -1
  370. package/dist/factories/cilium/compositions/cilium-bootstrap.d.ts +4 -4
  371. package/dist/factories/cilium/types.d.ts +3 -3
  372. package/dist/factories/cilium/types.d.ts.map +1 -1
  373. package/dist/factories/cnpg/compositions/cnpg-bootstrap.d.ts +1 -0
  374. package/dist/factories/cnpg/compositions/cnpg-bootstrap.d.ts.map +1 -1
  375. package/dist/factories/cnpg/compositions/cnpg-bootstrap.js +48 -0
  376. package/dist/factories/cnpg/compositions/cnpg-bootstrap.js.map +1 -1
  377. package/dist/factories/cnpg/resources/cluster.js +1 -1
  378. package/dist/factories/cnpg/resources/cluster.js.map +1 -1
  379. package/dist/factories/cnpg/resources/helm.d.ts.map +1 -1
  380. package/dist/factories/cnpg/resources/helm.js +1 -0
  381. package/dist/factories/cnpg/resources/helm.js.map +1 -1
  382. package/dist/factories/cnpg/resources/pooler.js +1 -1
  383. package/dist/factories/cnpg/resources/pooler.js.map +1 -1
  384. package/dist/factories/cnpg/types.d.ts +9 -8
  385. package/dist/factories/cnpg/types.d.ts.map +1 -1
  386. package/dist/factories/cnpg/types.js +11 -0
  387. package/dist/factories/cnpg/types.js.map +1 -1
  388. package/dist/factories/external-dns/compositions/external-dns-bootstrap.d.ts.map +1 -1
  389. package/dist/factories/external-dns/compositions/external-dns-bootstrap.js +153 -41
  390. package/dist/factories/external-dns/compositions/external-dns-bootstrap.js.map +1 -1
  391. package/dist/factories/external-dns/resources/dns-endpoint.js +1 -1
  392. package/dist/factories/external-dns/resources/dns-endpoint.js.map +1 -1
  393. package/dist/factories/external-dns/resources/helm.d.ts +1 -1
  394. package/dist/factories/external-dns/resources/helm.d.ts.map +1 -1
  395. package/dist/factories/external-dns/resources/helm.js +17 -10
  396. package/dist/factories/external-dns/resources/helm.js.map +1 -1
  397. package/dist/factories/external-dns/types.d.ts +5 -2
  398. package/dist/factories/external-dns/types.d.ts.map +1 -1
  399. package/dist/factories/external-dns/types.js.map +1 -1
  400. package/dist/factories/flux/git-repository.d.ts.map +1 -1
  401. package/dist/factories/flux/git-repository.js +1 -1
  402. package/dist/factories/flux/git-repository.js.map +1 -1
  403. package/dist/factories/flux/kustomize/kustomization.d.ts +2 -2
  404. package/dist/factories/flux/kustomize/kustomization.d.ts.map +1 -1
  405. package/dist/factories/flux/kustomize/readiness-evaluators.d.ts +1 -1
  406. package/dist/factories/flux/kustomize/readiness-evaluators.d.ts.map +1 -1
  407. package/dist/factories/flux/kustomize/readiness-evaluators.js +1 -1
  408. package/dist/factories/flux/kustomize/readiness-evaluators.js.map +1 -1
  409. package/dist/factories/helm/helm-release.d.ts +3 -2
  410. package/dist/factories/helm/helm-release.d.ts.map +1 -1
  411. package/dist/factories/helm/helm-release.js +1 -0
  412. package/dist/factories/helm/helm-release.js.map +1 -1
  413. package/dist/factories/helm/helm-repository.d.ts +1 -1
  414. package/dist/factories/helm/helm-repository.d.ts.map +1 -1
  415. package/dist/factories/helm/helm-repository.js +6 -4
  416. package/dist/factories/helm/helm-repository.js.map +1 -1
  417. package/dist/factories/helm/readiness-evaluators.d.ts +6 -6
  418. package/dist/factories/helm/readiness-evaluators.d.ts.map +1 -1
  419. package/dist/factories/helm/readiness-evaluators.js +15 -9
  420. package/dist/factories/helm/readiness-evaluators.js.map +1 -1
  421. package/dist/factories/helm/types.d.ts +5 -1
  422. package/dist/factories/helm/types.d.ts.map +1 -1
  423. package/dist/factories/inngest/compositions/inngest-bootstrap.d.ts +1 -0
  424. package/dist/factories/inngest/compositions/inngest-bootstrap.d.ts.map +1 -1
  425. package/dist/factories/inngest/compositions/inngest-bootstrap.js +4 -3
  426. package/dist/factories/inngest/compositions/inngest-bootstrap.js.map +1 -1
  427. package/dist/factories/inngest/resources/helm.js +1 -1
  428. package/dist/factories/inngest/resources/helm.js.map +1 -1
  429. package/dist/factories/inngest/types.d.ts +5 -4
  430. package/dist/factories/inngest/types.d.ts.map +1 -1
  431. package/dist/factories/inngest/types.js +2 -0
  432. package/dist/factories/inngest/types.js.map +1 -1
  433. package/dist/factories/kro/kro-custom-resource.js +1 -1
  434. package/dist/factories/kro/kro-custom-resource.js.map +1 -1
  435. package/dist/factories/kubernetes/config/config-map.d.ts +2 -2
  436. package/dist/factories/kubernetes/config/config-map.d.ts.map +1 -1
  437. package/dist/factories/kubernetes/config/secret.d.ts +2 -2
  438. package/dist/factories/kubernetes/config/secret.d.ts.map +1 -1
  439. package/dist/factories/kubernetes/networking/service.js +1 -1
  440. package/dist/factories/kubernetes/networking/service.js.map +1 -1
  441. package/dist/factories/kubernetes/yaml/yaml-directory.d.ts.map +1 -1
  442. package/dist/factories/kubernetes/yaml/yaml-directory.js +9 -0
  443. package/dist/factories/kubernetes/yaml/yaml-directory.js.map +1 -1
  444. package/dist/factories/kubernetes/yaml/yaml-file.d.ts.map +1 -1
  445. package/dist/factories/kubernetes/yaml/yaml-file.js +9 -0
  446. package/dist/factories/kubernetes/yaml/yaml-file.js.map +1 -1
  447. package/dist/factories/pebble/resources/helm.js.map +1 -1
  448. package/dist/factories/pebble/types.d.ts +2 -2
  449. package/dist/factories/searxng/compositions/searxng-bootstrap.d.ts +3 -2
  450. package/dist/factories/searxng/compositions/searxng-bootstrap.d.ts.map +1 -1
  451. package/dist/factories/searxng/compositions/searxng-bootstrap.js +205 -167
  452. package/dist/factories/searxng/compositions/searxng-bootstrap.js.map +1 -1
  453. package/dist/factories/searxng/resources/searxng.d.ts +1 -1
  454. package/dist/factories/searxng/resources/searxng.js +1 -1
  455. package/dist/factories/searxng/types.d.ts +5 -4
  456. package/dist/factories/searxng/types.d.ts.map +1 -1
  457. package/dist/factories/searxng/types.js +8 -7
  458. package/dist/factories/searxng/types.js.map +1 -1
  459. package/dist/factories/searxng/utils/settings-builder.d.ts +4 -3
  460. package/dist/factories/searxng/utils/settings-builder.d.ts.map +1 -1
  461. package/dist/factories/searxng/utils/settings-builder.js +4 -3
  462. package/dist/factories/searxng/utils/settings-builder.js.map +1 -1
  463. package/dist/factories/simple/config/config-map.d.ts +2 -2
  464. package/dist/factories/simple/config/config-map.d.ts.map +1 -1
  465. package/dist/factories/simple/config/secret.d.ts +2 -2
  466. package/dist/factories/simple/config/secret.d.ts.map +1 -1
  467. package/dist/factories/simple/helm/index.d.ts +1 -1
  468. package/dist/factories/simple/helm/index.d.ts.map +1 -1
  469. package/dist/factories/simple/helm/index.js.map +1 -1
  470. package/dist/factories/simple/storage/persistent-volume.js.map +1 -1
  471. package/dist/factories/simple/types.d.ts +11 -1
  472. package/dist/factories/simple/types.d.ts.map +1 -1
  473. package/dist/factories/simple/workloads/deployment.d.ts.map +1 -1
  474. package/dist/factories/simple/workloads/deployment.js +3 -0
  475. package/dist/factories/simple/workloads/deployment.js.map +1 -1
  476. package/dist/factories/valkey/compositions/valkey-bootstrap.d.ts +1 -0
  477. package/dist/factories/valkey/compositions/valkey-bootstrap.d.ts.map +1 -1
  478. package/dist/factories/valkey/compositions/valkey-bootstrap.js +116 -0
  479. package/dist/factories/valkey/compositions/valkey-bootstrap.js.map +1 -1
  480. package/dist/factories/valkey/resources/valkey.js +1 -1
  481. package/dist/factories/valkey/resources/valkey.js.map +1 -1
  482. package/dist/factories/valkey/types.d.ts +6 -5
  483. package/dist/factories/valkey/types.d.ts.map +1 -1
  484. package/dist/factories/valkey/types.js +10 -0
  485. package/dist/factories/valkey/types.js.map +1 -1
  486. package/dist/factories/webapp/compositions/web-app-with-processing.d.ts +90 -6
  487. package/dist/factories/webapp/compositions/web-app-with-processing.d.ts.map +1 -1
  488. package/dist/factories/webapp/compositions/web-app-with-processing.js +180 -20
  489. package/dist/factories/webapp/compositions/web-app-with-processing.js.map +1 -1
  490. package/dist/factories/webapp/index.d.ts +3 -4
  491. package/dist/factories/webapp/index.d.ts.map +1 -1
  492. package/dist/factories/webapp/index.js +3 -4
  493. package/dist/factories/webapp/index.js.map +1 -1
  494. package/dist/factories/webapp/types.d.ts +60 -2
  495. package/dist/factories/webapp/types.d.ts.map +1 -1
  496. package/dist/factories/webapp/types.js +80 -3
  497. package/dist/factories/webapp/types.js.map +1 -1
  498. package/dist/index.d.ts +2 -0
  499. package/dist/index.d.ts.map +1 -1
  500. package/dist/index.js +1 -0
  501. package/dist/index.js.map +1 -1
  502. package/dist/shared/brands.d.ts +18 -8
  503. package/dist/shared/brands.d.ts.map +1 -1
  504. package/dist/shared/brands.js +19 -9
  505. package/dist/shared/brands.js.map +1 -1
  506. package/dist/utils/cel-escape.d.ts +12 -0
  507. package/dist/utils/cel-escape.d.ts.map +1 -0
  508. package/dist/utils/cel-escape.js +19 -0
  509. package/dist/utils/cel-escape.js.map +1 -0
  510. package/package.json +3 -2
@@ -6,25 +6,74 @@
6
6
  */
7
7
  import * as k8s from '@kubernetes/client-node';
8
8
  import { compile as compileExpression } from 'angular-expressions';
9
+ import * as yaml from 'js-yaml';
9
10
  import { preserveNonEnumerableProperties } from '../../utils/helpers.js';
11
+ import { isKubernetesRef } from '../../utils/type-guards.js';
10
12
  import { DEFAULT_DEPLOYMENT_TIMEOUT, DEFAULT_KRO_INSTANCE_TIMEOUT, DEFAULT_RGD_TIMEOUT, } from '../config/defaults.js';
11
- import { CEL_EXPRESSION_BRAND } from '../constants/brands.js';
12
- import { CRDInstanceError, ensureError, ResourceGraphFactoryError } from '../errors.js';
13
+ import { CEL_EXPRESSION_BRAND, KUBERNETES_REF_SCHEMA_MARKER_SOURCE } from '../constants/brands.js';
14
+ import { CRDInstanceError, ensureError, ResourceGraphFactoryError, TypeKroError, } from '../errors.js';
13
15
  import { createBunCompatibleKubernetesObjectApi } from '../kubernetes/index.js';
14
16
  import { getComponentLogger } from '../logging/index.js';
15
17
  import { createSchemaProxy, DeploymentMode } from '../references/index.js';
18
+ import { createCompositionContext, runWithCompositionContext } from '../composition/context.js';
19
+ import { buildNestedCompositionAliasTargets } from '../composition/nested-status-cel.js';
20
+ import { applyAnalysisToResources } from '../expressions/composition/composition-analyzer.js';
16
21
  // Dependency inversion: kroCustomResource, resourceGraphDefinition, and
17
22
  // alchemy bridge are injected via FactoryOptions providers (Phase 3.5)
18
23
  // instead of dynamic import() from higher layers.
19
- import { getMetadataField } from '../metadata/index.js';
20
- import { getResourceId } from '../resources/id.js';
24
+ import { getMetadataField, setMetadataField } from '../metadata/index.js';
21
25
  import { generateKroSchemaFromArktype } from '../serialization/schema.js';
22
26
  import { applyTernaryConditionalsToResources } from '../serialization/kro-post-processing.js';
23
27
  import { serializeResourceGraphToYaml } from '../serialization/yaml.js';
28
+ import { logHandleSnapshot } from './handle-tracing.js';
24
29
  import { KubernetesClientManager } from './client-provider-manager.js';
25
30
  import { DirectDeploymentEngine } from './engine.js';
31
+ import { isNotFoundError } from './k8s-helpers.js';
26
32
  import { waitForKroInstanceReady as waitForKroInstanceReadyShared } from './kro-readiness.js';
27
- import { convertToKubernetesName, generateInstanceName, pluralizeKind, validateSpec, } from './shared-utilities.js';
33
+ import { getSingletonResourceId } from '../singleton/singleton.js';
34
+ import { convertToKubernetesName, generateInstanceName, getSingletonInstanceName, pluralizeKind, validateAlchemyScope, validateSpec, } from './shared-utilities.js';
35
+ import { assertNoDeployedSingletonSpecDrift, singletonSpecFingerprintAnnotationValue, } from './singleton-owner-drift.js';
36
+ /**
37
+ * Decide whether the RGD/CRD should be preserved after a `deleteInstance`
38
+ * call, i.e., whether other instances still depend on it.
39
+ *
40
+ * **Exported for testing.** This is the pure decision core extracted
41
+ * from {@link KroResourceFactoryImpl.deleteInstance}:
42
+ *
43
+ * - `instanceDeleted === true` — the poll loop confirmed the CR
44
+ * returned 404. Filter the target name out of the live instance
45
+ * list to handle list-cache lag (the CR is gone from GETs but may
46
+ * still appear in LISTs briefly). If any *other* instances remain,
47
+ * preserve the RGD so they keep working.
48
+ *
49
+ * - `instanceDeleted === false` — the poll loop timed out while KRO
50
+ * was still processing `kro.run/finalizer`. Do NOT filter the
51
+ * target name out: the stuck instance counts as remaining so the
52
+ * RGD stays up for KRO to complete finalizer processing in the
53
+ * background. Deleting the RGD mid-finalizer orphans the finalizer
54
+ * and permanently blocks cleanup — a regression that surfaced
55
+ * during real-world KRO dogfooding.
56
+ *
57
+ * Returns `true` when the RGD should be preserved (i.e., remaining
58
+ * instances exist or the target is stuck), `false` when it's safe
59
+ * to tear the RGD/CRD down.
60
+ */
61
+ export function shouldPreserveRgd(
62
+ // Loose typing — the callers pass Enhanced<TSpec, TStatus>[] at runtime
63
+ // but this decision only reads `metadata.name` / `metadata.namespace`
64
+ // and tolerates undefined.
65
+ instances, targetName, instanceDeleted, targetNamespace) {
66
+ const others = instanceDeleted
67
+ ? instances.filter((i) => {
68
+ if (i.metadata?.name !== targetName)
69
+ return true;
70
+ if (!targetNamespace)
71
+ return false;
72
+ return i.metadata?.namespace !== targetNamespace;
73
+ })
74
+ : instances;
75
+ return others.length > 0;
76
+ }
28
77
  /**
29
78
  * KroResourceFactory implementation
30
79
  *
@@ -43,6 +92,8 @@ export class KroResourceFactoryImpl {
43
92
  schemaDefinition;
44
93
  statusMappings;
45
94
  alchemyScope;
95
+ singletonDefinitions;
96
+ singletonOwnerStatuses = new Map();
46
97
  logger = getComponentLogger('kro-factory');
47
98
  factoryOptions;
48
99
  clientManager;
@@ -54,6 +105,19 @@ export class KroResourceFactoryImpl {
54
105
  * refactors. Applies across both `toYaml()` and `ensureRGDDeployed()` paths.
55
106
  */
56
107
  ternaryAndOmitApplied = false;
108
+ compositionAnalysisApplied = false;
109
+ /**
110
+ * Cached plural form of the schema kind, discovered from the actual CRD
111
+ * created by KRO after RGD deployment. Populated by
112
+ * {@link waitForCRDReadyWithEngine}. Used by {@link getInstances} (and
113
+ * any other method that needs to list/read the custom resource) instead
114
+ * of guessing the plural from client-side heuristics.
115
+ *
116
+ * KRO's server-side pluralization is authoritative and not always
117
+ * derivable from client code — for example, already-plural kind
118
+ * names don't get an extra "s" suffix.
119
+ */
120
+ discoveredPlural;
57
121
  // Dependency-inversion providers (Phase 3.5) — injected via FactoryOptions
58
122
  // instead of dynamic import() from factories/ and alchemy/ layers.
59
123
  kroCustomResourceProvider;
@@ -69,9 +133,14 @@ export class KroResourceFactoryImpl {
69
133
  this.closures = options.closures || {};
70
134
  this.schemaDefinition = schemaDefinition;
71
135
  this.statusMappings = statusMappings;
136
+ this.singletonDefinitions = options.singletonDefinitions ?? [];
72
137
  this.factoryOptions = options;
73
138
  this.clientManager = new KubernetesClientManager(options);
74
- this.schema = createSchemaProxy();
139
+ // Pass the Arktype JSON so the proxy is shape-aware: spread
140
+ // (`{ ...schema.spec.X }`) enumerates declared fields and
141
+ // `Object.keys(schema.spec.X)` returns them. See the docstring on
142
+ // `createSchemaProxy` for why this matters for nested compositions.
143
+ this.schema = createSchemaProxy(schemaDefinition.spec?.json, schemaDefinition.status?.json);
75
144
  // Injected providers — fall back to dynamic import() for backward compatibility
76
145
  this.kroCustomResourceProvider = options.kroCustomResourceProvider;
77
146
  this.rgdProvider = options.rgdProvider;
@@ -83,6 +152,164 @@ export class KroResourceFactoryImpl {
83
152
  getNestedStatusCel() {
84
153
  return this.statusMappings?.__nestedStatusCel;
85
154
  }
155
+ getSchemaVersion() {
156
+ return this.schemaDefinition.apiVersion.includes('/')
157
+ ? this.schemaDefinition.apiVersion.split('/')[1] || this.schemaDefinition.apiVersion
158
+ : this.schemaDefinition.apiVersion;
159
+ }
160
+ getSchemaGroup() {
161
+ if (this.schemaDefinition.group)
162
+ return this.schemaDefinition.group;
163
+ return this.schemaDefinition.apiVersion.includes('/')
164
+ ? this.schemaDefinition.apiVersion.split('/')[0] || 'kro.run'
165
+ : 'kro.run';
166
+ }
167
+ getInstanceApiVersion() {
168
+ return `${this.getSchemaGroup()}/${this.getSchemaVersion()}`;
169
+ }
170
+ /**
171
+ * Idempotently create the factory's target namespace if it doesn't
172
+ * exist. KRO does not auto-create the CR's containing namespace, and
173
+ * users specify `{ namespace }` in factory options expecting it to
174
+ * "just work" without having to `kubectl create ns` first.
175
+ *
176
+ * Uses the Kubernetes Object API's create path with a 409-conflict
177
+ * tolerance — ignored if the namespace already exists so concurrent
178
+ * callers don't collide.
179
+ */
180
+ async ensureTargetNamespace(namespace = this.namespace) {
181
+ try {
182
+ const { createBunCompatibleKubernetesObjectApi } = await import('../kubernetes/bun-api-client.js');
183
+ const k8sApi = createBunCompatibleKubernetesObjectApi(this.getKubeConfig());
184
+ const waitForNamespaceDeletion = async () => {
185
+ const start = Date.now();
186
+ while (Date.now() - start < 120000) {
187
+ try {
188
+ const existing = (await k8sApi.read({
189
+ apiVersion: 'v1',
190
+ kind: 'Namespace',
191
+ metadata: { name: namespace },
192
+ }));
193
+ if (!existing.metadata?.deletionTimestamp) {
194
+ return;
195
+ }
196
+ }
197
+ catch (pollError) {
198
+ const err = pollError;
199
+ const code = err.statusCode ?? err.body?.code;
200
+ if (code === 404) {
201
+ return;
202
+ }
203
+ throw pollError;
204
+ }
205
+ await new Promise((resolve) => setTimeout(resolve, 1000));
206
+ }
207
+ throw new Error(`Namespace ${namespace} is still terminating after 120000ms`);
208
+ };
209
+ try {
210
+ const existing = (await k8sApi.read({
211
+ apiVersion: 'v1',
212
+ kind: 'Namespace',
213
+ metadata: { name: namespace },
214
+ }));
215
+ if (existing.metadata?.deletionTimestamp) {
216
+ await waitForNamespaceDeletion();
217
+ }
218
+ else {
219
+ // Already exists — nothing to do.
220
+ return;
221
+ }
222
+ }
223
+ catch (readError) {
224
+ const k8sErr = readError;
225
+ const code = k8sErr.statusCode ?? k8sErr.body?.code;
226
+ if (code !== 404) {
227
+ // Non-404 read failure — propagate with context.
228
+ throw readError;
229
+ }
230
+ }
231
+ // Namespace is missing — create it.
232
+ this.logger.info('Creating target namespace for Kro deployment', {
233
+ namespace,
234
+ });
235
+ try {
236
+ await k8sApi.create({
237
+ apiVersion: 'v1',
238
+ kind: 'Namespace',
239
+ metadata: {
240
+ name: namespace,
241
+ labels: {
242
+ 'app.kubernetes.io/managed-by': 'typekro',
243
+ },
244
+ },
245
+ });
246
+ }
247
+ catch (createError) {
248
+ const k8sErr = createError;
249
+ const code = k8sErr.statusCode ?? k8sErr.body?.code;
250
+ const isNamespaceTerminating = k8sErr.body?.reason === 'Forbidden' &&
251
+ k8sErr.message?.includes('NamespaceTerminating') === true;
252
+ if (isNamespaceTerminating) {
253
+ await waitForNamespaceDeletion();
254
+ await k8sApi.create({
255
+ apiVersion: 'v1',
256
+ kind: 'Namespace',
257
+ metadata: {
258
+ name: namespace,
259
+ labels: {
260
+ 'app.kubernetes.io/managed-by': 'typekro',
261
+ },
262
+ },
263
+ });
264
+ return;
265
+ }
266
+ // 409 = race with another caller — namespace now exists, treat as success.
267
+ if (code !== 409)
268
+ throw createError;
269
+ }
270
+ }
271
+ catch (error) {
272
+ throw new ResourceGraphFactoryError(`Failed to ensure target namespace "${this.namespace}" exists: ${ensureError(error).message}`, this.name, 'deployment', ensureError(error));
273
+ }
274
+ }
275
+ /**
276
+ * One-shot lookup of the CRD plural from the live cluster for this
277
+ * factory's (kind, group). Does NOT wait for establishment — expects
278
+ * the CRD to already exist. Returns `undefined` if the CRD is missing
279
+ * or the lookup fails, so callers can fall back to a heuristic.
280
+ *
281
+ * Used by paths like `getInstances` when invoked on a fresh factory
282
+ * instance (e.g., `--delete` from the CLI) where the wait-for-CRD
283
+ * step hasn't populated {@link discoveredPlural}.
284
+ */
285
+ async lookupCRDPlural() {
286
+ try {
287
+ const k8sApi = this.createKubernetesObjectApi();
288
+ const crds = (await k8sApi.list('apiextensions.k8s.io/v1', 'CustomResourceDefinition'));
289
+ const match = crds?.items?.find((crd) => crd.spec?.group === this.getSchemaGroup() &&
290
+ crd.spec?.names?.kind === this.schemaDefinition.kind);
291
+ return match?.spec?.names?.plural;
292
+ }
293
+ catch (error) {
294
+ this.logger.debug('CRD plural lookup failed — falling back to heuristic', {
295
+ kind: this.schemaDefinition.kind,
296
+ error: ensureError(error).message,
297
+ });
298
+ return undefined;
299
+ }
300
+ }
301
+ async requireCRDPluralForCleanup() {
302
+ if (!this.discoveredPlural) {
303
+ this.discoveredPlural = await this.lookupCRDPlural();
304
+ }
305
+ if (!this.discoveredPlural) {
306
+ throw new CRDInstanceError(`Cannot determine CRD plural for ${this.schemaDefinition.kind}; preserving RGD/CRD to avoid deleting shared KRO state`, this.schemaDefinition.apiVersion, this.schemaDefinition.kind, '*', 'deletion');
307
+ }
308
+ return this.discoveredPlural;
309
+ }
310
+ createKubernetesObjectApi() {
311
+ return createBunCompatibleKubernetesObjectApi(this.getKubeConfig());
312
+ }
86
313
  /**
87
314
  * Validate closures for Kro mode compatibility
88
315
  * Kro mode only supports static values - no dynamic references (KubernetesRef)
@@ -117,10 +344,31 @@ export class KroResourceFactoryImpl {
117
344
  getCustomObjectsApi() {
118
345
  return this.clientManager.getCustomObjectsApi();
119
346
  }
347
+ getDebugState() {
348
+ return {
349
+ mode: this.mode,
350
+ rgdName: this.rgdName,
351
+ namespace: this.namespace,
352
+ discoveredPlural: this.discoveredPlural,
353
+ clientManager: this.clientManager.getDebugState(),
354
+ };
355
+ }
356
+ async dispose() {
357
+ logHandleSnapshot(this.logger, 'kro-factory.dispose.before', {
358
+ factoryState: this.getDebugState(),
359
+ });
360
+ this.clientManager.dispose();
361
+ logHandleSnapshot(this.logger, 'kro-factory.dispose.after', {
362
+ factoryState: this.getDebugState(),
363
+ });
364
+ }
120
365
  /**
121
366
  * Deploy a new instance by creating a custom resource
122
367
  */
123
- async deploy(spec) {
368
+ async deploy(spec, opts) {
369
+ if (opts?.targetScopes !== undefined) {
370
+ throw new TypeKroError('Scope-targeted deployment is not supported in KRO mode. KRO manages resource lifecycle via its own controller. Use direct mode for scope-targeted deploys.', 'UNSUPPORTED_OPTION', { targetScopes: opts.targetScopes, mode: 'kro' });
371
+ }
124
372
  // Validate spec against ArkType schema
125
373
  validateSpec(spec, this.schemaDefinition, {
126
374
  kind: this.schemaDefinition.kind,
@@ -128,13 +376,107 @@ export class KroResourceFactoryImpl {
128
376
  });
129
377
  // Execute closures before RGD creation (Kro mode requirement)
130
378
  await this.executeClosuresBeforeRGD(spec);
379
+ await this.ensureSingletonOwners(spec);
131
380
  if (this.isAlchemyManaged) {
132
- return this.deployWithAlchemy(spec);
381
+ return this.deployWithAlchemy(spec, opts?.instanceNameOverride, opts?.singletonSpecFingerprint);
133
382
  }
134
383
  else {
135
- return this.deployDirect(spec);
384
+ return this.deployDirect(spec, opts?.instanceNameOverride, opts?.singletonSpecFingerprint);
385
+ }
386
+ }
387
+ async ensureSingletonOwners(spec) {
388
+ const discoveredSingletons = new Map();
389
+ if (this.factoryOptions.compositionFn) {
390
+ const singletonContext = createCompositionContext('singleton-owner-discovery');
391
+ runWithCompositionContext(singletonContext, () => {
392
+ this.factoryOptions.compositionFn?.(spec);
393
+ });
394
+ for (const [key, definition] of singletonContext.singletonDefinitions ?? []) {
395
+ discoveredSingletons.set(key, definition);
396
+ }
397
+ }
398
+ for (const definition of this.singletonDefinitions) {
399
+ if (!discoveredSingletons.has(definition.key)) {
400
+ discoveredSingletons.set(definition.key, definition);
401
+ }
402
+ }
403
+ if (discoveredSingletons.size === 0)
404
+ return;
405
+ const singletonRecords = new Map();
406
+ for (const definition of discoveredSingletons.values()) {
407
+ if (!singletonRecords.has(definition.key)) {
408
+ singletonRecords.set(definition.key, definition);
409
+ }
410
+ }
411
+ for (const definition of singletonRecords.values()) {
412
+ await this.ensureTargetNamespace(definition.registryNamespace);
413
+ const singletonInstanceName = getSingletonInstanceName(definition.id);
414
+ const singletonFactory = definition.composition.factory('kro', {
415
+ namespace: definition.registryNamespace,
416
+ waitForReady: true,
417
+ ...(this.factoryOptions.timeout !== undefined
418
+ ? { timeout: this.factoryOptions.timeout }
419
+ : {}),
420
+ ...(this.factoryOptions.kubeConfig !== undefined
421
+ ? { kubeConfig: this.factoryOptions.kubeConfig }
422
+ : {}),
423
+ ...(this.factoryOptions.skipTLSVerify !== undefined
424
+ ? { skipTLSVerify: this.factoryOptions.skipTLSVerify }
425
+ : {}),
426
+ });
427
+ try {
428
+ this.logger.info('Ensuring singleton owner boundary', {
429
+ singletonId: definition.id,
430
+ singletonKey: definition.key,
431
+ registryNamespace: definition.registryNamespace,
432
+ });
433
+ const existingInstances = await this.getSingletonOwnerInstancesForDriftCheck(singletonFactory, definition);
434
+ assertNoDeployedSingletonSpecDrift(definition, singletonInstanceName, existingInstances);
435
+ const singletonDeployOptions = {
436
+ instanceNameOverride: singletonInstanceName,
437
+ singletonSpecFingerprint: singletonSpecFingerprintAnnotationValue(definition.specFingerprint),
438
+ };
439
+ const deployedSingleton = await singletonFactory.deploy(definition.spec, singletonDeployOptions);
440
+ const singletonStatus = deployedSingleton.status;
441
+ if (singletonStatus &&
442
+ typeof singletonStatus === 'object' &&
443
+ !Array.isArray(singletonStatus)) {
444
+ this.singletonOwnerStatuses.set(getSingletonResourceId(definition.key), singletonStatus);
445
+ }
446
+ }
447
+ finally {
448
+ await singletonFactory.dispose?.();
449
+ }
450
+ }
451
+ }
452
+ async getSingletonOwnerInstancesForDriftCheck(singletonFactory, definition) {
453
+ try {
454
+ return await singletonFactory.getInstances();
455
+ }
456
+ catch (error) {
457
+ if (this.isMissingSingletonOwnerCrdError(error)) {
458
+ this.logger.debug('Singleton owner CRD is not installed yet; continuing with first deploy', {
459
+ singletonId: definition.id,
460
+ singletonKey: definition.key,
461
+ registryNamespace: definition.registryNamespace,
462
+ error: ensureError(error).message,
463
+ });
464
+ return [];
465
+ }
466
+ throw error;
136
467
  }
137
468
  }
469
+ isMissingSingletonOwnerCrdError(error) {
470
+ const err = error;
471
+ const body = typeof err.body === 'string' ? err.body : JSON.stringify(err.body ?? '');
472
+ const text = `${err.message ?? ''} ${body} ${String(error)}`.toLowerCase();
473
+ return (err.statusCode === 404 ||
474
+ err.code === 404 ||
475
+ text.includes('404') ||
476
+ text.includes('not found') ||
477
+ text.includes('no matches for kind') ||
478
+ text.includes('the server could not find the requested resource'));
479
+ }
138
480
  /**
139
481
  * Execute closures before RGD creation (Kro mode requirement)
140
482
  * Closures must execute before ResourceGraphDefinition is created
@@ -151,6 +493,7 @@ export class KroResourceFactoryImpl {
151
493
  // We need to execute them with a mock context to trigger validation
152
494
  const mockDeploymentContext = {
153
495
  // kubernetesApi intentionally omitted - not needed for validation
496
+ validationOnly: true,
154
497
  namespace: this.namespace,
155
498
  deployedResources: new Map(),
156
499
  resolveReference: async (ref) => {
@@ -224,14 +567,20 @@ export class KroResourceFactoryImpl {
224
567
  /**
225
568
  * Deploy directly to Kubernetes using DirectDeploymentEngine
226
569
  */
227
- async deployDirect(spec) {
570
+ async deployDirect(spec, instanceNameOverride, singletonSpecFingerprint) {
228
571
  // Ensure RGD is deployed first
229
572
  await this.ensureRGDDeployed();
573
+ // Ensure the target namespace exists before posting the CR. KRO
574
+ // reconciles resources from the RGD into their own namespaces, but
575
+ // the CR instance itself must live in a namespace the user can
576
+ // write to. Without this, the first deploy after `kubectl delete ns`
577
+ // fails with a 404 on the CR POST.
578
+ await this.ensureTargetNamespace();
230
579
  // Create DirectDeploymentEngine with KRO mode for CEL string conversion
231
580
  const deploymentEngine = new DirectDeploymentEngine(this.getKubeConfig(), undefined, undefined, DeploymentMode.KRO);
232
581
  // Create custom resource instance
233
- const instanceName = generateInstanceName(spec, this.name);
234
- const customResourceData = this.createCustomResourceInstance(instanceName, spec);
582
+ const instanceName = instanceNameOverride ?? generateInstanceName(spec, this.name);
583
+ const customResourceData = this.createCustomResourceInstance(instanceName, spec, singletonSpecFingerprint);
235
584
  // Wrap with kroCustomResource factory to get Enhanced object with readiness evaluation
236
585
  const kroCustomResource = this.kroCustomResourceProvider ??
237
586
  (await import('../../factories/kro/kro-custom-resource.js')).kroCustomResource;
@@ -239,8 +588,7 @@ export class KroResourceFactoryImpl {
239
588
  apiVersion: customResourceData.apiVersion,
240
589
  kind: customResourceData.kind,
241
590
  metadata: {
242
- name: customResourceData.metadata.name,
243
- namespace: customResourceData.metadata.namespace,
591
+ ...customResourceData.metadata,
244
592
  },
245
593
  spec: customResourceData.spec,
246
594
  });
@@ -259,148 +607,206 @@ export class KroResourceFactoryImpl {
259
607
  preserveNonEnumerableProperties(enhancedCustomResource, deployableResource);
260
608
  // Deploy without waiting for readiness - we'll handle that ourselves
261
609
  this.logger.info('Deploying Kro instance', { instanceName, rgdName: this.rgdName });
262
- await deploymentEngine.deployResource(deployableResource, {
263
- mode: 'kro',
264
- namespace: this.namespace,
265
- waitForReady: false, // We'll handle Kro-specific readiness ourselves
266
- timeout: this.factoryOptions.timeout || DEFAULT_DEPLOYMENT_TIMEOUT,
267
- });
268
- this.logger.info('Instance deployed, checking readiness', {
269
- instanceName,
270
- rgdName: this.rgdName,
271
- });
272
- // Handle Kro-specific readiness checking if requested
273
- if (this.factoryOptions.waitForReady ?? true) {
274
- await this.waitForKroInstanceReady(instanceName, this.factoryOptions.timeout || DEFAULT_KRO_INSTANCE_TIMEOUT); // 10 minutes
610
+ try {
611
+ await deploymentEngine.deployResource(deployableResource, {
612
+ mode: 'kro',
613
+ namespace: this.namespace,
614
+ waitForReady: false, // We'll handle Kro-specific readiness ourselves
615
+ timeout: this.factoryOptions.timeout || DEFAULT_DEPLOYMENT_TIMEOUT,
616
+ });
617
+ this.logger.info('Instance deployed, checking readiness', {
618
+ instanceName,
619
+ rgdName: this.rgdName,
620
+ });
621
+ // Handle Kro-specific readiness checking if requested
622
+ if (this.factoryOptions.waitForReady ?? true) {
623
+ await this.waitForKroInstanceReady(instanceName, this.factoryOptions.timeout || DEFAULT_KRO_INSTANCE_TIMEOUT); // 10 minutes
624
+ }
625
+ this.logger.info('Instance ready, creating enhanced proxy', {
626
+ instanceName,
627
+ rgdName: this.rgdName,
628
+ });
629
+ // Create Enhanced proxy for the deployed instance
630
+ return await this.createEnhancedProxy(spec, instanceName);
631
+ }
632
+ finally {
633
+ await deploymentEngine.dispose();
275
634
  }
276
- this.logger.info('Instance ready, creating enhanced proxy', {
277
- instanceName,
278
- rgdName: this.rgdName,
279
- });
280
- // Create Enhanced proxy for the deployed instance
281
- return await this.createEnhancedProxy(spec, instanceName);
282
635
  }
283
636
  /**
284
637
  * Deploy using type-safe alchemy resource wrapping
285
638
  *
286
639
  * In alchemy mode, the RGD gets one typed alchemy Resource and each instance gets another
287
640
  */
288
- async deployWithAlchemy(spec) {
289
- if (!this.alchemyScope) {
290
- throw new ResourceGraphFactoryError('Alchemy scope is required for alchemy deployment', this.name, 'deployment');
291
- }
641
+ async deployWithAlchemy(spec, instanceNameOverride, singletonSpecFingerprint) {
642
+ validateAlchemyScope(this.alchemyScope, 'KRO Alchemy deployment');
643
+ const alchemyScope = this.alchemyScope;
292
644
  // Use static registration functions
293
645
  // Create deployer instance using DirectDeploymentEngine with KRO mode
294
646
  const kroEngine = new DirectDeploymentEngine(this.getKubeConfig(), undefined, undefined, DeploymentMode.KRO);
295
- const deployer = this.alchemyBridge
296
- ? this.alchemyBridge.createDeployer(kroEngine)
297
- : new (await import('../../alchemy/deployment.js')).KroTypeKroDeployer(kroEngine);
298
- // 1. Ensure RGD is deployed via alchemy (once per factory)
299
- const kroSchema = generateKroSchemaFromArktype(this.name, this.schemaDefinition, this.resources, this.statusMappings, this.getNestedStatusCel());
300
- const rgdManifest = {
301
- apiVersion: 'kro.run/v1alpha1',
302
- kind: 'ResourceGraphDefinition',
303
- metadata: {
304
- name: this.rgdName,
305
- namespace: this.namespace,
306
- },
307
- spec: {
308
- schema: kroSchema,
309
- resources: Object.values(this.resources).map((resource) => ({
310
- id: getResourceId(resource),
311
- template: resource,
312
- })),
313
- },
314
- };
315
- // Register RGD type dynamically
316
- const rgdFactory = this.rgdProvider ??
317
- (await import('../../factories/kro/resource-graph-definition.js')).resourceGraphDefinition;
318
- const rgdEnhanced = rgdFactory(rgdManifest);
319
- const bridge = this.alchemyBridge ?? (await import('../../alchemy/deployment.js'));
320
- const RGDProvider = bridge.ensureResourceTypeRegistered(rgdEnhanced);
321
- const rgdId = bridge.createAlchemyResourceId(rgdEnhanced, this.namespace);
322
- await RGDProvider(rgdId, {
323
- resource: rgdEnhanced,
324
- namespace: this.namespace,
325
- deployer: deployer,
326
- options: {
327
- waitForReady: true,
328
- timeout: DEFAULT_RGD_TIMEOUT, // RGD should be ready quickly
329
- },
330
- });
331
- // 2. Create instance via alchemy (once per deploy call)
332
- const instanceName = generateInstanceName(spec, this.name);
333
- const crdInstanceManifest = this.createCustomResourceInstance(instanceName, spec);
334
- // Register CRD instance type dynamically
335
- // Cast required: crdInstanceManifest is a plain KubernetesResource, but alchemy functions
336
- // expect Enhanced<unknown, unknown>. They only access kind/metadata.name for type inference.
337
- const crdAsEnhanced = crdInstanceManifest;
338
- const CRDInstanceProvider = bridge.ensureResourceTypeRegistered(crdAsEnhanced);
339
- const instanceId = bridge.createAlchemyResourceId(crdAsEnhanced, this.namespace);
340
- await CRDInstanceProvider(instanceId, {
341
- resource: crdAsEnhanced,
342
- namespace: this.namespace,
343
- deployer: deployer,
344
- options: {
345
- waitForReady: this.factoryOptions.waitForReady ?? true,
346
- timeout: this.factoryOptions.timeout ?? DEFAULT_DEPLOYMENT_TIMEOUT,
347
- },
348
- });
349
- // Create Enhanced proxy for the deployed instance
350
- return await this.createEnhancedProxy(spec, instanceName);
647
+ const kubeConfigOptions = this.extractKubeConfigOptionsForAlchemy();
648
+ const kroDeletion = this.createAlchemyKroDeletionOptions();
649
+ try {
650
+ // 1. Ensure RGD is deployed via alchemy (once per factory). Reuse the
651
+ // normal serializer so externalRef/forEach/includeWhen/readyWhen and
652
+ // singleton owner boundaries match non-Alchemy KRO deploys.
653
+ const rgdManifest = yaml.load(this.buildRgdYaml());
654
+ // Register RGD type dynamically
655
+ const rgdFactory = this.rgdProvider ??
656
+ (await import('../../factories/kro/resource-graph-definition.js')).resourceGraphDefinition;
657
+ const rgdEnhanced = rgdFactory(rgdManifest);
658
+ const bridge = this.alchemyBridge ?? (await import('../../alchemy/deployment.js'));
659
+ const RGDProvider = bridge.ensureResourceTypeRegistered(rgdEnhanced);
660
+ const rgdId = bridge.createAlchemyResourceId(rgdEnhanced, this.namespace);
661
+ await alchemyScope.run(async () => {
662
+ await RGDProvider(rgdId, {
663
+ resource: rgdEnhanced,
664
+ namespace: this.namespace,
665
+ deploymentStrategy: 'kro',
666
+ kubeConfigOptions,
667
+ kroDeletion,
668
+ options: {
669
+ waitForReady: true,
670
+ timeout: DEFAULT_RGD_TIMEOUT, // RGD should be ready quickly
671
+ },
672
+ });
673
+ });
674
+ await this.waitForCRDReadyWithEngine(kroEngine);
675
+ // 2. Create instance via alchemy (once per deploy call)
676
+ await this.ensureTargetNamespace();
677
+ const instanceName = instanceNameOverride ?? generateInstanceName(spec, this.name);
678
+ const crdInstanceManifest = this.createCustomResourceInstance(instanceName, spec, singletonSpecFingerprint);
679
+ // Register CRD instance type dynamically
680
+ // Cast required: crdInstanceManifest is a plain KubernetesResource, but alchemy functions
681
+ // expect Enhanced<unknown, unknown>. They only access kind/metadata.name for type inference.
682
+ const crdAsEnhanced = crdInstanceManifest;
683
+ const CRDInstanceProvider = bridge.ensureResourceTypeRegistered(crdAsEnhanced);
684
+ const instanceId = bridge.createAlchemyResourceId(crdAsEnhanced, this.namespace);
685
+ await alchemyScope.run(async () => {
686
+ await CRDInstanceProvider(instanceId, {
687
+ resource: crdAsEnhanced,
688
+ namespace: this.namespace,
689
+ deploymentStrategy: 'kro',
690
+ kubeConfigOptions,
691
+ kroDeletion,
692
+ options: {
693
+ waitForReady: false,
694
+ timeout: this.factoryOptions.timeout ?? DEFAULT_DEPLOYMENT_TIMEOUT,
695
+ },
696
+ });
697
+ });
698
+ if (this.factoryOptions.waitForReady ?? true) {
699
+ await this.waitForKroInstanceReady(instanceName, this.factoryOptions.timeout || DEFAULT_KRO_INSTANCE_TIMEOUT);
700
+ }
701
+ // Create Enhanced proxy for the deployed instance
702
+ return await this.createEnhancedProxy(spec, instanceName);
703
+ }
704
+ finally {
705
+ await kroEngine.dispose();
706
+ }
351
707
  }
352
708
  /**
353
709
  * Get all deployed instances
354
710
  */
355
711
  async getInstances() {
356
- const kubeConfig = this.getKubeConfig();
357
- // Use Bun-compatible API client to ensure proper TLS handling
358
- const { createBunCompatibleCustomObjectsApi } = await import('../kubernetes/bun-api-client.js');
359
- const customApi = createBunCompatibleCustomObjectsApi(kubeConfig);
712
+ const customApi = await this.createCustomObjectsApi();
360
713
  try {
361
- // The schema definition should contain just the version part (e.g., 'v1alpha1')
362
- // If it somehow contains the full API version, extract just the version part
363
- const version = this.schemaDefinition.apiVersion.includes('/')
364
- ? this.schemaDefinition.apiVersion.split('/')[1] || this.schemaDefinition.apiVersion
365
- : this.schemaDefinition.apiVersion;
714
+ const version = this.getSchemaVersion();
715
+ // Prefer the server-discovered plural (populated after the CRD is
716
+ // created by KRO) over any client-side heuristic. For first-call
717
+ // paths like delete-on-fresh-factory, discover the plural lazily
718
+ // from the live CRD so already-plural kinds work correctly. Fall
719
+ // back to `pluralizeKind` only if the CRD list query failed (e.g.,
720
+ // missing RBAC) — in which case the list call below may also fail
721
+ // and surface a clearer error.
722
+ if (!this.discoveredPlural) {
723
+ this.discoveredPlural = await this.lookupCRDPlural();
724
+ }
725
+ const plural = this.discoveredPlural ?? pluralizeKind(this.schemaDefinition.kind);
366
726
  // In the new API, methods take request objects and return objects directly
367
727
  const listResponse = await customApi.listNamespacedCustomObject({
368
- group: 'kro.run',
728
+ group: this.getSchemaGroup(),
369
729
  version,
370
730
  namespace: this.namespace,
371
- plural: `${this.schemaDefinition.kind.toLowerCase()}s`, // Pluralize the kind
731
+ plural,
372
732
  });
373
733
  const listResult = listResponse;
374
734
  const instances = listResult.items || [];
375
735
  return await Promise.all(instances.map(async (instance) => {
376
- return await this.createEnhancedProxy(instance.spec, instance.metadata?.name || 'unknown');
736
+ const enhanced = await this.createEnhancedProxy(instance.spec, instance.metadata?.name || 'unknown');
737
+ if (instance.metadata?.annotations) {
738
+ const mutableEnhanced = enhanced;
739
+ const existingMetadata = mutableEnhanced.metadata ?? {};
740
+ const existingAnnotations = existingMetadata.annotations && typeof existingMetadata.annotations === 'object'
741
+ ? existingMetadata.annotations
742
+ : {};
743
+ mutableEnhanced.metadata = {
744
+ ...existingMetadata,
745
+ annotations: {
746
+ ...existingAnnotations,
747
+ ...instance.metadata.annotations,
748
+ },
749
+ };
750
+ }
751
+ return enhanced;
377
752
  }));
378
753
  }
379
754
  catch (error) {
380
755
  const k8sError = error;
381
756
  // If the CRD doesn't exist yet or there are no instances, return empty array
382
757
  const bodyString = typeof k8sError.body === 'string' ? k8sError.body : JSON.stringify(k8sError.body || '');
383
- if (k8sError.message?.includes('not found') ||
384
- k8sError.message?.includes('404') ||
385
- bodyString.includes('not found') ||
386
- bodyString.includes('404') ||
387
- k8sError.statusCode === 404 ||
388
- String(error).includes('404') ||
389
- String(error).includes('not found')) {
758
+ const canTreatNotFoundAsEmpty = !this.discoveredPlural;
759
+ if (canTreatNotFoundAsEmpty &&
760
+ (k8sError.message?.includes('not found') ||
761
+ k8sError.message?.includes('404') ||
762
+ bodyString.includes('not found') ||
763
+ bodyString.includes('404') ||
764
+ k8sError.statusCode === 404 ||
765
+ String(error).includes('404') ||
766
+ String(error).includes('not found'))) {
390
767
  return [];
391
768
  }
392
769
  throw new CRDInstanceError(`Failed to list instances: ${k8sError.message || String(error)}`, this.schemaDefinition.apiVersion, this.schemaDefinition.kind, '*', 'statusResolution', ensureError(error));
393
770
  }
394
771
  }
772
+ async listInstancesForCleanup() {
773
+ const customApi = await this.createCustomObjectsApi();
774
+ const version = this.getSchemaVersion();
775
+ const plural = await this.requireCRDPluralForCleanup();
776
+ try {
777
+ const listResponse = await customApi.listClusterCustomObject({
778
+ group: this.getSchemaGroup(),
779
+ version,
780
+ plural,
781
+ });
782
+ const listResult = listResponse;
783
+ return listResult.items ?? [];
784
+ }
785
+ catch (error) {
786
+ throw new CRDInstanceError(`Failed to list instances cluster-wide: ${ensureError(error).message}`, this.schemaDefinition.apiVersion, this.schemaDefinition.kind, '*', 'statusResolution', ensureError(error));
787
+ }
788
+ }
789
+ async createCustomObjectsApi() {
790
+ const kubeConfig = this.getKubeConfig();
791
+ // Use Bun-compatible API client to ensure proper TLS handling
792
+ const { createBunCompatibleCustomObjectsApi } = await import('../kubernetes/bun-api-client.js');
793
+ return createBunCompatibleCustomObjectsApi(kubeConfig);
794
+ }
395
795
  /**
396
796
  * Delete a specific instance by name
397
797
  */
398
- async deleteInstance(name) {
798
+ async deleteInstance(name, opts) {
799
+ if (opts?.scopes?.length) {
800
+ throw new TypeKroError('Scope-filtered deletion is not supported in KRO mode. KRO manages resource lifecycle via its own controller. Use direct mode for scope-filtered deletes.', 'UNSUPPORTED_OPTION', { scopes: opts.scopes, instanceName: name, mode: 'kro' });
801
+ }
399
802
  const kubeConfig = this.getKubeConfig();
400
803
  const k8sApi = createBunCompatibleKubernetesObjectApi(kubeConfig);
401
- const apiVersion = this.schemaDefinition.apiVersion.includes('/')
402
- ? this.schemaDefinition.apiVersion
403
- : `kro.run/${this.schemaDefinition.apiVersion}`;
804
+ const apiVersion = this.getInstanceApiVersion();
805
+ // Tracks whether the CR was confirmed 404 by the poll loop. Used
806
+ // later to decide whether to tear down the RGD/CRD or preserve
807
+ // them for KRO to continue finalizer processing in the background.
808
+ let instanceDeleted = false;
809
+ let deletionTimedOut = false;
404
810
  try {
405
811
  // Delete the instance. KRO's controller processes kro.run/finalizer,
406
812
  // which does graph-based deletion of all child resources.
@@ -415,12 +821,8 @@ export class KroResourceFactoryImpl {
415
821
  // Wait for KRO to finish cleanup (finalizer processing).
416
822
  // KRO needs the RGD to exist during this phase — the caller must
417
823
  // not delete the RGD until deleteInstance completes.
418
- // Cap at 5 minutes — deletion should be faster than deployment.
419
- // KRO processes ~15-30s per resource for finalizer cleanup.
420
- const MAX_DELETION_WAIT = 300000;
421
- const timeout = Math.min(this.factoryOptions.timeout ?? 120000, MAX_DELETION_WAIT);
824
+ const timeout = this.factoryOptions.timeout ?? 300000;
422
825
  const startTime = Date.now();
423
- let deleted = false;
424
826
  while (Date.now() - startTime < timeout) {
425
827
  try {
426
828
  await k8sApi.read({
@@ -429,13 +831,13 @@ export class KroResourceFactoryImpl {
429
831
  metadata: { name, namespace: this.namespace },
430
832
  });
431
833
  // Still exists — KRO is processing finalizer
432
- await new Promise(r => setTimeout(r, 2000));
834
+ await new Promise((r) => setTimeout(r, 2000));
433
835
  }
434
836
  catch (pollError) {
435
837
  const pollK8sError = pollError;
436
838
  const errorCode = pollK8sError.statusCode ?? pollK8sError.code ?? pollK8sError.body?.code;
437
839
  if (errorCode === 404) {
438
- deleted = true;
840
+ instanceDeleted = true;
439
841
  break;
440
842
  }
441
843
  // Non-404 error (permissions, server error) — log and retry
@@ -443,14 +845,14 @@ export class KroResourceFactoryImpl {
443
845
  name,
444
846
  errorCode,
445
847
  });
446
- await new Promise(r => setTimeout(r, 2000));
848
+ await new Promise((r) => setTimeout(r, 2000));
447
849
  }
448
850
  }
449
- if (!deleted) {
450
- // KRO is still processing the finalizer. Don't throw the deletion
451
- // IS in progress. The hasRemainingInstances guard below will see the
452
- // instance (still DELETING) and skip RGD/CRD deletion, preserving
453
- // them so KRO can complete cleanup in the background.
851
+ if (!instanceDeleted) {
852
+ // KRO is still processing the finalizer. Treat the stuck instance as
853
+ // unsafe to clean up, so the RGD/CRD are preserved.
854
+ // Deleting the RGD while KRO is mid-finalizer would orphan cleanup.
855
+ deletionTimedOut = true;
454
856
  this.logger.warn('Instance deletion still in progress after timeout', {
455
857
  name,
456
858
  timeout,
@@ -462,19 +864,24 @@ export class KroResourceFactoryImpl {
462
864
  catch (error) {
463
865
  const k8sError = error;
464
866
  const errorCode = k8sError.statusCode ?? k8sError.code ?? k8sError.body?.code;
465
- if (errorCode !== 404) {
867
+ if (errorCode === 404) {
868
+ instanceDeleted = true;
869
+ }
870
+ else {
466
871
  throw new CRDInstanceError(`Failed to delete instance ${name}: ${k8sError.message || String(error)}`, this.schemaDefinition.apiVersion, this.schemaDefinition.kind, name, 'deletion', ensureError(error));
467
872
  }
468
873
  }
874
+ if (deletionTimedOut) {
875
+ throw new CRDInstanceError(`KRO instance ${name} deletion did not complete within ${this.factoryOptions.timeout ?? 300000}ms`, this.schemaDefinition.apiVersion, this.schemaDefinition.kind, name, 'deletion');
876
+ }
469
877
  // Only delete the RGD and CRD if no other instances remain. Multiple
470
878
  // instances can share one RGD — deleting it would break the others.
879
+ // The decision is a pure function of (listed instances, target name,
880
+ // instanceDeleted flag) — see {@link shouldPreserveRgd} for the rules.
471
881
  let hasRemainingInstances = false;
472
882
  try {
473
- const instances = await this.getInstances();
474
- // Filter out the just-deleted instance — it may still appear in the list
475
- // if the API server's list cache hasn't caught up with the 404 on GET.
476
- const others = instances.filter(i => i.metadata?.name !== name);
477
- hasRemainingInstances = others.length > 0;
883
+ const instances = await this.listInstancesForCleanup();
884
+ hasRemainingInstances = shouldPreserveRgd(instances, name, instanceDeleted, this.namespace);
478
885
  }
479
886
  catch (listError) {
480
887
  // Can't list instances — could be CRD gone (safe) or transient error
@@ -487,6 +894,10 @@ export class KroResourceFactoryImpl {
487
894
  hasRemainingInstances = true;
488
895
  }
489
896
  if (!hasRemainingInstances) {
897
+ // Prove the generated CRD can be cleaned up before deleting the RGD.
898
+ // If plural discovery is unavailable, preserving both avoids orphaning
899
+ // the generated CRD without its owning RGD.
900
+ const crdPlural = await this.requireCRDPluralForCleanup();
490
901
  // Delete the RGD after the instance is gone.
491
902
  try {
492
903
  await k8sApi.delete({
@@ -500,13 +911,18 @@ export class KroResourceFactoryImpl {
500
911
  const k8sErr = error;
501
912
  const errorCode = k8sErr.statusCode ?? k8sErr.code ?? k8sErr.body?.code;
502
913
  if (errorCode !== 404) {
503
- this.logger.warn('RGD cleanup failed', { rgdName: this.rgdName, error: ensureError(error).message });
914
+ this.logger.warn('RGD cleanup failed', {
915
+ rgdName: this.rgdName,
916
+ error: ensureError(error).message,
917
+ });
918
+ throw error;
504
919
  }
505
920
  }
506
921
  // Delete the CRD that KRO created from the RGD. KRO's default config
507
922
  // has allowCRDDeletion=false, so it won't clean up the CRD when the
508
- // RGD is deleted.
509
- const crdName = `${this.schemaDefinition.kind.toLowerCase()}s.kro.run`;
923
+ // RGD is deleted. Prefer the server-discovered plural over the
924
+ // heuristic fallback so already-plural kinds clean up correctly.
925
+ const crdName = `${crdPlural}.${this.getSchemaGroup()}`;
510
926
  try {
511
927
  await k8sApi.delete({
512
928
  apiVersion: 'apiextensions.k8s.io/v1',
@@ -519,7 +935,8 @@ export class KroResourceFactoryImpl {
519
935
  const k8sErr = error;
520
936
  const errorCode = k8sErr.statusCode ?? k8sErr.code ?? k8sErr.body?.code;
521
937
  if (errorCode !== 404) {
522
- this.logger.debug('CRD cleanup failed (non-critical)', { crdName, error: ensureError(error).message });
938
+ this.logger.warn('CRD cleanup failed', { crdName, error: ensureError(error).message });
939
+ throw error;
523
940
  }
524
941
  }
525
942
  }
@@ -532,6 +949,52 @@ export class KroResourceFactoryImpl {
532
949
  // KRO's finalizer processing handles deleting all child resources
533
950
  // (including Namespaces) via its applyset — no manual cleanup needed.
534
951
  }
952
+ extractKubeConfigOptionsForAlchemy() {
953
+ const kc = this.getKubeConfig();
954
+ const cluster = kc.getCurrentCluster();
955
+ const user = typeof kc.getCurrentUser === 'function' ? kc.getCurrentUser() : undefined;
956
+ const context = typeof kc.getCurrentContext === 'function' ? kc.getCurrentContext() : undefined;
957
+ const finalSkipTLS = this.factoryOptions.skipTLSVerify === true ? true : (cluster?.skipTLSVerify ?? false);
958
+ return {
959
+ skipTLSVerify: finalSkipTLS,
960
+ ...(cluster?.server && { server: cluster.server }),
961
+ ...(context && { context }),
962
+ ...(cluster && {
963
+ cluster: {
964
+ name: cluster.name,
965
+ server: cluster.server,
966
+ skipTLSVerify: finalSkipTLS,
967
+ ...(cluster.caData && { caData: cluster.caData }),
968
+ ...(cluster.caFile && { caFile: cluster.caFile }),
969
+ },
970
+ }),
971
+ ...(user && {
972
+ user: {
973
+ name: user.name,
974
+ ...(user.token && { token: user.token }),
975
+ ...(user.certData && { certData: user.certData }),
976
+ ...(user.certFile && { certFile: user.certFile }),
977
+ ...(user.keyData && { keyData: user.keyData }),
978
+ ...(user.keyFile && { keyFile: user.keyFile }),
979
+ ...(user.exec ? { exec: user.exec } : {}),
980
+ ...(user.authProvider
981
+ ? { authProvider: user.authProvider }
982
+ : {}),
983
+ },
984
+ }),
985
+ };
986
+ }
987
+ createAlchemyKroDeletionOptions() {
988
+ return {
989
+ apiVersion: this.schemaDefinition.apiVersion,
990
+ kind: this.schemaDefinition.kind,
991
+ ...(this.schemaDefinition.group && { group: this.schemaDefinition.group }),
992
+ namespace: this.namespace,
993
+ rgdName: this.rgdName,
994
+ ...(this.discoveredPlural && { plural: this.discoveredPlural }),
995
+ timeout: this.factoryOptions.timeout ?? 300000,
996
+ };
997
+ }
535
998
  /**
536
999
  * Get factory status
537
1000
  */
@@ -560,7 +1023,6 @@ export class KroResourceFactoryImpl {
560
1023
  kind: 'ResourceGraphDefinition',
561
1024
  metadata: {
562
1025
  name: this.rgdName,
563
- namespace: this.namespace,
564
1026
  },
565
1027
  });
566
1028
  const rgd = response;
@@ -611,19 +1073,9 @@ export class KroResourceFactoryImpl {
611
1073
  // Generate CRD instance YAML
612
1074
  const instanceName = generateInstanceName(spec, this.name);
613
1075
  const customResource = this.createCustomResourceInstance(instanceName, spec);
614
- return `apiVersion: ${customResource.apiVersion}
615
- kind: ${customResource.kind}
616
- metadata:
617
- name: ${customResource.metadata.name}
618
- namespace: ${customResource.metadata.namespace}
619
- spec:
620
- ${Object.entries(spec)
621
- .map(([key, value]) => ` ${key}: ${typeof value === 'string' ? `"${value}"` : value}`)
622
- .join('\n')}`;
623
- }
624
- else {
625
- return this.buildRgdYaml();
1076
+ return yaml.dump(customResource, { lineWidth: -1, noRefs: true, sortKeys: false }).trimEnd();
626
1077
  }
1078
+ return this.buildRgdYaml();
627
1079
  }
628
1080
  /**
629
1081
  * Build the RGD YAML string shared by `toYaml()` and `ensureRGDDeployed()`.
@@ -638,7 +1090,28 @@ ${Object.entries(spec)
638
1090
  * populates from `kroSchema.__omitFields` automatically.
639
1091
  */
640
1092
  buildRgdYaml() {
1093
+ if (this.factoryOptions.compositionAnalysis && !this.compositionAnalysisApplied) {
1094
+ this.compositionAnalysisApplied = true;
1095
+ applyAnalysisToResources(this.resources, this.factoryOptions.compositionAnalysis);
1096
+ }
641
1097
  const kroSchema = generateKroSchemaFromArktype(this.name, this.schemaDefinition, this.resources, this.statusMappings, this.getNestedStatusCel());
1098
+ // Attach nested status CEL mappings as non-enumerable property so
1099
+ // serializeResourceGraphToYaml can inline virtual composition IDs.
1100
+ const nestedCel = this.getNestedStatusCel();
1101
+ if (nestedCel && Object.keys(nestedCel).length > 0) {
1102
+ Object.defineProperty(kroSchema, '__nestedStatusCel', {
1103
+ value: nestedCel,
1104
+ enumerable: false,
1105
+ });
1106
+ }
1107
+ const statusOverrides = this.factoryOptions.compositionAnalysis?.statusOverrides ?? [];
1108
+ if (statusOverrides.length > 0) {
1109
+ kroSchema.status ??= {};
1110
+ for (const override of statusOverrides) {
1111
+ const yamlSafe = override.celExpression.replace(/"([^"\\]*)"/g, "'$1'");
1112
+ kroSchema.status[override.propertyPath] = yamlSafe;
1113
+ }
1114
+ }
642
1115
  // Apply ternary conditionals to this.resources BEFORE serialization.
643
1116
  // Mutates in place (JSON.clone is NOT safe here because it strips
644
1117
  // proxy-valued fields like `metadata.namespace` that are KubernetesRef
@@ -649,11 +1122,67 @@ ${Object.entries(spec)
649
1122
  if (!this.ternaryAndOmitApplied) {
650
1123
  this.ternaryAndOmitApplied = true;
651
1124
  if (kroSchema.__ternaryConditionals?.length) {
652
- applyTernaryConditionalsToResources(this.resources, kroSchema.__ternaryConditionals);
1125
+ applyTernaryConditionalsToResources(this.resources, kroSchema.__ternaryConditionals, kroSchema.__nestedStatusCel);
653
1126
  }
654
1127
  }
655
1128
  return serializeResourceGraphToYaml(this.rgdName, this.resources, { namespace: this.namespace }, kroSchema);
656
1129
  }
1130
+ static asRecord(value) {
1131
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
1132
+ return undefined;
1133
+ }
1134
+ return value;
1135
+ }
1136
+ static getRgdSchemaStatusMap(rgd) {
1137
+ const resource = KroResourceFactoryImpl.asRecord(rgd);
1138
+ const spec = KroResourceFactoryImpl.asRecord(resource?.spec);
1139
+ const schema = KroResourceFactoryImpl.asRecord(spec?.schema);
1140
+ return KroResourceFactoryImpl.asRecord(schema?.status);
1141
+ }
1142
+ /**
1143
+ * Kubernetes merge-patch preserves omitted map keys. RGD status schema fields
1144
+ * that are removed from a composition must be sent as `null` so stale CEL does
1145
+ * not survive on an existing cluster-scoped RGD.
1146
+ */
1147
+ async addRgdSchemaStatusPruneMarkers(rgdManifest) {
1148
+ const k8sApi = createBunCompatibleKubernetesObjectApi(this.getKubeConfig());
1149
+ let existing;
1150
+ try {
1151
+ existing = await k8sApi.read({
1152
+ apiVersion: 'kro.run/v1alpha1',
1153
+ kind: 'ResourceGraphDefinition',
1154
+ metadata: { name: this.rgdName },
1155
+ });
1156
+ }
1157
+ catch (error) {
1158
+ if (isNotFoundError(error)) {
1159
+ return;
1160
+ }
1161
+ this.logger.debug('Skipping RGD status schema prune markers; existing RGD could not be read', {
1162
+ rgdName: this.rgdName,
1163
+ error: ensureError(error).message,
1164
+ });
1165
+ return;
1166
+ }
1167
+ const existingStatus = KroResourceFactoryImpl.getRgdSchemaStatusMap(existing);
1168
+ const desiredStatus = KroResourceFactoryImpl.getRgdSchemaStatusMap(rgdManifest);
1169
+ if (!existingStatus || !desiredStatus) {
1170
+ return;
1171
+ }
1172
+ const removedFields = [];
1173
+ for (const fieldName of Object.keys(existingStatus)) {
1174
+ if (!(fieldName in desiredStatus)) {
1175
+ desiredStatus[fieldName] = null;
1176
+ removedFields.push(fieldName);
1177
+ }
1178
+ }
1179
+ if (removedFields.length > 0) {
1180
+ this.logger.debug('Adding RGD status schema prune markers', {
1181
+ rgdName: this.rgdName,
1182
+ removedFields,
1183
+ });
1184
+ }
1185
+ }
657
1186
  /**
658
1187
  * Ensure the ResourceGraphDefinition is deployed using DirectDeploymentEngine
659
1188
  */
@@ -672,9 +1201,9 @@ ${Object.entries(spec)
672
1201
  metadata: {
673
1202
  ...rgdManifest.metadata,
674
1203
  name: this.rgdName,
675
- namespace: this.namespace,
676
1204
  },
677
1205
  };
1206
+ await this.addRgdSchemaStatusPruneMarkers(rgdWithMetadata);
678
1207
  // Create Enhanced RGD with readiness evaluator
679
1208
  const rgdFactory = this.rgdProvider ??
680
1209
  (await import('../../factories/kro/resource-graph-definition.js')).resourceGraphDefinition;
@@ -686,6 +1215,7 @@ ${Object.entries(spec)
686
1215
  };
687
1216
  // Preserve non-enumerable properties (readinessEvaluator, __resourceId) lost during spread
688
1217
  preserveNonEnumerableProperties(enhancedRGD, deployableRGD);
1218
+ setMetadataField(deployableRGD, 'scope', 'cluster');
689
1219
  // Debug: Log the RGD being deployed
690
1220
  this.logger.debug('Deploying RGD', {
691
1221
  rgdName: this.rgdName,
@@ -714,7 +1244,7 @@ ${Object.entries(spec)
714
1244
  const rgdStatus = await k8sApi.read({
715
1245
  apiVersion: 'kro.run/v1alpha1',
716
1246
  kind: 'ResourceGraphDefinition',
717
- metadata: { name: this.rgdName, namespace: this.namespace },
1247
+ metadata: { name: this.rgdName },
718
1248
  });
719
1249
  const rgdResult = rgdStatus;
720
1250
  this.logger.error('RGD deployment failed, current status:', undefined, {
@@ -728,19 +1258,24 @@ ${Object.entries(spec)
728
1258
  }
729
1259
  throw new ResourceGraphFactoryError(`Failed to deploy RGD using DirectDeploymentEngine: ${ensureError(error).message}`, this.name, 'deployment', ensureError(error));
730
1260
  }
1261
+ finally {
1262
+ await deploymentEngine.dispose();
1263
+ }
731
1264
  }
732
1265
  /**
733
- * Wait for the CRD to be created by Kro using DirectDeploymentEngine
1266
+ * Wait for the CRD to be created by Kro using DirectDeploymentEngine.
1267
+ *
1268
+ * Discovers the CRD by (kind, group=kro.run) via the CRD list API
1269
+ * rather than pre-computing a plural form. KRO's server-side pluralization
1270
+ * is authoritative, and client-side heuristics cannot handle all cases
1271
+ * (e.g., already-plural kind names that shouldn't get an extra "s").
734
1272
  */
735
1273
  async waitForCRDReadyWithEngine(deploymentEngine) {
736
- const crdName = `${pluralizeKind(this.schemaDefinition.kind)}.kro.run`;
737
- // Debug: Check if the method exists
738
- if (typeof deploymentEngine.waitForCRDReady !== 'function') {
739
- throw new ResourceGraphFactoryError(`deploymentEngine.waitForCRDReady is not a function. Available methods: ${Object.getOwnPropertyNames(Object.getPrototypeOf(deploymentEngine)).join(', ')}`, this.name, 'deployment');
740
- }
741
- // Use the deployment engine's built-in CRD readiness checking
742
- // This will wait for the CRD to be created by Kro and become ready
743
- await deploymentEngine.waitForCRDReady(crdName, this.factoryOptions.timeout || DEFAULT_RGD_TIMEOUT);
1274
+ if (typeof deploymentEngine.waitForCRDByKindAndGroup !== 'function') {
1275
+ throw new ResourceGraphFactoryError(`deploymentEngine.waitForCRDByKindAndGroup is not a function. Available methods: ${Object.getOwnPropertyNames(Object.getPrototypeOf(deploymentEngine)).join(', ')}`, this.name, 'deployment');
1276
+ }
1277
+ const { plural } = await deploymentEngine.waitForCRDByKindAndGroup(this.schemaDefinition.kind, this.getSchemaGroup(), this.factoryOptions.timeout || DEFAULT_RGD_TIMEOUT);
1278
+ this.discoveredPlural = plural;
744
1279
  }
745
1280
  /**
746
1281
  * Separate static and dynamic status fields
@@ -759,61 +1294,78 @@ ${Object.entries(spec)
759
1294
  async evaluateStaticFields(staticFields, spec) {
760
1295
  const evaluatedFields = {};
761
1296
  for (const [fieldName, fieldValue] of Object.entries(staticFields)) {
762
- if (this.isCelExpression(fieldValue)) {
763
- try {
764
- // Evaluate CEL expressions that contain only schema references
765
- const evaluatedValue = this.evaluateStaticCelExpression(fieldValue, spec);
766
- evaluatedFields[fieldName] = evaluatedValue;
767
- }
768
- catch (error) {
769
- this.logger.warn('Failed to evaluate static CEL expression', {
770
- field: fieldName,
771
- expression: fieldValue.expression,
772
- error: ensureError(error).message,
773
- });
774
- // Fallback to the original value
775
- evaluatedFields[fieldName] = fieldValue;
776
- }
1297
+ evaluatedFields[fieldName] = await this.evaluateStaticFieldValue(fieldValue, spec, fieldName);
1298
+ }
1299
+ return evaluatedFields;
1300
+ }
1301
+ async evaluateStaticFieldValue(fieldValue, spec, fieldName) {
1302
+ if (this.isCelExpression(fieldValue)) {
1303
+ try {
1304
+ // Evaluate CEL expressions that contain only schema references
1305
+ return this.evaluateStaticCelExpression(fieldValue, spec);
777
1306
  }
778
- else if (typeof fieldValue === 'string' &&
779
- fieldValue.includes('__KUBERNETES_REF___schema___')) {
780
- // Resolve __KUBERNETES_REF_ marker strings from template literal coercion.
781
- // When the composition function uses template literals like `${spec.name}-suffix`,
782
- // the proxy's Symbol.toPrimitive produces marker strings at runtime. These need
783
- // to be resolved to actual spec values at deploy time.
784
- evaluatedFields[fieldName] = this.resolveSchemaRefMarkers(fieldValue, spec);
1307
+ catch (error) {
1308
+ this.logger.warn('Failed to evaluate static CEL expression', {
1309
+ field: fieldName,
1310
+ expression: fieldValue.expression,
1311
+ error: ensureError(error).message,
1312
+ });
1313
+ // Fallback to the original value
1314
+ return fieldValue;
785
1315
  }
786
- else if (typeof fieldValue === 'string' &&
787
- fieldValue.startsWith('${') &&
788
- fieldValue.endsWith('}')) {
789
- // Evaluate inline CEL expression strings produced by the composition AST analyzer.
790
- // statusOverrides from analyzeCompositionBody write ternary/conditional expressions
791
- // as plain strings like "${schema.spec.enabled ? 2 : 1}" into statusMappings.
792
- // These must be evaluated with actual spec values at deploy time.
793
- try {
794
- evaluatedFields[fieldName] = this.evaluateInlineCelString(fieldValue, spec);
795
- }
796
- catch (error) {
797
- this.logger.warn('Failed to evaluate inline CEL expression string', {
798
- field: fieldName,
799
- expression: fieldValue,
800
- error: ensureError(error).message,
801
- });
802
- evaluatedFields[fieldName] = fieldValue;
803
- }
1316
+ }
1317
+ if (isKubernetesRef(fieldValue)) {
1318
+ if (fieldValue.resourceId === '__schema__') {
1319
+ return this.resolveSchemaRefValue(fieldValue.fieldPath, spec);
804
1320
  }
805
- else if (typeof fieldValue === 'object' &&
806
- fieldValue !== null &&
807
- !Array.isArray(fieldValue)) {
808
- // Recursively evaluate nested objects
809
- evaluatedFields[fieldName] = await this.evaluateStaticFields(fieldValue, spec);
1321
+ return fieldValue;
1322
+ }
1323
+ if (typeof fieldValue === 'string' && fieldValue.includes('__KUBERNETES_REF___schema___')) {
1324
+ // Resolve __KUBERNETES_REF_ marker strings from template literal coercion.
1325
+ // When the composition function uses template literals like `${spec.name}-suffix`,
1326
+ // the proxy's Symbol.toPrimitive produces marker strings at runtime. These need
1327
+ // to be resolved to actual spec values at deploy time.
1328
+ return this.resolveSchemaRefMarkers(fieldValue, spec);
1329
+ }
1330
+ if (typeof fieldValue === 'string' && fieldValue.startsWith('${') && fieldValue.endsWith('}')) {
1331
+ // Evaluate inline CEL expression strings produced by the composition AST analyzer.
1332
+ // statusOverrides from analyzeCompositionBody write ternary/conditional expressions
1333
+ // as plain strings like "${schema.spec.enabled ? 2 : 1}" into statusMappings.
1334
+ // These must be evaluated with actual spec values at deploy time.
1335
+ try {
1336
+ return this.evaluateInlineCelString(fieldValue, spec);
1337
+ }
1338
+ catch (error) {
1339
+ this.logger.warn('Failed to evaluate inline CEL expression string', {
1340
+ field: fieldName,
1341
+ expression: fieldValue,
1342
+ error: ensureError(error).message,
1343
+ });
1344
+ return fieldValue;
1345
+ }
1346
+ }
1347
+ if (Array.isArray(fieldValue)) {
1348
+ return Promise.all(fieldValue.map((item, index) => this.evaluateStaticFieldValue(item, spec, `${fieldName}[${index}]`)));
1349
+ }
1350
+ if (typeof fieldValue === 'object' && fieldValue !== null) {
1351
+ // Recursively evaluate nested objects
1352
+ return this.evaluateStaticFields(fieldValue, spec);
1353
+ }
1354
+ // Keep non-CEL values as-is
1355
+ return fieldValue;
1356
+ }
1357
+ resolveSchemaRefValue(fieldPath, spec) {
1358
+ const parts = fieldPath.replace(/^spec\./, '').split('.');
1359
+ let current = spec;
1360
+ for (const part of parts) {
1361
+ if (current != null && typeof current === 'object') {
1362
+ current = current[part];
810
1363
  }
811
1364
  else {
812
- // Keep non-CEL values as-is
813
- evaluatedFields[fieldName] = fieldValue;
1365
+ return undefined;
814
1366
  }
815
1367
  }
816
- return evaluatedFields;
1368
+ return current;
817
1369
  }
818
1370
  /**
819
1371
  * Evaluate a static CEL expression that contains only schema references or literal values.
@@ -824,22 +1376,8 @@ ${Object.entries(spec)
824
1376
  */
825
1377
  evaluateStaticCelExpression(celExpression, spec) {
826
1378
  const expression = celExpression.expression;
827
- // Use null-prototype object to prevent prototype chain access (defense-in-depth).
828
- // angular-expressions has hasOwnProperty guards, but a null-prototype scope
829
- // eliminates any residual risk from constructor/toString/__proto__ leaking.
830
- // Object.freeze prevents expression-based mutation of the original spec data.
831
- const specRecord = Object.freeze(Object.assign(Object.create(null), spec));
832
- // Build a scope expression by stripping schema.spec. or spec. prefixes so that
833
- // angular-expressions can resolve field references directly from the spec scope.
834
- let scopeExpression = expression;
835
- if (expression.includes('schema.spec.')) {
836
- // Replace schema.spec.fieldName → fieldName (resolved from scope)
837
- scopeExpression = scopeExpression.replace(/schema\.spec\.(\w+)/g, '$1');
838
- }
839
- if (scopeExpression.includes('spec.')) {
840
- // Replace spec.fieldName → fieldName (resolved from scope)
841
- scopeExpression = scopeExpression.replace(/\bspec\.(\w+)/g, '$1');
842
- }
1379
+ const specRecord = this.createStaticEvaluationScope(spec);
1380
+ const scopeExpression = this.prepareStaticExpressionForEvaluation(expression);
843
1381
  try {
844
1382
  const evaluator = compileExpression(scopeExpression);
845
1383
  const result = evaluator(specRecord);
@@ -871,7 +1409,7 @@ ${Object.entries(spec)
871
1409
  * At deploy time we replace each marker with the actual spec value.
872
1410
  */
873
1411
  resolveSchemaRefMarkers(value, spec) {
874
- const resolved = value.replace(/__KUBERNETES_REF___schema___([a-zA-Z0-9.$]+)__/g, (_match, fieldPath) => {
1412
+ const resolved = value.replace(new RegExp(KUBERNETES_REF_SCHEMA_MARKER_SOURCE, 'g'), (_match, fieldPath) => {
875
1413
  // fieldPath is e.g. "spec.name" or "spec.nested.field"
876
1414
  const parts = fieldPath.replace(/^spec\./, '').split('.');
877
1415
  let current = spec;
@@ -902,18 +1440,12 @@ ${Object.entries(spec)
902
1440
  evaluateInlineCelString(celString, spec) {
903
1441
  // Strip the wrapping ${ ... }
904
1442
  const innerExpression = celString.slice(2, -1);
905
- // Build scope expression: strip schema.spec. / spec. prefixes
906
- let scopeExpression = innerExpression;
907
- if (scopeExpression.includes('schema.spec.')) {
908
- scopeExpression = scopeExpression.replace(/schema\.spec\.(\w+)/g, '$1');
909
- }
910
- if (scopeExpression.includes('spec.')) {
911
- scopeExpression = scopeExpression.replace(/\bspec\.(\w+)/g, '$1');
912
- }
1443
+ // Build scope expression: translate KRO helpers and strip schema.spec. / spec. prefixes
1444
+ let scopeExpression = this.prepareStaticExpressionForEvaluation(innerExpression);
913
1445
  // Resolve any __KUBERNETES_REF_ markers that may be embedded in the expression
914
1446
  // (e.g. from template literals inside ternary branches)
915
1447
  if (scopeExpression.includes('__KUBERNETES_REF___schema___')) {
916
- scopeExpression = scopeExpression.replace(/__KUBERNETES_REF___schema___([a-zA-Z0-9.$]+)__/g, (_match, fieldPath) => {
1448
+ scopeExpression = scopeExpression.replace(new RegExp(KUBERNETES_REF_SCHEMA_MARKER_SOURCE, 'g'), (_match, fieldPath) => {
917
1449
  const parts = fieldPath.replace(/^spec\./, '').split('.');
918
1450
  return parts.join('.');
919
1451
  });
@@ -921,10 +1453,52 @@ ${Object.entries(spec)
921
1453
  // Convert CEL single-quoted string literals to double-quoted for angular-expressions
922
1454
  // Match single-quoted strings that are NOT inside backticks
923
1455
  scopeExpression = scopeExpression.replace(/'([^'\\]*)'/g, '"$1"');
924
- const specRecord = Object.freeze(Object.assign(Object.create(null), spec));
1456
+ const specRecord = this.createStaticEvaluationScope(spec);
925
1457
  const evaluator = compileExpression(scopeExpression);
926
1458
  return evaluator(specRecord);
927
1459
  }
1460
+ /**
1461
+ * Translate the small KRO/CEL helper subset that can appear in static schema-only
1462
+ * expressions into functions that angular-expressions can safely evaluate.
1463
+ */
1464
+ prepareStaticExpressionForEvaluation(expression) {
1465
+ let scopeExpression = expression;
1466
+ const schemaPath = '[a-zA-Z_$][\\w$]*(?:\\.[a-zA-Z_$][\\w$]*)*';
1467
+ scopeExpression = scopeExpression.replace(new RegExp(`\\bhas\\((?:schema\\.)?spec\\.(${schemaPath})\\)`, 'g'), '__has("$1")');
1468
+ scopeExpression = scopeExpression.replace(new RegExp(`\\b(?:schema\\.)?spec\\.(${schemaPath})\\.orValue\\(([^()]*)\\)`, 'g'), '__orValue($1, $2)');
1469
+ scopeExpression = scopeExpression.replace(/\bstring\(/g, '__string(');
1470
+ // Replace schema.spec.fieldName → fieldName (resolved from scope)
1471
+ scopeExpression = scopeExpression.replace(/schema\.spec\.(\w+)/g, '$1');
1472
+ // Replace spec.fieldName → fieldName (resolved from scope)
1473
+ scopeExpression = scopeExpression.replace(/\bspec\.(\w+)/g, '$1');
1474
+ return scopeExpression;
1475
+ }
1476
+ createStaticEvaluationScope(spec) {
1477
+ // Use null-prototype object to prevent prototype chain access (defense-in-depth).
1478
+ // angular-expressions has hasOwnProperty guards, but a null-prototype scope
1479
+ // eliminates any residual risk from constructor/toString/__proto__ leaking.
1480
+ // Object.freeze prevents expression-based mutation of the original spec data.
1481
+ return Object.freeze(Object.assign(Object.create(null), spec, {
1482
+ __has: (path) => this.hasSchemaValue(path, spec),
1483
+ __orValue: (value, defaultValue) => value ?? defaultValue,
1484
+ __string: (value) => String(value ?? ''),
1485
+ omit: () => undefined,
1486
+ }));
1487
+ }
1488
+ hasSchemaValue(fieldPath, spec) {
1489
+ const parts = fieldPath.replace(/^spec\./, '').split('.');
1490
+ let current = spec;
1491
+ for (const part of parts) {
1492
+ if (current == null || typeof current !== 'object') {
1493
+ return false;
1494
+ }
1495
+ if (!Object.hasOwn(current, part)) {
1496
+ return false;
1497
+ }
1498
+ current = current[part];
1499
+ }
1500
+ return current !== undefined;
1501
+ }
928
1502
  /**
929
1503
  * Check if a value is a CEL expression (using canonical brand symbol)
930
1504
  */
@@ -940,18 +1514,26 @@ ${Object.entries(spec)
940
1514
  /**
941
1515
  * Create custom resource instance
942
1516
  */
943
- createCustomResourceInstance(instanceName, spec) {
944
- // The schema definition contains just the version part (e.g., 'v1alpha1')
945
- // We need to construct the full API version for the instance (e.g., 'kro.run/v1alpha1')
946
- const apiVersion = this.schemaDefinition.apiVersion.includes('/')
947
- ? this.schemaDefinition.apiVersion // Already has group prefix
948
- : `kro.run/${this.schemaDefinition.apiVersion}`; // Add kro.run group
1517
+ createCustomResourceInstance(instanceName, spec, singletonSpecFingerprint) {
1518
+ const apiVersion = this.getInstanceApiVersion();
949
1519
  return {
950
1520
  apiVersion,
951
1521
  kind: this.schemaDefinition.kind,
952
1522
  metadata: {
953
1523
  name: instanceName,
954
1524
  namespace: this.namespace,
1525
+ labels: {
1526
+ 'typekro.io/factory': this.name,
1527
+ 'typekro.io/mode': this.mode,
1528
+ 'typekro.io/rgd': this.rgdName,
1529
+ },
1530
+ ...(singletonSpecFingerprint
1531
+ ? {
1532
+ annotations: {
1533
+ 'typekro.io/singleton-spec-fingerprint': singletonSpecFingerprint,
1534
+ },
1535
+ }
1536
+ : {}),
955
1537
  },
956
1538
  spec,
957
1539
  };
@@ -969,9 +1551,7 @@ ${Object.entries(spec)
969
1551
  const status = { ...evaluatedStaticFields };
970
1552
  // Create the initial Enhanced proxy
971
1553
  // The Enhanced proxy should represent the actual instance, which uses the full API version
972
- const instanceApiVersion = this.schemaDefinition.apiVersion.includes('/')
973
- ? this.schemaDefinition.apiVersion
974
- : `kro.run/${this.schemaDefinition.apiVersion}`;
1554
+ const instanceApiVersion = this.getInstanceApiVersion();
975
1555
  const enhancedProxy = {
976
1556
  apiVersion: instanceApiVersion,
977
1557
  kind: this.schemaDefinition.kind,
@@ -1015,13 +1595,20 @@ ${Object.entries(spec)
1015
1595
  }
1016
1596
  // Post-process: re-execute the composition with live cluster data to fill
1017
1597
  // in status fields that neither static evaluation nor KRO could provide.
1018
- if (this.factoryOptions.compositionFn) {
1598
+ if (this.factoryOptions.hydrateStatus !== false && this.factoryOptions.compositionFn) {
1019
1599
  try {
1020
1600
  const liveStatus = await this.reExecuteWithLiveStatus(spec);
1021
1601
  if (liveStatus) {
1022
1602
  for (const [key, value] of Object.entries(liveStatus)) {
1023
1603
  if (key.startsWith('__'))
1024
1604
  continue;
1605
+ // Live re-execution uses the actual deploy spec plus cluster state, so it is the
1606
+ // most accurate source for non-dynamic fields. Dynamic fields remain owned by the
1607
+ // live Kro instance status and should not be replaced here.
1608
+ if (!(key in dynamicFields)) {
1609
+ enhancedProxy.status[key] = value;
1610
+ continue;
1611
+ }
1025
1612
  const current = enhancedProxy.status[key];
1026
1613
  if (current === undefined || current === null || current === '') {
1027
1614
  enhancedProxy.status[key] = value;
@@ -1053,12 +1640,8 @@ ${Object.entries(spec)
1053
1640
  const k8sApi = createBunCompatibleKubernetesObjectApi(kubeConfig);
1054
1641
  const resourceEntries = Object.entries(this.resources);
1055
1642
  const results = await Promise.allSettled(resourceEntries.map(async ([resourceId, resource]) => {
1056
- const name = typeof resource.metadata?.name === 'string'
1057
- ? resource.metadata.name
1058
- : resourceId;
1059
- const ns = typeof resource.metadata?.namespace === 'string'
1060
- ? resource.metadata.namespace
1061
- : this.namespace;
1643
+ const name = this.resolveLiveResourceIdentityValue(resource.metadata?.name, spec, resourceId);
1644
+ const ns = this.resolveLiveResourceIdentityValue(resource.metadata?.namespace, spec, this.namespace);
1062
1645
  const isClusterScoped = getMetadataField(resource, 'scope') === 'cluster';
1063
1646
  const live = await k8sApi.read({
1064
1647
  apiVersion: resource.apiVersion || '',
@@ -1066,9 +1649,14 @@ ${Object.entries(spec)
1066
1649
  metadata: { name, ...(isClusterScoped ? {} : { namespace: ns }) },
1067
1650
  });
1068
1651
  if (live && typeof live === 'object' && 'status' in live) {
1069
- return { resourceId, status: live.status };
1652
+ return {
1653
+ resourceId,
1654
+ status: live.status,
1655
+ };
1070
1656
  }
1071
- return null;
1657
+ // Statusless resources (Service, ConfigMap, Secret, etc.) should still
1658
+ // count as visible children for nested-composition recovery.
1659
+ return { resourceId, status: {} };
1072
1660
  }));
1073
1661
  for (const result of results) {
1074
1662
  if (result.status === 'fulfilled' && result.value) {
@@ -1078,19 +1666,76 @@ ${Object.entries(spec)
1078
1666
  // Probe to discover nested composition IDs
1079
1667
  const probeContext = createCompositionContext('kro-re-execution-probe', {
1080
1668
  deduplicateIds: true,
1669
+ isReExecution: true,
1081
1670
  });
1082
1671
  probeContext.liveStatusMap = liveStatusMap;
1083
1672
  runWithCompositionContext(probeContext, () => compositionFn(spec));
1084
1673
  // Synthesize nested composition status
1085
- const enrichedMap = synthesizeNestedCompositionStatus(probeContext.resources, liveStatusMap, this.logger, probeContext.nestedCompositionIds);
1674
+ const enrichedMap = synthesizeNestedCompositionStatus(probeContext.resources, liveStatusMap, this.logger, probeContext.nestedCompositionIds, probeContext.nestedStatusSnapshots);
1675
+ const aliasTargets = buildNestedCompositionAliasTargets(compositionFn.toString(), probeContext.nestedCompositionIds);
1676
+ for (const [aliasName, baseId] of Object.entries(aliasTargets)) {
1677
+ const synthesizedStatus = enrichedMap.get(baseId);
1678
+ if (synthesizedStatus && !enrichedMap.has(aliasName)) {
1679
+ enrichedMap.set(aliasName, synthesizedStatus);
1680
+ }
1681
+ }
1682
+ const singletonDefinitions = new Map();
1683
+ for (const definition of this.singletonDefinitions) {
1684
+ singletonDefinitions.set(definition.key, definition);
1685
+ }
1686
+ for (const definition of probeContext.singletonDefinitions?.values() ?? []) {
1687
+ singletonDefinitions.set(definition.key, definition);
1688
+ }
1689
+ for (const definition of singletonDefinitions.values()) {
1690
+ const singletonResourceId = getSingletonResourceId(definition.key);
1691
+ const singletonStatus = this.singletonOwnerStatuses.get(singletonResourceId);
1692
+ if (singletonStatus && !enrichedMap.has(singletonResourceId)) {
1693
+ enrichedMap.set(singletonResourceId, singletonStatus);
1694
+ }
1695
+ }
1086
1696
  // Real execution with live status
1087
1697
  const reExecutionContext = createCompositionContext('kro-re-execution', {
1088
1698
  deduplicateIds: true,
1699
+ isReExecution: true,
1089
1700
  });
1090
1701
  reExecutionContext.liveStatusMap = enrichedMap;
1091
1702
  const result = runWithCompositionContext(reExecutionContext, () => compositionFn(spec));
1092
1703
  return result;
1093
1704
  }
1705
+ resolveLiveResourceIdentityValue(value, spec, fallback) {
1706
+ if (isKubernetesRef(value)) {
1707
+ if (value.resourceId !== '__schema__')
1708
+ return fallback;
1709
+ const parts = value.fieldPath.replace(/^spec\./, '').split('.');
1710
+ let current = spec;
1711
+ for (const part of parts) {
1712
+ if (current == null || typeof current !== 'object')
1713
+ return fallback;
1714
+ current = current[part];
1715
+ }
1716
+ return current == null ? fallback : String(current);
1717
+ }
1718
+ if (typeof value !== 'string')
1719
+ return fallback;
1720
+ let resolved = value;
1721
+ if (resolved.includes('__KUBERNETES_REF___schema___')) {
1722
+ resolved = String(this.resolveSchemaRefMarkers(resolved, spec));
1723
+ }
1724
+ resolved = resolved.replace(/\$\{(?:string\()?schema\.spec\.([a-zA-Z0-9_.$]+)\)?\}/g, (_match, fieldPath) => {
1725
+ const parts = fieldPath.split('.');
1726
+ let current = spec;
1727
+ for (const part of parts) {
1728
+ if (current == null || typeof current !== 'object') {
1729
+ return _match;
1730
+ }
1731
+ current = current[part];
1732
+ }
1733
+ return String(current ?? '');
1734
+ });
1735
+ return resolved === '' || resolved.includes('__KUBERNETES_REF_') || resolved.includes('${')
1736
+ ? fallback
1737
+ : resolved;
1738
+ }
1094
1739
  /**
1095
1740
  * Create an Enhanced proxy for the instance (backward compatibility method)
1096
1741
  */
@@ -1102,9 +1747,7 @@ ${Object.entries(spec)
1102
1747
  * Delegates to the shared `waitForKroInstanceReady` in `kro-readiness.ts`.
1103
1748
  */
1104
1749
  async waitForKroInstanceReady(instanceName, timeout) {
1105
- const apiVersion = this.schemaDefinition.apiVersion.includes('/')
1106
- ? this.schemaDefinition.apiVersion
1107
- : `kro.run/${this.schemaDefinition.apiVersion}`;
1750
+ const apiVersion = this.getInstanceApiVersion();
1108
1751
  const kubeConfig = this.getKubeConfig();
1109
1752
  const k8sApi = createBunCompatibleKubernetesObjectApi(kubeConfig);
1110
1753
  return waitForKroInstanceReadyShared({
@@ -1115,7 +1758,7 @@ ${Object.entries(spec)
1115
1758
  namespace: this.namespace,
1116
1759
  apiVersion,
1117
1760
  kind: this.schemaDefinition.kind,
1118
- rgdName: this.name,
1761
+ rgdName: this.rgdName,
1119
1762
  factoryContext: this.name,
1120
1763
  });
1121
1764
  }
@@ -1125,9 +1768,7 @@ ${Object.entries(spec)
1125
1768
  async hydrateDynamicStatusFields(instanceName, dynamicFields) {
1126
1769
  const dynamicLogger = this.logger.child({ instanceName });
1127
1770
  // Get the live custom resource to extract dynamic status fields
1128
- const apiVersion = this.schemaDefinition.apiVersion.includes('/')
1129
- ? this.schemaDefinition.apiVersion
1130
- : `kro.run/${this.schemaDefinition.apiVersion}`;
1771
+ const apiVersion = this.getInstanceApiVersion();
1131
1772
  const kubeConfig = this.getKubeConfig();
1132
1773
  const k8sApi = createBunCompatibleKubernetesObjectApi(kubeConfig);
1133
1774
  const response = await k8sApi.read({