typekro 0.7.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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/alchemy/deployers.d.ts +16 -1
- package/dist/alchemy/deployers.d.ts.map +1 -1
- package/dist/alchemy/deployers.js +131 -18
- package/dist/alchemy/deployers.js.map +1 -1
- package/dist/alchemy/deployment.d.ts +1 -1
- package/dist/alchemy/deployment.d.ts.map +1 -1
- package/dist/alchemy/deployment.js +1 -1
- package/dist/alchemy/deployment.js.map +1 -1
- package/dist/alchemy/kro-delete.d.ts +66 -0
- package/dist/alchemy/kro-delete.d.ts.map +1 -0
- package/dist/alchemy/kro-delete.js +183 -0
- package/dist/alchemy/kro-delete.js.map +1 -0
- package/dist/alchemy/resource-registration.d.ts +16 -0
- package/dist/alchemy/resource-registration.d.ts.map +1 -1
- package/dist/alchemy/resource-registration.js +138 -24
- package/dist/alchemy/resource-registration.js.map +1 -1
- package/dist/alchemy/types.d.ts +8 -4
- package/dist/alchemy/types.d.ts.map +1 -1
- package/dist/compositions/typekro-runtime/typekro-runtime.d.ts +1 -1
- package/dist/compositions/typekro-runtime/typekro-runtime.d.ts.map +1 -1
- package/dist/compositions/typekro-runtime/typekro-runtime.js +39 -7
- package/dist/compositions/typekro-runtime/typekro-runtime.js.map +1 -1
- package/dist/core/composition/context.d.ts +58 -2
- package/dist/core/composition/context.d.ts.map +1 -1
- package/dist/core/composition/context.js +4 -0
- package/dist/core/composition/context.js.map +1 -1
- package/dist/core/composition/imperative.d.ts +9 -0
- package/dist/core/composition/imperative.d.ts.map +1 -1
- package/dist/core/composition/imperative.js +538 -54
- package/dist/core/composition/imperative.js.map +1 -1
- package/dist/core/composition/nested-status-cel.d.ts +34 -1
- package/dist/core/composition/nested-status-cel.d.ts.map +1 -1
- package/dist/core/composition/nested-status-cel.js +379 -41
- package/dist/core/composition/nested-status-cel.js.map +1 -1
- package/dist/core/composition-debugger.d.ts +1 -1
- package/dist/core/composition-debugger.d.ts.map +1 -1
- package/dist/core/composition-debugger.js.map +1 -1
- package/dist/core/constants/brands.d.ts +1 -1
- package/dist/core/constants/brands.d.ts.map +1 -1
- package/dist/core/constants/brands.js +1 -1
- package/dist/core/constants/brands.js.map +1 -1
- package/dist/core/containers/build.d.ts +4 -4
- package/dist/core/containers/build.d.ts.map +1 -1
- package/dist/core/containers/build.js +44 -16
- package/dist/core/containers/build.js.map +1 -1
- package/dist/core/containers/registries/types.d.ts +1 -1
- package/dist/core/containers/registries/types.d.ts.map +1 -1
- package/dist/core/dependencies/resolver.d.ts +19 -0
- package/dist/core/dependencies/resolver.d.ts.map +1 -1
- package/dist/core/dependencies/resolver.js +261 -1
- package/dist/core/dependencies/resolver.js.map +1 -1
- package/dist/core/deployment/client-provider-manager.d.ts +9 -3
- package/dist/core/deployment/client-provider-manager.d.ts.map +1 -1
- package/dist/core/deployment/client-provider-manager.js +12 -0
- package/dist/core/deployment/client-provider-manager.js.map +1 -1
- package/dist/core/deployment/crd-manager.d.ts +24 -0
- package/dist/core/deployment/crd-manager.d.ts.map +1 -1
- package/dist/core/deployment/crd-manager.js +79 -1
- package/dist/core/deployment/crd-manager.js.map +1 -1
- package/dist/core/deployment/deployment-state-discovery.d.ts +116 -0
- package/dist/core/deployment/deployment-state-discovery.d.ts.map +1 -0
- package/dist/core/deployment/deployment-state-discovery.js +400 -0
- package/dist/core/deployment/deployment-state-discovery.js.map +1 -0
- package/dist/core/deployment/direct-factory.d.ts +65 -6
- package/dist/core/deployment/direct-factory.d.ts.map +1 -1
- package/dist/core/deployment/direct-factory.js +551 -56
- package/dist/core/deployment/direct-factory.js.map +1 -1
- package/dist/core/deployment/engine.d.ts +64 -3
- package/dist/core/deployment/engine.d.ts.map +1 -1
- package/dist/core/deployment/engine.js +194 -27
- package/dist/core/deployment/engine.js.map +1 -1
- package/dist/core/deployment/event-filter.js +1 -1
- package/dist/core/deployment/event-filter.js.map +1 -1
- package/dist/core/deployment/event-monitor.d.ts +11 -0
- package/dist/core/deployment/event-monitor.d.ts.map +1 -1
- package/dist/core/deployment/event-monitor.js +14 -0
- package/dist/core/deployment/event-monitor.js.map +1 -1
- package/dist/core/deployment/handle-tracing.d.ts +14 -0
- package/dist/core/deployment/handle-tracing.d.ts.map +1 -0
- package/dist/core/deployment/handle-tracing.js +38 -0
- package/dist/core/deployment/handle-tracing.js.map +1 -0
- package/dist/core/deployment/kro-factory.d.ts +136 -3
- package/dist/core/deployment/kro-factory.d.ts.map +1 -1
- package/dist/core/deployment/kro-factory.js +945 -268
- package/dist/core/deployment/kro-factory.js.map +1 -1
- package/dist/core/deployment/kro-readiness.d.ts.map +1 -1
- package/dist/core/deployment/kro-readiness.js +21 -12
- package/dist/core/deployment/kro-readiness.js.map +1 -1
- package/dist/core/deployment/nested-composition-status.d.ts +1 -1
- package/dist/core/deployment/nested-composition-status.d.ts.map +1 -1
- package/dist/core/deployment/nested-composition-status.js +96 -53
- package/dist/core/deployment/nested-composition-status.js.map +1 -1
- package/dist/core/deployment/resource-applier.d.ts +15 -2
- package/dist/core/deployment/resource-applier.d.ts.map +1 -1
- package/dist/core/deployment/resource-applier.js +75 -25
- package/dist/core/deployment/resource-applier.js.map +1 -1
- package/dist/core/deployment/resource-tagging.d.ts +220 -0
- package/dist/core/deployment/resource-tagging.d.ts.map +1 -0
- package/dist/core/deployment/resource-tagging.js +292 -0
- package/dist/core/deployment/resource-tagging.js.map +1 -0
- package/dist/core/deployment/rollback-manager.d.ts +25 -4
- package/dist/core/deployment/rollback-manager.d.ts.map +1 -1
- package/dist/core/deployment/rollback-manager.js +70 -57
- package/dist/core/deployment/rollback-manager.js.map +1 -1
- package/dist/core/deployment/shared-utilities.d.ts +6 -0
- package/dist/core/deployment/shared-utilities.d.ts.map +1 -1
- package/dist/core/deployment/shared-utilities.js +32 -2
- package/dist/core/deployment/shared-utilities.js.map +1 -1
- package/dist/core/deployment/singleton-owner-drift.d.ts +16 -0
- package/dist/core/deployment/singleton-owner-drift.d.ts.map +1 -0
- package/dist/core/deployment/singleton-owner-drift.js +54 -0
- package/dist/core/deployment/singleton-owner-drift.js.map +1 -0
- package/dist/core/deployment/strategies/alchemy-strategy.d.ts +3 -1
- package/dist/core/deployment/strategies/alchemy-strategy.d.ts.map +1 -1
- package/dist/core/deployment/strategies/alchemy-strategy.js +121 -18
- package/dist/core/deployment/strategies/alchemy-strategy.js.map +1 -1
- package/dist/core/deployment/strategies/base-strategy.d.ts +9 -3
- package/dist/core/deployment/strategies/base-strategy.d.ts.map +1 -1
- package/dist/core/deployment/strategies/base-strategy.js +32 -4
- package/dist/core/deployment/strategies/base-strategy.js.map +1 -1
- package/dist/core/deployment/strategies/direct-strategy.d.ts +12 -4
- package/dist/core/deployment/strategies/direct-strategy.d.ts.map +1 -1
- package/dist/core/deployment/strategies/direct-strategy.js +112 -8
- package/dist/core/deployment/strategies/direct-strategy.js.map +1 -1
- package/dist/core/errors.d.ts +2 -2
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js.map +1 -1
- package/dist/core/expressions/analysis/cache.d.ts +2 -2
- package/dist/core/expressions/analysis/cache.d.ts.map +1 -1
- package/dist/core/expressions/analysis/cache.js +4 -0
- package/dist/core/expressions/analysis/cache.js.map +1 -1
- package/dist/core/expressions/analysis/fn-toString-self-test.d.ts.map +1 -1
- package/dist/core/expressions/analysis/fn-toString-self-test.js +0 -1
- package/dist/core/expressions/analysis/fn-toString-self-test.js.map +1 -1
- package/dist/core/expressions/analysis/shared-types.d.ts +2 -2
- package/dist/core/expressions/analysis/shared-types.d.ts.map +1 -1
- package/dist/core/expressions/analysis/source-map.d.ts.map +1 -1
- package/dist/core/expressions/analysis/source-map.js +1 -0
- package/dist/core/expressions/analysis/source-map.js.map +1 -1
- package/dist/core/expressions/analysis/types.d.ts +7 -7
- package/dist/core/expressions/analysis/types.d.ts.map +1 -1
- package/dist/core/expressions/composition/composition-analyzer-helpers.d.ts +37 -3
- package/dist/core/expressions/composition/composition-analyzer-helpers.d.ts.map +1 -1
- package/dist/core/expressions/composition/composition-analyzer-helpers.js +370 -7
- package/dist/core/expressions/composition/composition-analyzer-helpers.js.map +1 -1
- package/dist/core/expressions/composition/composition-analyzer-ternary.d.ts +2 -2
- package/dist/core/expressions/composition/composition-analyzer-ternary.d.ts.map +1 -1
- package/dist/core/expressions/composition/composition-analyzer-ternary.js +264 -29
- package/dist/core/expressions/composition/composition-analyzer-ternary.js.map +1 -1
- package/dist/core/expressions/composition/composition-analyzer-traversal.d.ts.map +1 -1
- package/dist/core/expressions/composition/composition-analyzer-traversal.js +78 -6
- package/dist/core/expressions/composition/composition-analyzer-traversal.js.map +1 -1
- package/dist/core/expressions/composition/composition-analyzer-types.d.ts +60 -0
- package/dist/core/expressions/composition/composition-analyzer-types.d.ts.map +1 -1
- package/dist/core/expressions/composition/composition-analyzer.d.ts +1 -1
- package/dist/core/expressions/composition/composition-analyzer.d.ts.map +1 -1
- package/dist/core/expressions/composition/composition-analyzer.js +158 -8
- package/dist/core/expressions/composition/composition-analyzer.js.map +1 -1
- package/dist/core/expressions/composition/expression-analyzer.d.ts.map +1 -1
- package/dist/core/expressions/composition/expression-analyzer.js +21 -4
- package/dist/core/expressions/composition/expression-analyzer.js.map +1 -1
- package/dist/core/expressions/composition/imperative-analyzer.d.ts +1 -1
- package/dist/core/expressions/composition/imperative-analyzer.d.ts.map +1 -1
- package/dist/core/expressions/composition/imperative-analyzer.js +31 -7
- package/dist/core/expressions/composition/imperative-analyzer.js.map +1 -1
- package/dist/core/expressions/composition/scope-manager.d.ts +2 -2
- package/dist/core/expressions/composition/scope-manager.d.ts.map +1 -1
- package/dist/core/expressions/composition/scope-manager.js.map +1 -1
- package/dist/core/expressions/conditional/conditional-expression-processor.d.ts +3 -3
- package/dist/core/expressions/conditional/conditional-expression-processor.d.ts.map +1 -1
- package/dist/core/expressions/conditional/conditional-expression-processor.js.map +1 -1
- package/dist/core/expressions/conditional/conditional-integration.d.ts.map +1 -1
- package/dist/core/expressions/conditional/conditional-integration.js.map +1 -1
- package/dist/core/expressions/context/context-aware-generator.d.ts +5 -5
- package/dist/core/expressions/context/context-aware-generator.d.ts.map +1 -1
- package/dist/core/expressions/context/context-aware-generator.js.map +1 -1
- package/dist/core/expressions/context/context-detector.d.ts +3 -3
- package/dist/core/expressions/context/context-detector.d.ts.map +1 -1
- package/dist/core/expressions/context/context-detector.js.map +1 -1
- package/dist/core/expressions/context/context-validator.d.ts +6 -6
- package/dist/core/expressions/context/context-validator.d.ts.map +1 -1
- package/dist/core/expressions/context/context-validator.js +2 -2
- package/dist/core/expressions/context/context-validator.js.map +1 -1
- package/dist/core/expressions/factory/cel-conversion-engine.d.ts +4 -4
- package/dist/core/expressions/factory/cel-conversion-engine.d.ts.map +1 -1
- package/dist/core/expressions/factory/cel-conversion-engine.js.map +1 -1
- package/dist/core/expressions/factory/dependency-tracker.d.ts +2 -2
- package/dist/core/expressions/factory/dependency-tracker.d.ts.map +1 -1
- package/dist/core/expressions/factory/dependency-tracker.js +21 -5
- package/dist/core/expressions/factory/dependency-tracker.js.map +1 -1
- package/dist/core/expressions/factory/factory-integration.d.ts +2 -4
- package/dist/core/expressions/factory/factory-integration.d.ts.map +1 -1
- package/dist/core/expressions/factory/factory-integration.js +0 -6
- package/dist/core/expressions/factory/factory-integration.js.map +1 -1
- package/dist/core/expressions/factory/factory-pattern-handler.d.ts +3 -3
- package/dist/core/expressions/factory/factory-pattern-handler.d.ts.map +1 -1
- package/dist/core/expressions/factory/factory-pattern-handler.js +1 -0
- package/dist/core/expressions/factory/factory-pattern-handler.js.map +1 -1
- package/dist/core/expressions/factory/migration-helpers.js.map +1 -1
- package/dist/core/expressions/factory/resource-analyzer.d.ts +4 -4
- package/dist/core/expressions/factory/resource-analyzer.d.ts.map +1 -1
- package/dist/core/expressions/factory/resource-analyzer.js.map +1 -1
- package/dist/core/expressions/factory/resource-type-validator.d.ts +5 -5
- package/dist/core/expressions/factory/resource-type-validator.d.ts.map +1 -1
- package/dist/core/expressions/factory/resource-type-validator.js.map +1 -1
- package/dist/core/expressions/factory/status-builder-analyzer.d.ts +6 -7
- package/dist/core/expressions/factory/status-builder-analyzer.d.ts.map +1 -1
- package/dist/core/expressions/factory/status-builder-analyzer.js +0 -3
- package/dist/core/expressions/factory/status-builder-analyzer.js.map +1 -1
- package/dist/core/expressions/factory/status-builder-types.d.ts +1 -1
- package/dist/core/expressions/factory/status-builder-types.d.ts.map +1 -1
- package/dist/core/expressions/factory/status-cel-generation.d.ts +1 -1
- package/dist/core/expressions/factory/status-cel-generation.d.ts.map +1 -1
- package/dist/core/expressions/factory/status-cel-generation.js +1 -1
- package/dist/core/expressions/factory/status-cel-generation.js.map +1 -1
- package/dist/core/expressions/factory/status-field-analysis.d.ts +3 -3
- package/dist/core/expressions/factory/status-field-analysis.d.ts.map +1 -1
- package/dist/core/expressions/factory/status-field-analysis.js.map +1 -1
- package/dist/core/expressions/magic-proxy/magic-assignable-analyzer.d.ts +5 -5
- package/dist/core/expressions/magic-proxy/magic-assignable-analyzer.d.ts.map +1 -1
- package/dist/core/expressions/magic-proxy/magic-assignable-analyzer.js +1 -1
- package/dist/core/expressions/magic-proxy/magic-assignable-analyzer.js.map +1 -1
- package/dist/core/expressions/magic-proxy/magic-proxy-analyzer.d.ts +10 -10
- package/dist/core/expressions/magic-proxy/magic-proxy-analyzer.d.ts.map +1 -1
- package/dist/core/expressions/magic-proxy/magic-proxy-analyzer.js +5 -1
- package/dist/core/expressions/magic-proxy/magic-proxy-analyzer.js.map +1 -1
- package/dist/core/expressions/magic-proxy/magic-proxy-ast.d.ts +2 -2
- package/dist/core/expressions/magic-proxy/magic-proxy-ast.d.ts.map +1 -1
- package/dist/core/expressions/magic-proxy/magic-proxy-ast.js.map +1 -1
- package/dist/core/expressions/magic-proxy/magic-proxy-detector.d.ts +5 -5
- package/dist/core/expressions/magic-proxy/magic-proxy-detector.d.ts.map +1 -1
- package/dist/core/expressions/magic-proxy/magic-proxy-detector.js.map +1 -1
- package/dist/core/expressions/magic-proxy/magic-proxy-types.d.ts +2 -2
- package/dist/core/expressions/magic-proxy/magic-proxy-types.d.ts.map +1 -1
- package/dist/core/expressions/magic-proxy/optionality-handler.d.ts +1 -2
- package/dist/core/expressions/magic-proxy/optionality-handler.d.ts.map +1 -1
- package/dist/core/expressions/magic-proxy/optionality-handler.js +2 -15
- package/dist/core/expressions/magic-proxy/optionality-handler.js.map +1 -1
- package/dist/core/expressions/validation/compile-time-checker.d.ts +1 -1
- package/dist/core/expressions/validation/compile-time-checker.d.ts.map +1 -1
- package/dist/core/expressions/validation/compile-time-checker.js.map +1 -1
- package/dist/core/expressions/validation/compile-time-types.d.ts +2 -2
- package/dist/core/expressions/validation/compile-time-types.d.ts.map +1 -1
- package/dist/core/expressions/validation/kubernetes-field-types.d.ts +4 -4
- package/dist/core/expressions/validation/kubernetes-field-types.d.ts.map +1 -1
- package/dist/core/expressions/validation/kubernetes-field-types.js.map +1 -1
- package/dist/core/expressions/validation/resource-field-utils.d.ts +10 -10
- package/dist/core/expressions/validation/resource-field-utils.d.ts.map +1 -1
- package/dist/core/expressions/validation/resource-field-utils.js.map +1 -1
- package/dist/core/expressions/validation/resource-validation.d.ts +3 -3
- package/dist/core/expressions/validation/resource-validation.d.ts.map +1 -1
- package/dist/core/expressions/validation/resource-validation.js.map +1 -1
- package/dist/core/expressions/validation/type-inference-types.d.ts +2 -2
- package/dist/core/expressions/validation/type-inference-types.d.ts.map +1 -1
- package/dist/core/expressions/validation/type-safety.d.ts +2 -2
- package/dist/core/expressions/validation/type-safety.d.ts.map +1 -1
- package/dist/core/expressions/validation/type-safety.js +1 -0
- package/dist/core/expressions/validation/type-safety.js.map +1 -1
- package/dist/core/kubernetes/bun-api-client.js.map +1 -1
- package/dist/core/kubernetes/bun-http-library.d.ts.map +1 -1
- package/dist/core/kubernetes/bun-http-library.js +29 -3
- package/dist/core/kubernetes/bun-http-library.js.map +1 -1
- package/dist/core/kubernetes/client-provider.d.ts +12 -0
- package/dist/core/kubernetes/client-provider.d.ts.map +1 -1
- package/dist/core/kubernetes/client-provider.js +35 -0
- package/dist/core/kubernetes/client-provider.js.map +1 -1
- package/dist/core/metadata/resource-metadata.d.ts +46 -1
- package/dist/core/metadata/resource-metadata.d.ts.map +1 -1
- package/dist/core/metadata/resource-metadata.js.map +1 -1
- package/dist/core/proxy/create-resource.d.ts +15 -0
- package/dist/core/proxy/create-resource.d.ts.map +1 -1
- package/dist/core/proxy/create-resource.js +56 -2
- package/dist/core/proxy/create-resource.js.map +1 -1
- package/dist/core/readiness/registry.js +2 -2
- package/dist/core/readiness/registry.js.map +1 -1
- package/dist/core/references/cel-evaluator.d.ts +1 -4
- package/dist/core/references/cel-evaluator.d.ts.map +1 -1
- package/dist/core/references/cel-evaluator.js +3 -7
- package/dist/core/references/cel-evaluator.js.map +1 -1
- package/dist/core/references/cel.d.ts +70 -0
- package/dist/core/references/cel.d.ts.map +1 -1
- package/dist/core/references/cel.js +188 -8
- package/dist/core/references/cel.js.map +1 -1
- package/dist/core/references/external-refs.d.ts.map +1 -1
- package/dist/core/references/external-refs.js +3 -0
- package/dist/core/references/external-refs.js.map +1 -1
- package/dist/core/references/resolver.d.ts.map +1 -1
- package/dist/core/references/resolver.js +28 -17
- package/dist/core/references/resolver.js.map +1 -1
- package/dist/core/references/schema-proxy.d.ts +18 -10
- package/dist/core/references/schema-proxy.d.ts.map +1 -1
- package/dist/core/references/schema-proxy.js +174 -23
- package/dist/core/references/schema-proxy.js.map +1 -1
- package/dist/core/runtime-patches/crd-schema-fix.d.ts.map +1 -1
- package/dist/core/runtime-patches/crd-schema-fix.js +4 -1
- package/dist/core/runtime-patches/crd-schema-fix.js.map +1 -1
- package/dist/core/serialization/cel-optimizer.d.ts.map +1 -1
- package/dist/core/serialization/cel-optimizer.js +2 -0
- package/dist/core/serialization/cel-optimizer.js.map +1 -1
- package/dist/core/serialization/cel-references.d.ts +75 -1
- package/dist/core/serialization/cel-references.d.ts.map +1 -1
- package/dist/core/serialization/cel-references.js +723 -145
- package/dist/core/serialization/cel-references.js.map +1 -1
- package/dist/core/serialization/core.d.ts +13 -8
- package/dist/core/serialization/core.d.ts.map +1 -1
- package/dist/core/serialization/core.js +973 -12
- package/dist/core/serialization/core.js.map +1 -1
- package/dist/core/serialization/kro-post-processing.d.ts +46 -0
- package/dist/core/serialization/kro-post-processing.d.ts.map +1 -0
- package/dist/core/serialization/kro-post-processing.js +150 -0
- package/dist/core/serialization/kro-post-processing.js.map +1 -0
- package/dist/core/serialization/schema.d.ts +62 -3
- package/dist/core/serialization/schema.d.ts.map +1 -1
- package/dist/core/serialization/schema.js +819 -6
- package/dist/core/serialization/schema.js.map +1 -1
- package/dist/core/serialization/status-analysis-pipeline.d.ts +1 -1
- package/dist/core/serialization/status-analysis-pipeline.d.ts.map +1 -1
- package/dist/core/serialization/status-analysis-pipeline.js.map +1 -1
- package/dist/core/serialization/yaml.d.ts +3 -2
- package/dist/core/serialization/yaml.d.ts.map +1 -1
- package/dist/core/serialization/yaml.js +404 -56
- package/dist/core/serialization/yaml.js.map +1 -1
- package/dist/core/singleton/singleton.d.ts +16 -0
- package/dist/core/singleton/singleton.d.ts.map +1 -0
- package/dist/core/singleton/singleton.js +135 -0
- package/dist/core/singleton/singleton.js.map +1 -0
- package/dist/core/types/common.d.ts +2 -2
- package/dist/core/types/common.d.ts.map +1 -1
- package/dist/core/types/composable.d.ts +1 -1
- package/dist/core/types/composable.d.ts.map +1 -1
- package/dist/core/types/deployment.d.ts +126 -6
- package/dist/core/types/deployment.d.ts.map +1 -1
- package/dist/core/types/deployment.js +1 -1
- package/dist/core/types/deployment.js.map +1 -1
- package/dist/core/types/kubernetes.d.ts +25 -17
- package/dist/core/types/kubernetes.d.ts.map +1 -1
- package/dist/core/types/references.d.ts +1 -1
- package/dist/core/types/references.d.ts.map +1 -1
- package/dist/core/types/references.js.map +1 -1
- package/dist/core/types/resource-graph.d.ts +1 -1
- package/dist/core/types/resource-graph.d.ts.map +1 -1
- package/dist/core/types/schema.d.ts +1 -1
- package/dist/core/types/schema.d.ts.map +1 -1
- package/dist/core/types/serialization.d.ts +62 -6
- package/dist/core/types/serialization.d.ts.map +1 -1
- package/dist/core/validation/cel-validator.d.ts +15 -2
- package/dist/core/validation/cel-validator.d.ts.map +1 -1
- package/dist/core/validation/cel-validator.js +144 -63
- package/dist/core/validation/cel-validator.js.map +1 -1
- package/dist/factories/apisix/compositions/apisix-bootstrap.d.ts +2 -41
- package/dist/factories/apisix/compositions/apisix-bootstrap.d.ts.map +1 -1
- package/dist/factories/apisix/compositions/apisix-bootstrap.js +262 -217
- package/dist/factories/apisix/compositions/apisix-bootstrap.js.map +1 -1
- package/dist/factories/apisix/index.d.ts +2 -2
- package/dist/factories/apisix/index.js +2 -2
- package/dist/factories/apisix/resources/helm.d.ts +2 -2
- package/dist/factories/apisix/resources/helm.d.ts.map +1 -1
- package/dist/factories/apisix/resources/helm.js.map +1 -1
- package/dist/factories/apisix/types.d.ts +21 -11
- package/dist/factories/apisix/types.d.ts.map +1 -1
- package/dist/factories/apisix/types.js +106 -4
- package/dist/factories/apisix/types.js.map +1 -1
- package/dist/factories/apisix/utils/admin-credentials.d.ts +5 -3
- package/dist/factories/apisix/utils/admin-credentials.d.ts.map +1 -1
- package/dist/factories/apisix/utils/admin-credentials.js +14 -10
- package/dist/factories/apisix/utils/admin-credentials.js.map +1 -1
- package/dist/factories/apisix/utils/helm-values-mapper.d.ts.map +1 -1
- package/dist/factories/apisix/utils/helm-values-mapper.js +4 -2
- package/dist/factories/apisix/utils/helm-values-mapper.js.map +1 -1
- package/dist/factories/cert-manager/resources/challenges.js.map +1 -1
- package/dist/factories/cert-manager/types.d.ts +3 -3
- package/dist/factories/cert-manager/types.d.ts.map +1 -1
- package/dist/factories/cilium/compositions/cilium-bootstrap.d.ts +4 -4
- package/dist/factories/cilium/types.d.ts +3 -3
- package/dist/factories/cilium/types.d.ts.map +1 -1
- package/dist/factories/cnpg/compositions/cnpg-bootstrap.d.ts +1 -0
- package/dist/factories/cnpg/compositions/cnpg-bootstrap.d.ts.map +1 -1
- package/dist/factories/cnpg/compositions/cnpg-bootstrap.js +48 -0
- package/dist/factories/cnpg/compositions/cnpg-bootstrap.js.map +1 -1
- package/dist/factories/cnpg/resources/cluster.js +1 -1
- package/dist/factories/cnpg/resources/cluster.js.map +1 -1
- package/dist/factories/cnpg/resources/helm.d.ts.map +1 -1
- package/dist/factories/cnpg/resources/helm.js +1 -0
- package/dist/factories/cnpg/resources/helm.js.map +1 -1
- package/dist/factories/cnpg/resources/pooler.js +1 -1
- package/dist/factories/cnpg/resources/pooler.js.map +1 -1
- package/dist/factories/cnpg/types.d.ts +9 -8
- package/dist/factories/cnpg/types.d.ts.map +1 -1
- package/dist/factories/cnpg/types.js +11 -0
- package/dist/factories/cnpg/types.js.map +1 -1
- package/dist/factories/external-dns/compositions/external-dns-bootstrap.d.ts.map +1 -1
- package/dist/factories/external-dns/compositions/external-dns-bootstrap.js +153 -41
- package/dist/factories/external-dns/compositions/external-dns-bootstrap.js.map +1 -1
- package/dist/factories/external-dns/resources/dns-endpoint.js +1 -1
- package/dist/factories/external-dns/resources/dns-endpoint.js.map +1 -1
- package/dist/factories/external-dns/resources/helm.d.ts +1 -1
- package/dist/factories/external-dns/resources/helm.d.ts.map +1 -1
- package/dist/factories/external-dns/resources/helm.js +17 -10
- package/dist/factories/external-dns/resources/helm.js.map +1 -1
- package/dist/factories/external-dns/types.d.ts +5 -2
- package/dist/factories/external-dns/types.d.ts.map +1 -1
- package/dist/factories/external-dns/types.js.map +1 -1
- package/dist/factories/flux/git-repository.d.ts.map +1 -1
- package/dist/factories/flux/git-repository.js +1 -1
- package/dist/factories/flux/git-repository.js.map +1 -1
- package/dist/factories/flux/kustomize/kustomization.d.ts +2 -2
- package/dist/factories/flux/kustomize/kustomization.d.ts.map +1 -1
- package/dist/factories/flux/kustomize/readiness-evaluators.d.ts +1 -1
- package/dist/factories/flux/kustomize/readiness-evaluators.d.ts.map +1 -1
- package/dist/factories/flux/kustomize/readiness-evaluators.js +1 -1
- package/dist/factories/flux/kustomize/readiness-evaluators.js.map +1 -1
- package/dist/factories/helm/helm-release.d.ts +3 -2
- package/dist/factories/helm/helm-release.d.ts.map +1 -1
- package/dist/factories/helm/helm-release.js +1 -0
- package/dist/factories/helm/helm-release.js.map +1 -1
- package/dist/factories/helm/helm-repository.d.ts +1 -1
- package/dist/factories/helm/helm-repository.d.ts.map +1 -1
- package/dist/factories/helm/helm-repository.js +6 -4
- package/dist/factories/helm/helm-repository.js.map +1 -1
- package/dist/factories/helm/readiness-evaluators.d.ts +6 -6
- package/dist/factories/helm/readiness-evaluators.d.ts.map +1 -1
- package/dist/factories/helm/readiness-evaluators.js +15 -9
- package/dist/factories/helm/readiness-evaluators.js.map +1 -1
- package/dist/factories/helm/types.d.ts +5 -1
- package/dist/factories/helm/types.d.ts.map +1 -1
- package/dist/factories/inngest/compositions/inngest-bootstrap.d.ts +1 -0
- package/dist/factories/inngest/compositions/inngest-bootstrap.d.ts.map +1 -1
- package/dist/factories/inngest/compositions/inngest-bootstrap.js +4 -3
- package/dist/factories/inngest/compositions/inngest-bootstrap.js.map +1 -1
- package/dist/factories/inngest/resources/helm.js +1 -1
- package/dist/factories/inngest/resources/helm.js.map +1 -1
- package/dist/factories/inngest/types.d.ts +5 -4
- package/dist/factories/inngest/types.d.ts.map +1 -1
- package/dist/factories/inngest/types.js +2 -0
- package/dist/factories/inngest/types.js.map +1 -1
- package/dist/factories/kro/kro-custom-resource.js +1 -1
- package/dist/factories/kro/kro-custom-resource.js.map +1 -1
- package/dist/factories/kubernetes/config/config-map.d.ts +2 -2
- package/dist/factories/kubernetes/config/config-map.d.ts.map +1 -1
- package/dist/factories/kubernetes/config/secret.d.ts +2 -2
- package/dist/factories/kubernetes/config/secret.d.ts.map +1 -1
- package/dist/factories/kubernetes/config/secret.js +11 -1
- package/dist/factories/kubernetes/config/secret.js.map +1 -1
- package/dist/factories/kubernetes/networking/service.js +1 -1
- package/dist/factories/kubernetes/networking/service.js.map +1 -1
- package/dist/factories/kubernetes/yaml/yaml-directory.d.ts.map +1 -1
- package/dist/factories/kubernetes/yaml/yaml-directory.js +9 -0
- package/dist/factories/kubernetes/yaml/yaml-directory.js.map +1 -1
- package/dist/factories/kubernetes/yaml/yaml-file.d.ts.map +1 -1
- package/dist/factories/kubernetes/yaml/yaml-file.js +9 -0
- package/dist/factories/kubernetes/yaml/yaml-file.js.map +1 -1
- package/dist/factories/pebble/resources/helm.js.map +1 -1
- package/dist/factories/pebble/types.d.ts +2 -2
- package/dist/factories/searxng/compositions/index.d.ts +2 -0
- package/dist/factories/searxng/compositions/index.d.ts.map +1 -0
- package/dist/factories/searxng/compositions/index.js +2 -0
- package/dist/factories/searxng/compositions/index.js.map +1 -0
- package/dist/factories/searxng/compositions/searxng-bootstrap.d.ts +66 -0
- package/dist/factories/searxng/compositions/searxng-bootstrap.d.ts.map +1 -0
- package/dist/factories/searxng/compositions/searxng-bootstrap.js +275 -0
- package/dist/factories/searxng/compositions/searxng-bootstrap.js.map +1 -0
- package/dist/factories/searxng/index.d.ts +29 -0
- package/dist/factories/searxng/index.d.ts.map +1 -0
- package/dist/factories/searxng/index.js +27 -0
- package/dist/factories/searxng/index.js.map +1 -0
- package/dist/factories/searxng/resources/index.d.ts +2 -0
- package/dist/factories/searxng/resources/index.d.ts.map +1 -0
- package/dist/factories/searxng/resources/index.js +2 -0
- package/dist/factories/searxng/resources/index.js.map +1 -0
- package/dist/factories/searxng/resources/searxng.d.ts +65 -0
- package/dist/factories/searxng/resources/searxng.d.ts.map +1 -0
- package/dist/factories/searxng/resources/searxng.js +188 -0
- package/dist/factories/searxng/resources/searxng.js.map +1 -0
- package/dist/factories/searxng/types.d.ts +127 -0
- package/dist/factories/searxng/types.d.ts.map +1 -0
- package/dist/factories/searxng/types.js +177 -0
- package/dist/factories/searxng/types.js.map +1 -0
- package/dist/factories/searxng/utils/settings-builder.d.ts +47 -0
- package/dist/factories/searxng/utils/settings-builder.d.ts.map +1 -0
- package/dist/factories/searxng/utils/settings-builder.js +53 -0
- package/dist/factories/searxng/utils/settings-builder.js.map +1 -0
- package/dist/factories/simple/config/config-map.d.ts +2 -2
- package/dist/factories/simple/config/config-map.d.ts.map +1 -1
- package/dist/factories/simple/config/secret.d.ts +2 -2
- package/dist/factories/simple/config/secret.d.ts.map +1 -1
- package/dist/factories/simple/config/secret.js +28 -0
- package/dist/factories/simple/config/secret.js.map +1 -1
- package/dist/factories/simple/helm/index.d.ts +1 -1
- package/dist/factories/simple/helm/index.d.ts.map +1 -1
- package/dist/factories/simple/helm/index.js.map +1 -1
- package/dist/factories/simple/storage/persistent-volume.js.map +1 -1
- package/dist/factories/simple/types.d.ts +11 -1
- package/dist/factories/simple/types.d.ts.map +1 -1
- package/dist/factories/simple/workloads/deployment.d.ts.map +1 -1
- package/dist/factories/simple/workloads/deployment.js +3 -0
- package/dist/factories/simple/workloads/deployment.js.map +1 -1
- package/dist/factories/valkey/compositions/valkey-bootstrap.d.ts +1 -0
- package/dist/factories/valkey/compositions/valkey-bootstrap.d.ts.map +1 -1
- package/dist/factories/valkey/compositions/valkey-bootstrap.js +116 -0
- package/dist/factories/valkey/compositions/valkey-bootstrap.js.map +1 -1
- package/dist/factories/valkey/resources/valkey.js +1 -1
- package/dist/factories/valkey/resources/valkey.js.map +1 -1
- package/dist/factories/valkey/types.d.ts +6 -5
- package/dist/factories/valkey/types.d.ts.map +1 -1
- package/dist/factories/valkey/types.js +10 -0
- package/dist/factories/valkey/types.js.map +1 -1
- package/dist/factories/webapp/compositions/web-app-with-processing.d.ts +95 -12
- package/dist/factories/webapp/compositions/web-app-with-processing.d.ts.map +1 -1
- package/dist/factories/webapp/compositions/web-app-with-processing.js +185 -26
- package/dist/factories/webapp/compositions/web-app-with-processing.js.map +1 -1
- package/dist/factories/webapp/index.d.ts +3 -4
- package/dist/factories/webapp/index.d.ts.map +1 -1
- package/dist/factories/webapp/index.js +3 -4
- package/dist/factories/webapp/index.js.map +1 -1
- package/dist/factories/webapp/types.d.ts +60 -2
- package/dist/factories/webapp/types.d.ts.map +1 -1
- package/dist/factories/webapp/types.js +80 -3
- package/dist/factories/webapp/types.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/shared/brands.d.ts +18 -8
- package/dist/shared/brands.d.ts.map +1 -1
- package/dist/shared/brands.js +19 -9
- package/dist/shared/brands.js.map +1 -1
- package/dist/utils/cel-escape.d.ts +12 -0
- package/dist/utils/cel-escape.d.ts.map +1 -0
- package/dist/utils/cel-escape.js +19 -0
- package/dist/utils/cel-escape.js.map +1 -0
- package/package.json +7 -2
|
@@ -6,24 +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';
|
|
26
|
+
import { applyTernaryConditionalsToResources } from '../serialization/kro-post-processing.js';
|
|
22
27
|
import { serializeResourceGraphToYaml } from '../serialization/yaml.js';
|
|
28
|
+
import { logHandleSnapshot } from './handle-tracing.js';
|
|
23
29
|
import { KubernetesClientManager } from './client-provider-manager.js';
|
|
24
30
|
import { DirectDeploymentEngine } from './engine.js';
|
|
31
|
+
import { isNotFoundError } from './k8s-helpers.js';
|
|
25
32
|
import { waitForKroInstanceReady as waitForKroInstanceReadyShared } from './kro-readiness.js';
|
|
26
|
-
import {
|
|
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
|
+
}
|
|
27
77
|
/**
|
|
28
78
|
* KroResourceFactory implementation
|
|
29
79
|
*
|
|
@@ -42,9 +92,32 @@ export class KroResourceFactoryImpl {
|
|
|
42
92
|
schemaDefinition;
|
|
43
93
|
statusMappings;
|
|
44
94
|
alchemyScope;
|
|
95
|
+
singletonDefinitions;
|
|
96
|
+
singletonOwnerStatuses = new Map();
|
|
45
97
|
logger = getComponentLogger('kro-factory');
|
|
46
98
|
factoryOptions;
|
|
47
99
|
clientManager;
|
|
100
|
+
/**
|
|
101
|
+
* Tracks whether ternary-conditional post-processing has already been applied
|
|
102
|
+
* to `this.resources`. The mutation is idempotent today (consumed markers are
|
|
103
|
+
* replaced with CEL in the first pass), but we guard explicitly for symmetry
|
|
104
|
+
* with `core.ts` and to avoid relying on that idempotency claim across future
|
|
105
|
+
* refactors. Applies across both `toYaml()` and `ensureRGDDeployed()` paths.
|
|
106
|
+
*/
|
|
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;
|
|
48
121
|
// Dependency-inversion providers (Phase 3.5) — injected via FactoryOptions
|
|
49
122
|
// instead of dynamic import() from factories/ and alchemy/ layers.
|
|
50
123
|
kroCustomResourceProvider;
|
|
@@ -60,9 +133,14 @@ export class KroResourceFactoryImpl {
|
|
|
60
133
|
this.closures = options.closures || {};
|
|
61
134
|
this.schemaDefinition = schemaDefinition;
|
|
62
135
|
this.statusMappings = statusMappings;
|
|
136
|
+
this.singletonDefinitions = options.singletonDefinitions ?? [];
|
|
63
137
|
this.factoryOptions = options;
|
|
64
138
|
this.clientManager = new KubernetesClientManager(options);
|
|
65
|
-
|
|
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);
|
|
66
144
|
// Injected providers — fall back to dynamic import() for backward compatibility
|
|
67
145
|
this.kroCustomResourceProvider = options.kroCustomResourceProvider;
|
|
68
146
|
this.rgdProvider = options.rgdProvider;
|
|
@@ -74,6 +152,164 @@ export class KroResourceFactoryImpl {
|
|
|
74
152
|
getNestedStatusCel() {
|
|
75
153
|
return this.statusMappings?.__nestedStatusCel;
|
|
76
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
|
+
}
|
|
77
313
|
/**
|
|
78
314
|
* Validate closures for Kro mode compatibility
|
|
79
315
|
* Kro mode only supports static values - no dynamic references (KubernetesRef)
|
|
@@ -108,10 +344,31 @@ export class KroResourceFactoryImpl {
|
|
|
108
344
|
getCustomObjectsApi() {
|
|
109
345
|
return this.clientManager.getCustomObjectsApi();
|
|
110
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
|
+
}
|
|
111
365
|
/**
|
|
112
366
|
* Deploy a new instance by creating a custom resource
|
|
113
367
|
*/
|
|
114
|
-
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
|
+
}
|
|
115
372
|
// Validate spec against ArkType schema
|
|
116
373
|
validateSpec(spec, this.schemaDefinition, {
|
|
117
374
|
kind: this.schemaDefinition.kind,
|
|
@@ -119,12 +376,106 @@ export class KroResourceFactoryImpl {
|
|
|
119
376
|
});
|
|
120
377
|
// Execute closures before RGD creation (Kro mode requirement)
|
|
121
378
|
await this.executeClosuresBeforeRGD(spec);
|
|
379
|
+
await this.ensureSingletonOwners(spec);
|
|
122
380
|
if (this.isAlchemyManaged) {
|
|
123
|
-
return this.deployWithAlchemy(spec);
|
|
381
|
+
return this.deployWithAlchemy(spec, opts?.instanceNameOverride, opts?.singletonSpecFingerprint);
|
|
124
382
|
}
|
|
125
383
|
else {
|
|
126
|
-
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
|
+
}
|
|
127
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;
|
|
467
|
+
}
|
|
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'));
|
|
128
479
|
}
|
|
129
480
|
/**
|
|
130
481
|
* Execute closures before RGD creation (Kro mode requirement)
|
|
@@ -142,6 +493,7 @@ export class KroResourceFactoryImpl {
|
|
|
142
493
|
// We need to execute them with a mock context to trigger validation
|
|
143
494
|
const mockDeploymentContext = {
|
|
144
495
|
// kubernetesApi intentionally omitted - not needed for validation
|
|
496
|
+
validationOnly: true,
|
|
145
497
|
namespace: this.namespace,
|
|
146
498
|
deployedResources: new Map(),
|
|
147
499
|
resolveReference: async (ref) => {
|
|
@@ -215,14 +567,20 @@ export class KroResourceFactoryImpl {
|
|
|
215
567
|
/**
|
|
216
568
|
* Deploy directly to Kubernetes using DirectDeploymentEngine
|
|
217
569
|
*/
|
|
218
|
-
async deployDirect(spec) {
|
|
570
|
+
async deployDirect(spec, instanceNameOverride, singletonSpecFingerprint) {
|
|
219
571
|
// Ensure RGD is deployed first
|
|
220
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();
|
|
221
579
|
// Create DirectDeploymentEngine with KRO mode for CEL string conversion
|
|
222
580
|
const deploymentEngine = new DirectDeploymentEngine(this.getKubeConfig(), undefined, undefined, DeploymentMode.KRO);
|
|
223
581
|
// Create custom resource instance
|
|
224
|
-
const instanceName = generateInstanceName(spec, this.name);
|
|
225
|
-
const customResourceData = this.createCustomResourceInstance(instanceName, spec);
|
|
582
|
+
const instanceName = instanceNameOverride ?? generateInstanceName(spec, this.name);
|
|
583
|
+
const customResourceData = this.createCustomResourceInstance(instanceName, spec, singletonSpecFingerprint);
|
|
226
584
|
// Wrap with kroCustomResource factory to get Enhanced object with readiness evaluation
|
|
227
585
|
const kroCustomResource = this.kroCustomResourceProvider ??
|
|
228
586
|
(await import('../../factories/kro/kro-custom-resource.js')).kroCustomResource;
|
|
@@ -230,8 +588,7 @@ export class KroResourceFactoryImpl {
|
|
|
230
588
|
apiVersion: customResourceData.apiVersion,
|
|
231
589
|
kind: customResourceData.kind,
|
|
232
590
|
metadata: {
|
|
233
|
-
|
|
234
|
-
namespace: customResourceData.metadata.namespace,
|
|
591
|
+
...customResourceData.metadata,
|
|
235
592
|
},
|
|
236
593
|
spec: customResourceData.spec,
|
|
237
594
|
});
|
|
@@ -250,148 +607,206 @@ export class KroResourceFactoryImpl {
|
|
|
250
607
|
preserveNonEnumerableProperties(enhancedCustomResource, deployableResource);
|
|
251
608
|
// Deploy without waiting for readiness - we'll handle that ourselves
|
|
252
609
|
this.logger.info('Deploying Kro instance', { instanceName, rgdName: this.rgdName });
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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();
|
|
266
634
|
}
|
|
267
|
-
this.logger.info('Instance ready, creating enhanced proxy', {
|
|
268
|
-
instanceName,
|
|
269
|
-
rgdName: this.rgdName,
|
|
270
|
-
});
|
|
271
|
-
// Create Enhanced proxy for the deployed instance
|
|
272
|
-
return await this.createEnhancedProxy(spec, instanceName);
|
|
273
635
|
}
|
|
274
636
|
/**
|
|
275
637
|
* Deploy using type-safe alchemy resource wrapping
|
|
276
638
|
*
|
|
277
639
|
* In alchemy mode, the RGD gets one typed alchemy Resource and each instance gets another
|
|
278
640
|
*/
|
|
279
|
-
async deployWithAlchemy(spec) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
641
|
+
async deployWithAlchemy(spec, instanceNameOverride, singletonSpecFingerprint) {
|
|
642
|
+
validateAlchemyScope(this.alchemyScope, 'KRO Alchemy deployment');
|
|
643
|
+
const alchemyScope = this.alchemyScope;
|
|
283
644
|
// Use static registration functions
|
|
284
645
|
// Create deployer instance using DirectDeploymentEngine with KRO mode
|
|
285
646
|
const kroEngine = new DirectDeploymentEngine(this.getKubeConfig(), undefined, undefined, DeploymentMode.KRO);
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
+
}
|
|
342
707
|
}
|
|
343
708
|
/**
|
|
344
709
|
* Get all deployed instances
|
|
345
710
|
*/
|
|
346
711
|
async getInstances() {
|
|
347
|
-
const
|
|
348
|
-
// Use Bun-compatible API client to ensure proper TLS handling
|
|
349
|
-
const { createBunCompatibleCustomObjectsApi } = await import('../kubernetes/bun-api-client.js');
|
|
350
|
-
const customApi = createBunCompatibleCustomObjectsApi(kubeConfig);
|
|
712
|
+
const customApi = await this.createCustomObjectsApi();
|
|
351
713
|
try {
|
|
352
|
-
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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);
|
|
357
726
|
// In the new API, methods take request objects and return objects directly
|
|
358
727
|
const listResponse = await customApi.listNamespacedCustomObject({
|
|
359
|
-
group:
|
|
728
|
+
group: this.getSchemaGroup(),
|
|
360
729
|
version,
|
|
361
730
|
namespace: this.namespace,
|
|
362
|
-
plural
|
|
731
|
+
plural,
|
|
363
732
|
});
|
|
364
733
|
const listResult = listResponse;
|
|
365
734
|
const instances = listResult.items || [];
|
|
366
735
|
return await Promise.all(instances.map(async (instance) => {
|
|
367
|
-
|
|
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;
|
|
368
752
|
}));
|
|
369
753
|
}
|
|
370
754
|
catch (error) {
|
|
371
755
|
const k8sError = error;
|
|
372
756
|
// If the CRD doesn't exist yet or there are no instances, return empty array
|
|
373
757
|
const bodyString = typeof k8sError.body === 'string' ? k8sError.body : JSON.stringify(k8sError.body || '');
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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'))) {
|
|
381
767
|
return [];
|
|
382
768
|
}
|
|
383
769
|
throw new CRDInstanceError(`Failed to list instances: ${k8sError.message || String(error)}`, this.schemaDefinition.apiVersion, this.schemaDefinition.kind, '*', 'statusResolution', ensureError(error));
|
|
384
770
|
}
|
|
385
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
|
+
}
|
|
386
795
|
/**
|
|
387
796
|
* Delete a specific instance by name
|
|
388
797
|
*/
|
|
389
|
-
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
|
+
}
|
|
390
802
|
const kubeConfig = this.getKubeConfig();
|
|
391
803
|
const k8sApi = createBunCompatibleKubernetesObjectApi(kubeConfig);
|
|
392
|
-
const apiVersion = this.
|
|
393
|
-
|
|
394
|
-
|
|
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;
|
|
395
810
|
try {
|
|
396
811
|
// Delete the instance. KRO's controller processes kro.run/finalizer,
|
|
397
812
|
// which does graph-based deletion of all child resources.
|
|
@@ -406,12 +821,8 @@ export class KroResourceFactoryImpl {
|
|
|
406
821
|
// Wait for KRO to finish cleanup (finalizer processing).
|
|
407
822
|
// KRO needs the RGD to exist during this phase — the caller must
|
|
408
823
|
// not delete the RGD until deleteInstance completes.
|
|
409
|
-
|
|
410
|
-
// KRO processes ~15-30s per resource for finalizer cleanup.
|
|
411
|
-
const MAX_DELETION_WAIT = 300000;
|
|
412
|
-
const timeout = Math.min(this.factoryOptions.timeout ?? 120000, MAX_DELETION_WAIT);
|
|
824
|
+
const timeout = this.factoryOptions.timeout ?? 300000;
|
|
413
825
|
const startTime = Date.now();
|
|
414
|
-
let deleted = false;
|
|
415
826
|
while (Date.now() - startTime < timeout) {
|
|
416
827
|
try {
|
|
417
828
|
await k8sApi.read({
|
|
@@ -420,13 +831,13 @@ export class KroResourceFactoryImpl {
|
|
|
420
831
|
metadata: { name, namespace: this.namespace },
|
|
421
832
|
});
|
|
422
833
|
// Still exists — KRO is processing finalizer
|
|
423
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
834
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
424
835
|
}
|
|
425
836
|
catch (pollError) {
|
|
426
837
|
const pollK8sError = pollError;
|
|
427
838
|
const errorCode = pollK8sError.statusCode ?? pollK8sError.code ?? pollK8sError.body?.code;
|
|
428
839
|
if (errorCode === 404) {
|
|
429
|
-
|
|
840
|
+
instanceDeleted = true;
|
|
430
841
|
break;
|
|
431
842
|
}
|
|
432
843
|
// Non-404 error (permissions, server error) — log and retry
|
|
@@ -434,14 +845,14 @@ export class KroResourceFactoryImpl {
|
|
|
434
845
|
name,
|
|
435
846
|
errorCode,
|
|
436
847
|
});
|
|
437
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
848
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
438
849
|
}
|
|
439
850
|
}
|
|
440
|
-
if (!
|
|
441
|
-
// KRO is still processing the finalizer.
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
|
|
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;
|
|
445
856
|
this.logger.warn('Instance deletion still in progress after timeout', {
|
|
446
857
|
name,
|
|
447
858
|
timeout,
|
|
@@ -453,19 +864,24 @@ export class KroResourceFactoryImpl {
|
|
|
453
864
|
catch (error) {
|
|
454
865
|
const k8sError = error;
|
|
455
866
|
const errorCode = k8sError.statusCode ?? k8sError.code ?? k8sError.body?.code;
|
|
456
|
-
if (errorCode
|
|
867
|
+
if (errorCode === 404) {
|
|
868
|
+
instanceDeleted = true;
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
457
871
|
throw new CRDInstanceError(`Failed to delete instance ${name}: ${k8sError.message || String(error)}`, this.schemaDefinition.apiVersion, this.schemaDefinition.kind, name, 'deletion', ensureError(error));
|
|
458
872
|
}
|
|
459
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
|
+
}
|
|
460
877
|
// Only delete the RGD and CRD if no other instances remain. Multiple
|
|
461
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.
|
|
462
881
|
let hasRemainingInstances = false;
|
|
463
882
|
try {
|
|
464
|
-
const instances = await this.
|
|
465
|
-
|
|
466
|
-
// if the API server's list cache hasn't caught up with the 404 on GET.
|
|
467
|
-
const others = instances.filter(i => i.metadata?.name !== name);
|
|
468
|
-
hasRemainingInstances = others.length > 0;
|
|
883
|
+
const instances = await this.listInstancesForCleanup();
|
|
884
|
+
hasRemainingInstances = shouldPreserveRgd(instances, name, instanceDeleted, this.namespace);
|
|
469
885
|
}
|
|
470
886
|
catch (listError) {
|
|
471
887
|
// Can't list instances — could be CRD gone (safe) or transient error
|
|
@@ -478,6 +894,10 @@ export class KroResourceFactoryImpl {
|
|
|
478
894
|
hasRemainingInstances = true;
|
|
479
895
|
}
|
|
480
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();
|
|
481
901
|
// Delete the RGD after the instance is gone.
|
|
482
902
|
try {
|
|
483
903
|
await k8sApi.delete({
|
|
@@ -491,13 +911,18 @@ export class KroResourceFactoryImpl {
|
|
|
491
911
|
const k8sErr = error;
|
|
492
912
|
const errorCode = k8sErr.statusCode ?? k8sErr.code ?? k8sErr.body?.code;
|
|
493
913
|
if (errorCode !== 404) {
|
|
494
|
-
this.logger.warn('RGD cleanup failed', {
|
|
914
|
+
this.logger.warn('RGD cleanup failed', {
|
|
915
|
+
rgdName: this.rgdName,
|
|
916
|
+
error: ensureError(error).message,
|
|
917
|
+
});
|
|
918
|
+
throw error;
|
|
495
919
|
}
|
|
496
920
|
}
|
|
497
921
|
// Delete the CRD that KRO created from the RGD. KRO's default config
|
|
498
922
|
// has allowCRDDeletion=false, so it won't clean up the CRD when the
|
|
499
|
-
// RGD is deleted.
|
|
500
|
-
|
|
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()}`;
|
|
501
926
|
try {
|
|
502
927
|
await k8sApi.delete({
|
|
503
928
|
apiVersion: 'apiextensions.k8s.io/v1',
|
|
@@ -510,7 +935,8 @@ export class KroResourceFactoryImpl {
|
|
|
510
935
|
const k8sErr = error;
|
|
511
936
|
const errorCode = k8sErr.statusCode ?? k8sErr.code ?? k8sErr.body?.code;
|
|
512
937
|
if (errorCode !== 404) {
|
|
513
|
-
this.logger.
|
|
938
|
+
this.logger.warn('CRD cleanup failed', { crdName, error: ensureError(error).message });
|
|
939
|
+
throw error;
|
|
514
940
|
}
|
|
515
941
|
}
|
|
516
942
|
}
|
|
@@ -523,6 +949,52 @@ export class KroResourceFactoryImpl {
|
|
|
523
949
|
// KRO's finalizer processing handles deleting all child resources
|
|
524
950
|
// (including Namespaces) via its applyset — no manual cleanup needed.
|
|
525
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
|
+
}
|
|
526
998
|
/**
|
|
527
999
|
* Get factory status
|
|
528
1000
|
*/
|
|
@@ -551,7 +1023,6 @@ export class KroResourceFactoryImpl {
|
|
|
551
1023
|
kind: 'ResourceGraphDefinition',
|
|
552
1024
|
metadata: {
|
|
553
1025
|
name: this.rgdName,
|
|
554
|
-
namespace: this.namespace,
|
|
555
1026
|
},
|
|
556
1027
|
});
|
|
557
1028
|
const rgd = response;
|
|
@@ -602,20 +1073,114 @@ export class KroResourceFactoryImpl {
|
|
|
602
1073
|
// Generate CRD instance YAML
|
|
603
1074
|
const instanceName = generateInstanceName(spec, this.name);
|
|
604
1075
|
const customResource = this.createCustomResourceInstance(instanceName, spec);
|
|
605
|
-
return
|
|
606
|
-
kind: ${customResource.kind}
|
|
607
|
-
metadata:
|
|
608
|
-
name: ${customResource.metadata.name}
|
|
609
|
-
namespace: ${customResource.metadata.namespace}
|
|
610
|
-
spec:
|
|
611
|
-
${Object.entries(spec)
|
|
612
|
-
.map(([key, value]) => ` ${key}: ${typeof value === 'string' ? `"${value}"` : value}`)
|
|
613
|
-
.join('\n')}`;
|
|
1076
|
+
return yaml.dump(customResource, { lineWidth: -1, noRefs: true, sortKeys: false }).trimEnd();
|
|
614
1077
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
1078
|
+
return this.buildRgdYaml();
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Build the RGD YAML string shared by `toYaml()` and `ensureRGDDeployed()`.
|
|
1082
|
+
*
|
|
1083
|
+
* Both call sites must apply ternary-conditional post-processing;
|
|
1084
|
+
* extracting the logic ensures the two paths stay in sync and share the
|
|
1085
|
+
* single-apply guard on `this.ternaryAndOmitApplied`.
|
|
1086
|
+
*
|
|
1087
|
+
* Note: omit() wrapping for optional fields is no longer a post-processing
|
|
1088
|
+
* step — it's applied inline during ref-to-CEL conversion via
|
|
1089
|
+
* `SerializationContext.omitFields`, which `serializeResourceGraphToYaml`
|
|
1090
|
+
* populates from `kroSchema.__omitFields` automatically.
|
|
1091
|
+
*/
|
|
1092
|
+
buildRgdYaml() {
|
|
1093
|
+
if (this.factoryOptions.compositionAnalysis && !this.compositionAnalysisApplied) {
|
|
1094
|
+
this.compositionAnalysisApplied = true;
|
|
1095
|
+
applyAnalysisToResources(this.resources, this.factoryOptions.compositionAnalysis);
|
|
1096
|
+
}
|
|
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
|
+
}
|
|
1115
|
+
// Apply ternary conditionals to this.resources BEFORE serialization.
|
|
1116
|
+
// Mutates in place (JSON.clone is NOT safe here because it strips
|
|
1117
|
+
// proxy-valued fields like `metadata.namespace` that are KubernetesRef
|
|
1118
|
+
// proxies with typeof function). The single-apply guard mirrors
|
|
1119
|
+
// core.ts — the underlying operation is idempotent today, but we
|
|
1120
|
+
// explicitly skip re-runs to keep both paths structurally identical
|
|
1121
|
+
// and avoid depending on that idempotency across future refactors.
|
|
1122
|
+
if (!this.ternaryAndOmitApplied) {
|
|
1123
|
+
this.ternaryAndOmitApplied = true;
|
|
1124
|
+
if (kroSchema.__ternaryConditionals?.length) {
|
|
1125
|
+
applyTernaryConditionalsToResources(this.resources, kroSchema.__ternaryConditionals, kroSchema.__nestedStatusCel);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return serializeResourceGraphToYaml(this.rgdName, this.resources, { namespace: this.namespace }, kroSchema);
|
|
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
|
+
});
|
|
619
1184
|
}
|
|
620
1185
|
}
|
|
621
1186
|
/**
|
|
@@ -624,9 +1189,9 @@ ${Object.entries(spec)
|
|
|
624
1189
|
async ensureRGDDeployed() {
|
|
625
1190
|
// Create DirectDeploymentEngine instance with KRO mode for CEL string generation
|
|
626
1191
|
const deploymentEngine = new DirectDeploymentEngine(this.getKubeConfig(), undefined, undefined, DeploymentMode.KRO);
|
|
627
|
-
//
|
|
628
|
-
|
|
629
|
-
const rgdYaml =
|
|
1192
|
+
// Build the RGD YAML — shared with toYaml() so both call sites emit
|
|
1193
|
+
// identical post-processed output and share the single-apply guard.
|
|
1194
|
+
const rgdYaml = this.buildRgdYaml();
|
|
630
1195
|
// Parse the YAML to get the RGD object
|
|
631
1196
|
const rgdManifests = k8s.loadAllYaml(rgdYaml);
|
|
632
1197
|
const rgdManifest = rgdManifests[0];
|
|
@@ -636,9 +1201,9 @@ ${Object.entries(spec)
|
|
|
636
1201
|
metadata: {
|
|
637
1202
|
...rgdManifest.metadata,
|
|
638
1203
|
name: this.rgdName,
|
|
639
|
-
namespace: this.namespace,
|
|
640
1204
|
},
|
|
641
1205
|
};
|
|
1206
|
+
await this.addRgdSchemaStatusPruneMarkers(rgdWithMetadata);
|
|
642
1207
|
// Create Enhanced RGD with readiness evaluator
|
|
643
1208
|
const rgdFactory = this.rgdProvider ??
|
|
644
1209
|
(await import('../../factories/kro/resource-graph-definition.js')).resourceGraphDefinition;
|
|
@@ -650,6 +1215,7 @@ ${Object.entries(spec)
|
|
|
650
1215
|
};
|
|
651
1216
|
// Preserve non-enumerable properties (readinessEvaluator, __resourceId) lost during spread
|
|
652
1217
|
preserveNonEnumerableProperties(enhancedRGD, deployableRGD);
|
|
1218
|
+
setMetadataField(deployableRGD, 'scope', 'cluster');
|
|
653
1219
|
// Debug: Log the RGD being deployed
|
|
654
1220
|
this.logger.debug('Deploying RGD', {
|
|
655
1221
|
rgdName: this.rgdName,
|
|
@@ -678,7 +1244,7 @@ ${Object.entries(spec)
|
|
|
678
1244
|
const rgdStatus = await k8sApi.read({
|
|
679
1245
|
apiVersion: 'kro.run/v1alpha1',
|
|
680
1246
|
kind: 'ResourceGraphDefinition',
|
|
681
|
-
metadata: { name: this.rgdName
|
|
1247
|
+
metadata: { name: this.rgdName },
|
|
682
1248
|
});
|
|
683
1249
|
const rgdResult = rgdStatus;
|
|
684
1250
|
this.logger.error('RGD deployment failed, current status:', undefined, {
|
|
@@ -692,19 +1258,24 @@ ${Object.entries(spec)
|
|
|
692
1258
|
}
|
|
693
1259
|
throw new ResourceGraphFactoryError(`Failed to deploy RGD using DirectDeploymentEngine: ${ensureError(error).message}`, this.name, 'deployment', ensureError(error));
|
|
694
1260
|
}
|
|
1261
|
+
finally {
|
|
1262
|
+
await deploymentEngine.dispose();
|
|
1263
|
+
}
|
|
695
1264
|
}
|
|
696
1265
|
/**
|
|
697
|
-
* 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").
|
|
698
1272
|
*/
|
|
699
1273
|
async waitForCRDReadyWithEngine(deploymentEngine) {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
// Use the deployment engine's built-in CRD readiness checking
|
|
706
|
-
// This will wait for the CRD to be created by Kro and become ready
|
|
707
|
-
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;
|
|
708
1279
|
}
|
|
709
1280
|
/**
|
|
710
1281
|
* Separate static and dynamic status fields
|
|
@@ -723,61 +1294,78 @@ ${Object.entries(spec)
|
|
|
723
1294
|
async evaluateStaticFields(staticFields, spec) {
|
|
724
1295
|
const evaluatedFields = {};
|
|
725
1296
|
for (const [fieldName, fieldValue] of Object.entries(staticFields)) {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
expression: fieldValue.expression,
|
|
736
|
-
error: ensureError(error).message,
|
|
737
|
-
});
|
|
738
|
-
// Fallback to the original value
|
|
739
|
-
evaluatedFields[fieldName] = fieldValue;
|
|
740
|
-
}
|
|
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);
|
|
741
1306
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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;
|
|
749
1315
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1316
|
+
}
|
|
1317
|
+
if (isKubernetesRef(fieldValue)) {
|
|
1318
|
+
if (fieldValue.resourceId === '__schema__') {
|
|
1319
|
+
return this.resolveSchemaRefValue(fieldValue.fieldPath, spec);
|
|
1320
|
+
}
|
|
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);
|
|
768
1337
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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];
|
|
774
1363
|
}
|
|
775
1364
|
else {
|
|
776
|
-
|
|
777
|
-
evaluatedFields[fieldName] = fieldValue;
|
|
1365
|
+
return undefined;
|
|
778
1366
|
}
|
|
779
1367
|
}
|
|
780
|
-
return
|
|
1368
|
+
return current;
|
|
781
1369
|
}
|
|
782
1370
|
/**
|
|
783
1371
|
* Evaluate a static CEL expression that contains only schema references or literal values.
|
|
@@ -788,22 +1376,8 @@ ${Object.entries(spec)
|
|
|
788
1376
|
*/
|
|
789
1377
|
evaluateStaticCelExpression(celExpression, spec) {
|
|
790
1378
|
const expression = celExpression.expression;
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
// eliminates any residual risk from constructor/toString/__proto__ leaking.
|
|
794
|
-
// Object.freeze prevents expression-based mutation of the original spec data.
|
|
795
|
-
const specRecord = Object.freeze(Object.assign(Object.create(null), spec));
|
|
796
|
-
// Build a scope expression by stripping schema.spec. or spec. prefixes so that
|
|
797
|
-
// angular-expressions can resolve field references directly from the spec scope.
|
|
798
|
-
let scopeExpression = expression;
|
|
799
|
-
if (expression.includes('schema.spec.')) {
|
|
800
|
-
// Replace schema.spec.fieldName → fieldName (resolved from scope)
|
|
801
|
-
scopeExpression = scopeExpression.replace(/schema\.spec\.(\w+)/g, '$1');
|
|
802
|
-
}
|
|
803
|
-
if (scopeExpression.includes('spec.')) {
|
|
804
|
-
// Replace spec.fieldName → fieldName (resolved from scope)
|
|
805
|
-
scopeExpression = scopeExpression.replace(/\bspec\.(\w+)/g, '$1');
|
|
806
|
-
}
|
|
1379
|
+
const specRecord = this.createStaticEvaluationScope(spec);
|
|
1380
|
+
const scopeExpression = this.prepareStaticExpressionForEvaluation(expression);
|
|
807
1381
|
try {
|
|
808
1382
|
const evaluator = compileExpression(scopeExpression);
|
|
809
1383
|
const result = evaluator(specRecord);
|
|
@@ -835,7 +1409,7 @@ ${Object.entries(spec)
|
|
|
835
1409
|
* At deploy time we replace each marker with the actual spec value.
|
|
836
1410
|
*/
|
|
837
1411
|
resolveSchemaRefMarkers(value, spec) {
|
|
838
|
-
const resolved = value.replace(
|
|
1412
|
+
const resolved = value.replace(new RegExp(KUBERNETES_REF_SCHEMA_MARKER_SOURCE, 'g'), (_match, fieldPath) => {
|
|
839
1413
|
// fieldPath is e.g. "spec.name" or "spec.nested.field"
|
|
840
1414
|
const parts = fieldPath.replace(/^spec\./, '').split('.');
|
|
841
1415
|
let current = spec;
|
|
@@ -866,18 +1440,12 @@ ${Object.entries(spec)
|
|
|
866
1440
|
evaluateInlineCelString(celString, spec) {
|
|
867
1441
|
// Strip the wrapping ${ ... }
|
|
868
1442
|
const innerExpression = celString.slice(2, -1);
|
|
869
|
-
// Build scope expression: strip schema.spec. / spec. prefixes
|
|
870
|
-
let scopeExpression = innerExpression;
|
|
871
|
-
if (scopeExpression.includes('schema.spec.')) {
|
|
872
|
-
scopeExpression = scopeExpression.replace(/schema\.spec\.(\w+)/g, '$1');
|
|
873
|
-
}
|
|
874
|
-
if (scopeExpression.includes('spec.')) {
|
|
875
|
-
scopeExpression = scopeExpression.replace(/\bspec\.(\w+)/g, '$1');
|
|
876
|
-
}
|
|
1443
|
+
// Build scope expression: translate KRO helpers and strip schema.spec. / spec. prefixes
|
|
1444
|
+
let scopeExpression = this.prepareStaticExpressionForEvaluation(innerExpression);
|
|
877
1445
|
// Resolve any __KUBERNETES_REF_ markers that may be embedded in the expression
|
|
878
1446
|
// (e.g. from template literals inside ternary branches)
|
|
879
1447
|
if (scopeExpression.includes('__KUBERNETES_REF___schema___')) {
|
|
880
|
-
scopeExpression = scopeExpression.replace(
|
|
1448
|
+
scopeExpression = scopeExpression.replace(new RegExp(KUBERNETES_REF_SCHEMA_MARKER_SOURCE, 'g'), (_match, fieldPath) => {
|
|
881
1449
|
const parts = fieldPath.replace(/^spec\./, '').split('.');
|
|
882
1450
|
return parts.join('.');
|
|
883
1451
|
});
|
|
@@ -885,10 +1453,52 @@ ${Object.entries(spec)
|
|
|
885
1453
|
// Convert CEL single-quoted string literals to double-quoted for angular-expressions
|
|
886
1454
|
// Match single-quoted strings that are NOT inside backticks
|
|
887
1455
|
scopeExpression = scopeExpression.replace(/'([^'\\]*)'/g, '"$1"');
|
|
888
|
-
const specRecord =
|
|
1456
|
+
const specRecord = this.createStaticEvaluationScope(spec);
|
|
889
1457
|
const evaluator = compileExpression(scopeExpression);
|
|
890
1458
|
return evaluator(specRecord);
|
|
891
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
|
+
}
|
|
892
1502
|
/**
|
|
893
1503
|
* Check if a value is a CEL expression (using canonical brand symbol)
|
|
894
1504
|
*/
|
|
@@ -904,18 +1514,26 @@ ${Object.entries(spec)
|
|
|
904
1514
|
/**
|
|
905
1515
|
* Create custom resource instance
|
|
906
1516
|
*/
|
|
907
|
-
createCustomResourceInstance(instanceName, spec) {
|
|
908
|
-
|
|
909
|
-
// We need to construct the full API version for the instance (e.g., 'kro.run/v1alpha1')
|
|
910
|
-
const apiVersion = this.schemaDefinition.apiVersion.includes('/')
|
|
911
|
-
? this.schemaDefinition.apiVersion // Already has group prefix
|
|
912
|
-
: `kro.run/${this.schemaDefinition.apiVersion}`; // Add kro.run group
|
|
1517
|
+
createCustomResourceInstance(instanceName, spec, singletonSpecFingerprint) {
|
|
1518
|
+
const apiVersion = this.getInstanceApiVersion();
|
|
913
1519
|
return {
|
|
914
1520
|
apiVersion,
|
|
915
1521
|
kind: this.schemaDefinition.kind,
|
|
916
1522
|
metadata: {
|
|
917
1523
|
name: instanceName,
|
|
918
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
|
+
: {}),
|
|
919
1537
|
},
|
|
920
1538
|
spec,
|
|
921
1539
|
};
|
|
@@ -933,9 +1551,7 @@ ${Object.entries(spec)
|
|
|
933
1551
|
const status = { ...evaluatedStaticFields };
|
|
934
1552
|
// Create the initial Enhanced proxy
|
|
935
1553
|
// The Enhanced proxy should represent the actual instance, which uses the full API version
|
|
936
|
-
const instanceApiVersion = this.
|
|
937
|
-
? this.schemaDefinition.apiVersion
|
|
938
|
-
: `kro.run/${this.schemaDefinition.apiVersion}`;
|
|
1554
|
+
const instanceApiVersion = this.getInstanceApiVersion();
|
|
939
1555
|
const enhancedProxy = {
|
|
940
1556
|
apiVersion: instanceApiVersion,
|
|
941
1557
|
kind: this.schemaDefinition.kind,
|
|
@@ -979,13 +1595,20 @@ ${Object.entries(spec)
|
|
|
979
1595
|
}
|
|
980
1596
|
// Post-process: re-execute the composition with live cluster data to fill
|
|
981
1597
|
// in status fields that neither static evaluation nor KRO could provide.
|
|
982
|
-
if (this.factoryOptions.compositionFn) {
|
|
1598
|
+
if (this.factoryOptions.hydrateStatus !== false && this.factoryOptions.compositionFn) {
|
|
983
1599
|
try {
|
|
984
1600
|
const liveStatus = await this.reExecuteWithLiveStatus(spec);
|
|
985
1601
|
if (liveStatus) {
|
|
986
1602
|
for (const [key, value] of Object.entries(liveStatus)) {
|
|
987
1603
|
if (key.startsWith('__'))
|
|
988
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
|
+
}
|
|
989
1612
|
const current = enhancedProxy.status[key];
|
|
990
1613
|
if (current === undefined || current === null || current === '') {
|
|
991
1614
|
enhancedProxy.status[key] = value;
|
|
@@ -1017,12 +1640,8 @@ ${Object.entries(spec)
|
|
|
1017
1640
|
const k8sApi = createBunCompatibleKubernetesObjectApi(kubeConfig);
|
|
1018
1641
|
const resourceEntries = Object.entries(this.resources);
|
|
1019
1642
|
const results = await Promise.allSettled(resourceEntries.map(async ([resourceId, resource]) => {
|
|
1020
|
-
const name =
|
|
1021
|
-
|
|
1022
|
-
: resourceId;
|
|
1023
|
-
const ns = typeof resource.metadata?.namespace === 'string'
|
|
1024
|
-
? resource.metadata.namespace
|
|
1025
|
-
: this.namespace;
|
|
1643
|
+
const name = this.resolveLiveResourceIdentityValue(resource.metadata?.name, spec, resourceId);
|
|
1644
|
+
const ns = this.resolveLiveResourceIdentityValue(resource.metadata?.namespace, spec, this.namespace);
|
|
1026
1645
|
const isClusterScoped = getMetadataField(resource, 'scope') === 'cluster';
|
|
1027
1646
|
const live = await k8sApi.read({
|
|
1028
1647
|
apiVersion: resource.apiVersion || '',
|
|
@@ -1030,9 +1649,14 @@ ${Object.entries(spec)
|
|
|
1030
1649
|
metadata: { name, ...(isClusterScoped ? {} : { namespace: ns }) },
|
|
1031
1650
|
});
|
|
1032
1651
|
if (live && typeof live === 'object' && 'status' in live) {
|
|
1033
|
-
return {
|
|
1652
|
+
return {
|
|
1653
|
+
resourceId,
|
|
1654
|
+
status: live.status,
|
|
1655
|
+
};
|
|
1034
1656
|
}
|
|
1035
|
-
|
|
1657
|
+
// Statusless resources (Service, ConfigMap, Secret, etc.) should still
|
|
1658
|
+
// count as visible children for nested-composition recovery.
|
|
1659
|
+
return { resourceId, status: {} };
|
|
1036
1660
|
}));
|
|
1037
1661
|
for (const result of results) {
|
|
1038
1662
|
if (result.status === 'fulfilled' && result.value) {
|
|
@@ -1042,19 +1666,76 @@ ${Object.entries(spec)
|
|
|
1042
1666
|
// Probe to discover nested composition IDs
|
|
1043
1667
|
const probeContext = createCompositionContext('kro-re-execution-probe', {
|
|
1044
1668
|
deduplicateIds: true,
|
|
1669
|
+
isReExecution: true,
|
|
1045
1670
|
});
|
|
1046
1671
|
probeContext.liveStatusMap = liveStatusMap;
|
|
1047
1672
|
runWithCompositionContext(probeContext, () => compositionFn(spec));
|
|
1048
1673
|
// Synthesize nested composition status
|
|
1049
|
-
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
|
+
}
|
|
1050
1696
|
// Real execution with live status
|
|
1051
1697
|
const reExecutionContext = createCompositionContext('kro-re-execution', {
|
|
1052
1698
|
deduplicateIds: true,
|
|
1699
|
+
isReExecution: true,
|
|
1053
1700
|
});
|
|
1054
1701
|
reExecutionContext.liveStatusMap = enrichedMap;
|
|
1055
1702
|
const result = runWithCompositionContext(reExecutionContext, () => compositionFn(spec));
|
|
1056
1703
|
return result;
|
|
1057
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
|
+
}
|
|
1058
1739
|
/**
|
|
1059
1740
|
* Create an Enhanced proxy for the instance (backward compatibility method)
|
|
1060
1741
|
*/
|
|
@@ -1066,9 +1747,7 @@ ${Object.entries(spec)
|
|
|
1066
1747
|
* Delegates to the shared `waitForKroInstanceReady` in `kro-readiness.ts`.
|
|
1067
1748
|
*/
|
|
1068
1749
|
async waitForKroInstanceReady(instanceName, timeout) {
|
|
1069
|
-
const apiVersion = this.
|
|
1070
|
-
? this.schemaDefinition.apiVersion
|
|
1071
|
-
: `kro.run/${this.schemaDefinition.apiVersion}`;
|
|
1750
|
+
const apiVersion = this.getInstanceApiVersion();
|
|
1072
1751
|
const kubeConfig = this.getKubeConfig();
|
|
1073
1752
|
const k8sApi = createBunCompatibleKubernetesObjectApi(kubeConfig);
|
|
1074
1753
|
return waitForKroInstanceReadyShared({
|
|
@@ -1079,7 +1758,7 @@ ${Object.entries(spec)
|
|
|
1079
1758
|
namespace: this.namespace,
|
|
1080
1759
|
apiVersion,
|
|
1081
1760
|
kind: this.schemaDefinition.kind,
|
|
1082
|
-
rgdName: this.
|
|
1761
|
+
rgdName: this.rgdName,
|
|
1083
1762
|
factoryContext: this.name,
|
|
1084
1763
|
});
|
|
1085
1764
|
}
|
|
@@ -1089,9 +1768,7 @@ ${Object.entries(spec)
|
|
|
1089
1768
|
async hydrateDynamicStatusFields(instanceName, dynamicFields) {
|
|
1090
1769
|
const dynamicLogger = this.logger.child({ instanceName });
|
|
1091
1770
|
// Get the live custom resource to extract dynamic status fields
|
|
1092
|
-
const apiVersion = this.
|
|
1093
|
-
? this.schemaDefinition.apiVersion
|
|
1094
|
-
: `kro.run/${this.schemaDefinition.apiVersion}`;
|
|
1771
|
+
const apiVersion = this.getInstanceApiVersion();
|
|
1095
1772
|
const kubeConfig = this.getKubeConfig();
|
|
1096
1773
|
const k8sApi = createBunCompatibleKubernetesObjectApi(kubeConfig);
|
|
1097
1774
|
const response = await k8sApi.read({
|