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