typekro 0.33.9 → 0.34.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/compositions/typekro-runtime/typekro-runtime.d.ts +0 -33
- package/dist/compositions/typekro-runtime/typekro-runtime.d.ts.map +1 -1
- package/dist/compositions/typekro-runtime/typekro-runtime.js +63 -1
- package/dist/compositions/typekro-runtime/typekro-runtime.js.map +1 -1
- package/dist/compositions/typekro-runtime/types.d.ts +8 -0
- package/dist/compositions/typekro-runtime/types.d.ts.map +1 -1
- package/dist/compositions/typekro-runtime/types.js +1 -0
- package/dist/compositions/typekro-runtime/types.js.map +1 -1
- package/dist/core/aspects/values-merge.d.ts +17 -0
- package/dist/core/aspects/values-merge.d.ts.map +1 -1
- package/dist/core/aspects/values-merge.js +67 -8
- package/dist/core/aspects/values-merge.js.map +1 -1
- package/dist/core/deployment/direct-factory.d.ts +9 -0
- package/dist/core/deployment/direct-factory.d.ts.map +1 -1
- package/dist/core/deployment/direct-factory.js +37 -3
- package/dist/core/deployment/direct-factory.js.map +1 -1
- package/dist/core/deployment/engine.d.ts +34 -0
- package/dist/core/deployment/engine.d.ts.map +1 -1
- package/dist/core/deployment/engine.js +173 -29
- package/dist/core/deployment/engine.js.map +1 -1
- package/dist/core/deployment/index.d.ts +2 -0
- package/dist/core/deployment/index.d.ts.map +1 -1
- package/dist/core/deployment/index.js +1 -0
- package/dist/core/deployment/index.js.map +1 -1
- package/dist/core/deployment/k8s-helpers.d.ts +73 -0
- package/dist/core/deployment/k8s-helpers.d.ts.map +1 -1
- package/dist/core/deployment/k8s-helpers.js +198 -4
- package/dist/core/deployment/k8s-helpers.js.map +1 -1
- package/dist/core/deployment/kro-factory.d.ts.map +1 -1
- package/dist/core/deployment/kro-factory.js +21 -1
- package/dist/core/deployment/kro-factory.js.map +1 -1
- package/dist/core/deployment/status-leaf-resolution.d.ts +101 -0
- package/dist/core/deployment/status-leaf-resolution.d.ts.map +1 -0
- package/dist/core/deployment/status-leaf-resolution.js +232 -0
- package/dist/core/deployment/status-leaf-resolution.js.map +1 -0
- package/dist/core/deployment/strategies/base-strategy.d.ts.map +1 -1
- package/dist/core/deployment/strategies/base-strategy.js +31 -3
- package/dist/core/deployment/strategies/base-strategy.js.map +1 -1
- package/dist/core/deployment/strategies/kro-strategy.d.ts.map +1 -1
- package/dist/core/deployment/strategies/kro-strategy.js +8 -1
- package/dist/core/deployment/strategies/kro-strategy.js.map +1 -1
- package/dist/core/expressions/composition/composition-analyzer-helpers.d.ts.map +1 -1
- package/dist/core/expressions/composition/composition-analyzer-helpers.js +20 -2
- package/dist/core/expressions/composition/composition-analyzer-helpers.js.map +1 -1
- package/dist/core/kro/index.d.ts +6 -0
- package/dist/core/kro/index.d.ts.map +1 -0
- package/dist/core/kro/index.js +6 -0
- package/dist/core/kro/index.js.map +1 -0
- package/dist/core/kro/label-guard-capability.d.ts +141 -0
- package/dist/core/kro/label-guard-capability.d.ts.map +1 -0
- package/dist/core/kro/label-guard-capability.js +257 -0
- package/dist/core/kro/label-guard-capability.js.map +1 -0
- package/dist/core/kro/labels.d.ts +56 -0
- package/dist/core/kro/labels.d.ts.map +1 -0
- package/dist/core/kro/labels.js +62 -0
- package/dist/core/kro/labels.js.map +1 -0
- package/dist/core/kubernetes/api-capability.d.ts +196 -0
- package/dist/core/kubernetes/api-capability.d.ts.map +1 -0
- package/dist/core/kubernetes/api-capability.js +301 -0
- package/dist/core/kubernetes/api-capability.js.map +1 -0
- package/dist/core/kubernetes/index.d.ts +2 -0
- package/dist/core/kubernetes/index.d.ts.map +1 -1
- package/dist/core/kubernetes/index.js +2 -0
- package/dist/core/kubernetes/index.js.map +1 -1
- package/dist/core/kubernetes/naming.d.ts +100 -0
- package/dist/core/kubernetes/naming.d.ts.map +1 -0
- package/dist/core/kubernetes/naming.js +93 -0
- package/dist/core/kubernetes/naming.js.map +1 -0
- package/dist/core/planning/direct-runtime-adapter.d.ts.map +1 -1
- package/dist/core/planning/direct-runtime-adapter.js +54 -17
- package/dist/core/planning/direct-runtime-adapter.js.map +1 -1
- package/dist/core/planning/planner.d.ts.map +1 -1
- package/dist/core/planning/planner.js +17 -0
- package/dist/core/planning/planner.js.map +1 -1
- package/dist/core/planning/schema.d.ts.map +1 -1
- package/dist/core/planning/schema.js +7 -3
- package/dist/core/planning/schema.js.map +1 -1
- package/dist/core/references/cel-lexical-scanner.d.ts +100 -1
- package/dist/core/references/cel-lexical-scanner.d.ts.map +1 -1
- package/dist/core/references/cel-lexical-scanner.js +196 -2
- package/dist/core/references/cel-lexical-scanner.js.map +1 -1
- package/dist/core/references/cel.d.ts +249 -1
- package/dist/core/references/cel.d.ts.map +1 -1
- package/dist/core/references/cel.js +289 -8
- package/dist/core/references/cel.js.map +1 -1
- package/dist/core/serialization/arktype-ast.d.ts +21 -0
- package/dist/core/serialization/arktype-ast.d.ts.map +1 -0
- package/dist/core/serialization/arktype-ast.js +29 -0
- package/dist/core/serialization/arktype-ast.js.map +1 -0
- package/dist/core/serialization/cel-references.d.ts +133 -32
- package/dist/core/serialization/cel-references.d.ts.map +1 -1
- package/dist/core/serialization/cel-references.js +908 -109
- package/dist/core/serialization/cel-references.js.map +1 -1
- package/dist/core/serialization/core.d.ts.map +1 -1
- package/dist/core/serialization/core.js +13 -2
- package/dist/core/serialization/core.js.map +1 -1
- package/dist/core/serialization/schema.d.ts +16 -2
- package/dist/core/serialization/schema.d.ts.map +1 -1
- package/dist/core/serialization/schema.js +113 -21
- package/dist/core/serialization/schema.js.map +1 -1
- package/dist/core/types/composable.d.ts +7 -1
- package/dist/core/types/composable.d.ts.map +1 -1
- package/dist/core/types/deployment.d.ts +32 -2
- package/dist/core/types/deployment.d.ts.map +1 -1
- package/dist/core/types/serialization.d.ts +37 -0
- package/dist/core/types/serialization.d.ts.map +1 -1
- package/dist/core/validation/cel-dialect.d.ts +246 -0
- package/dist/core/validation/cel-dialect.d.ts.map +1 -0
- package/dist/core/validation/cel-dialect.js +1997 -0
- package/dist/core/validation/cel-dialect.js.map +1 -0
- package/dist/core/validation/cel-validator.d.ts.map +1 -1
- package/dist/core/validation/cel-validator.js +8 -1
- package/dist/core/validation/cel-validator.js.map +1 -1
- package/dist/core/validation/literal-status.d.ts +107 -0
- package/dist/core/validation/literal-status.d.ts.map +1 -0
- package/dist/core/validation/literal-status.js +346 -0
- package/dist/core/validation/literal-status.js.map +1 -0
- package/dist/core/validation/structural-spec-dependence.d.ts +179 -0
- package/dist/core/validation/structural-spec-dependence.d.ts.map +1 -0
- package/dist/core/validation/structural-spec-dependence.js +911 -0
- package/dist/core/validation/structural-spec-dependence.js.map +1 -0
- package/dist/factories/apisix/compositions/apisix-bootstrap.d.ts.map +1 -1
- package/dist/factories/apisix/compositions/apisix-bootstrap.js +9 -0
- package/dist/factories/apisix/compositions/apisix-bootstrap.js.map +1 -1
- package/dist/factories/clickhouse/compositions/clickhouse-cluster.d.ts +6 -0
- package/dist/factories/clickhouse/compositions/clickhouse-cluster.d.ts.map +1 -1
- package/dist/factories/clickhouse/compositions/clickhouse-cluster.js +273 -23
- package/dist/factories/clickhouse/compositions/clickhouse-cluster.js.map +1 -1
- package/dist/factories/clickhouse/resources/index.d.ts +1 -0
- package/dist/factories/clickhouse/resources/index.d.ts.map +1 -1
- package/dist/factories/clickhouse/resources/index.js +1 -0
- package/dist/factories/clickhouse/resources/index.js.map +1 -1
- package/dist/factories/clickhouse/resources/installation.d.ts.map +1 -1
- package/dist/factories/clickhouse/resources/installation.js +31 -4
- package/dist/factories/clickhouse/resources/installation.js.map +1 -1
- package/dist/factories/clickhouse/resources/s3-backup.d.ts +215 -0
- package/dist/factories/clickhouse/resources/s3-backup.d.ts.map +1 -0
- package/dist/factories/clickhouse/resources/s3-backup.js +410 -0
- package/dist/factories/clickhouse/resources/s3-backup.js.map +1 -0
- package/dist/factories/clickhouse/types.d.ts +416 -20
- package/dist/factories/clickhouse/types.d.ts.map +1 -1
- package/dist/factories/clickhouse/types.js +235 -11
- package/dist/factories/clickhouse/types.js.map +1 -1
- package/dist/factories/clickhouse/utils/helm-values-mapper.d.ts +42 -0
- package/dist/factories/clickhouse/utils/helm-values-mapper.d.ts.map +1 -1
- package/dist/factories/clickhouse/utils/helm-values-mapper.js +48 -0
- package/dist/factories/clickhouse/utils/helm-values-mapper.js.map +1 -1
- package/dist/factories/clickhouse/utils/index.d.ts +2 -0
- package/dist/factories/clickhouse/utils/index.d.ts.map +1 -1
- package/dist/factories/clickhouse/utils/index.js +2 -0
- package/dist/factories/clickhouse/utils/index.js.map +1 -1
- package/dist/factories/clickhouse/utils/s3-storage.d.ts +503 -0
- package/dist/factories/clickhouse/utils/s3-storage.d.ts.map +1 -0
- package/dist/factories/clickhouse/utils/s3-storage.js +1139 -0
- package/dist/factories/clickhouse/utils/s3-storage.js.map +1 -0
- package/dist/factories/clickhouse/utils/validation.d.ts +103 -0
- package/dist/factories/clickhouse/utils/validation.d.ts.map +1 -1
- package/dist/factories/clickhouse/utils/validation.js +129 -0
- package/dist/factories/clickhouse/utils/validation.js.map +1 -1
- package/dist/factories/clickhouse/utils/xml.d.ts +87 -0
- package/dist/factories/clickhouse/utils/xml.d.ts.map +1 -0
- package/dist/factories/clickhouse/utils/xml.js +151 -0
- package/dist/factories/clickhouse/utils/xml.js.map +1 -0
- package/dist/factories/clickstack/compositions/clickstack-bootstrap.d.ts +57 -0
- package/dist/factories/clickstack/compositions/clickstack-bootstrap.d.ts.map +1 -1
- package/dist/factories/clickstack/compositions/clickstack-bootstrap.js +266 -7
- package/dist/factories/clickstack/compositions/clickstack-bootstrap.js.map +1 -1
- package/dist/factories/clickstack/types.d.ts +205 -26
- package/dist/factories/clickstack/types.d.ts.map +1 -1
- package/dist/factories/clickstack/types.js +106 -8
- package/dist/factories/clickstack/types.js.map +1 -1
- package/dist/factories/clickstack/utils/collector-config.d.ts +104 -0
- package/dist/factories/clickstack/utils/collector-config.d.ts.map +1 -0
- package/dist/factories/clickstack/utils/collector-config.js +266 -0
- package/dist/factories/clickstack/utils/collector-config.js.map +1 -0
- package/dist/factories/clickstack/utils/helm-values-mapper.d.ts +26 -0
- package/dist/factories/clickstack/utils/helm-values-mapper.d.ts.map +1 -1
- package/dist/factories/clickstack/utils/helm-values-mapper.js +53 -12
- package/dist/factories/clickstack/utils/helm-values-mapper.js.map +1 -1
- package/dist/factories/clickstack/utils/index.d.ts +2 -0
- package/dist/factories/clickstack/utils/index.d.ts.map +1 -1
- package/dist/factories/clickstack/utils/index.js +2 -0
- package/dist/factories/clickstack/utils/index.js.map +1 -1
- package/dist/factories/clickstack/utils/storage.d.ts +449 -0
- package/dist/factories/clickstack/utils/storage.d.ts.map +1 -0
- package/dist/factories/clickstack/utils/storage.js +698 -0
- package/dist/factories/clickstack/utils/storage.js.map +1 -0
- package/dist/factories/cnpg/resources/helm.d.ts.map +1 -1
- package/dist/factories/cnpg/resources/helm.js +4 -4
- package/dist/factories/cnpg/resources/helm.js.map +1 -1
- package/dist/factories/cnpg/types.d.ts +10 -1
- package/dist/factories/cnpg/types.d.ts.map +1 -1
- package/dist/factories/cnpg/types.js.map +1 -1
- package/dist/factories/cnpg/utils/helm-values-mapper.d.ts +19 -2
- package/dist/factories/cnpg/utils/helm-values-mapper.d.ts.map +1 -1
- package/dist/factories/cnpg/utils/helm-values-mapper.js +22 -4
- package/dist/factories/cnpg/utils/helm-values-mapper.js.map +1 -1
- package/dist/factories/envoy-ai-gateway/compositions/gateway.d.ts.map +1 -1
- package/dist/factories/envoy-ai-gateway/compositions/gateway.js +16 -3
- package/dist/factories/envoy-ai-gateway/compositions/gateway.js.map +1 -1
- package/dist/factories/envoy-ai-gateway/constants.d.ts +5 -2
- package/dist/factories/envoy-ai-gateway/constants.d.ts.map +1 -1
- package/dist/factories/envoy-ai-gateway/constants.js +5 -2
- package/dist/factories/envoy-ai-gateway/constants.js.map +1 -1
- package/dist/factories/envoy-ai-gateway/resources/gateway.d.ts +12 -0
- package/dist/factories/envoy-ai-gateway/resources/gateway.d.ts.map +1 -1
- package/dist/factories/envoy-ai-gateway/resources/gateway.js +27 -104
- package/dist/factories/envoy-ai-gateway/resources/gateway.js.map +1 -1
- package/dist/factories/envoy-ai-gateway/types.d.ts +15 -47
- package/dist/factories/envoy-ai-gateway/types.d.ts.map +1 -1
- package/dist/factories/envoy-ai-gateway/types.js.map +1 -1
- package/dist/factories/gateway-api/constants.d.ts +27 -0
- package/dist/factories/gateway-api/constants.d.ts.map +1 -0
- package/dist/factories/gateway-api/constants.js +27 -0
- package/dist/factories/gateway-api/constants.js.map +1 -0
- package/dist/factories/gateway-api/index.d.ts +14 -0
- package/dist/factories/gateway-api/index.d.ts.map +1 -0
- package/dist/factories/gateway-api/index.js +14 -0
- package/dist/factories/gateway-api/index.js.map +1 -0
- package/dist/factories/gateway-api/readiness.d.ts +67 -0
- package/dist/factories/gateway-api/readiness.d.ts.map +1 -0
- package/dist/factories/gateway-api/readiness.js +213 -0
- package/dist/factories/gateway-api/readiness.js.map +1 -0
- package/dist/factories/gateway-api/resources/gateway.d.ts +91 -0
- package/dist/factories/gateway-api/resources/gateway.d.ts.map +1 -0
- package/dist/factories/gateway-api/resources/gateway.js +99 -0
- package/dist/factories/gateway-api/resources/gateway.js.map +1 -0
- package/dist/factories/gateway-api/resources/index.d.ts +2 -0
- package/dist/factories/gateway-api/resources/index.d.ts.map +1 -0
- package/dist/factories/gateway-api/resources/index.js +2 -0
- package/dist/factories/gateway-api/resources/index.js.map +1 -0
- package/dist/factories/gateway-api/types.d.ts +1269 -0
- package/dist/factories/gateway-api/types.d.ts.map +1 -0
- package/dist/factories/gateway-api/types.js +417 -0
- package/dist/factories/gateway-api/types.js.map +1 -0
- package/dist/factories/helm/readiness-evaluators.d.ts +21 -3
- package/dist/factories/helm/readiness-evaluators.d.ts.map +1 -1
- package/dist/factories/helm/readiness-evaluators.js +117 -11
- package/dist/factories/helm/readiness-evaluators.js.map +1 -1
- package/dist/factories/helm/types.d.ts +28 -0
- package/dist/factories/helm/types.d.ts.map +1 -1
- package/dist/factories/index.d.ts +2 -0
- package/dist/factories/index.d.ts.map +1 -1
- package/dist/factories/index.js +8 -0
- package/dist/factories/index.js.map +1 -1
- package/dist/factories/inngest/compositions/inngest-bootstrap.d.ts.map +1 -1
- package/dist/factories/inngest/compositions/inngest-bootstrap.js +10 -0
- package/dist/factories/inngest/compositions/inngest-bootstrap.js.map +1 -1
- package/dist/factories/kubernetes/admission/index.d.ts +2 -0
- package/dist/factories/kubernetes/admission/index.d.ts.map +1 -1
- package/dist/factories/kubernetes/admission/index.js +2 -0
- package/dist/factories/kubernetes/admission/index.js.map +1 -1
- package/dist/factories/kubernetes/admission/label-propagation-guard.d.ts +108 -0
- package/dist/factories/kubernetes/admission/label-propagation-guard.d.ts.map +1 -0
- package/dist/factories/kubernetes/admission/label-propagation-guard.js +247 -0
- package/dist/factories/kubernetes/admission/label-propagation-guard.js.map +1 -0
- package/dist/factories/kubernetes/admission/mutating-admission-policy.d.ts +286 -0
- package/dist/factories/kubernetes/admission/mutating-admission-policy.d.ts.map +1 -0
- package/dist/factories/kubernetes/admission/mutating-admission-policy.js +157 -0
- package/dist/factories/kubernetes/admission/mutating-admission-policy.js.map +1 -0
- package/dist/factories/opensearch/compositions/cluster.d.ts.map +1 -1
- package/dist/factories/opensearch/compositions/cluster.js +6 -1
- package/dist/factories/opensearch/compositions/cluster.js.map +1 -1
- package/dist/factories/ory/compositions/ory-identity-stack.d.ts +8 -8
- package/dist/factories/ory/types.d.ts +16 -16
- package/dist/factories/rook/compositions/object-storage-claim.d.ts +1 -1
- package/dist/factories/rook/compositions/rook-ceph-platform.js +8 -1
- package/dist/factories/rook/compositions/rook-ceph-platform.js.map +1 -1
- package/dist/factories/rook/types.d.ts +1 -1
- package/dist/factories/searxng/compositions/searxng-bootstrap.d.ts +1 -1
- package/dist/factories/searxng/compositions/searxng-bootstrap.d.ts.map +1 -1
- package/dist/factories/searxng/compositions/searxng-bootstrap.js +18 -13
- package/dist/factories/searxng/compositions/searxng-bootstrap.js.map +1 -1
- package/dist/factories/searxng/index.d.ts +3 -0
- package/dist/factories/searxng/index.d.ts.map +1 -1
- package/dist/factories/searxng/index.js +3 -0
- package/dist/factories/searxng/index.js.map +1 -1
- package/dist/factories/searxng/types.js +1 -1
- package/dist/factories/searxng/types.js.map +1 -1
- package/dist/factories/traefik/compositions/index.d.ts +6 -0
- package/dist/factories/traefik/compositions/index.d.ts.map +1 -0
- package/dist/factories/traefik/compositions/index.js +6 -0
- package/dist/factories/traefik/compositions/index.js.map +1 -0
- package/dist/factories/traefik/compositions/traefik-bootstrap.d.ts +185 -0
- package/dist/factories/traefik/compositions/traefik-bootstrap.d.ts.map +1 -0
- package/dist/factories/traefik/compositions/traefik-bootstrap.js +339 -0
- package/dist/factories/traefik/compositions/traefik-bootstrap.js.map +1 -0
- package/dist/factories/traefik/compositions/traefik-helm-repository.d.ts +16 -0
- package/dist/factories/traefik/compositions/traefik-helm-repository.d.ts.map +1 -0
- package/dist/factories/traefik/compositions/traefik-helm-repository.js +31 -0
- package/dist/factories/traefik/compositions/traefik-helm-repository.js.map +1 -0
- package/dist/factories/traefik/constants.d.ts +78 -0
- package/dist/factories/traefik/constants.d.ts.map +1 -0
- package/dist/factories/traefik/constants.js +104 -0
- package/dist/factories/traefik/constants.js.map +1 -0
- package/dist/factories/traefik/index.d.ts +25 -0
- package/dist/factories/traefik/index.d.ts.map +1 -0
- package/dist/factories/traefik/index.js +25 -0
- package/dist/factories/traefik/index.js.map +1 -0
- package/dist/factories/traefik/resources/common.d.ts +68 -0
- package/dist/factories/traefik/resources/common.d.ts.map +1 -0
- package/dist/factories/traefik/resources/common.js +59 -0
- package/dist/factories/traefik/resources/common.js.map +1 -0
- package/dist/factories/traefik/resources/gateway.d.ts +85 -0
- package/dist/factories/traefik/resources/gateway.d.ts.map +1 -0
- package/dist/factories/traefik/resources/gateway.js +109 -0
- package/dist/factories/traefik/resources/gateway.js.map +1 -0
- package/dist/factories/traefik/resources/helm.d.ts +56 -0
- package/dist/factories/traefik/resources/helm.d.ts.map +1 -0
- package/dist/factories/traefik/resources/helm.js +131 -0
- package/dist/factories/traefik/resources/helm.js.map +1 -0
- package/dist/factories/traefik/resources/index.d.ts +10 -0
- package/dist/factories/traefik/resources/index.d.ts.map +1 -0
- package/dist/factories/traefik/resources/index.js +10 -0
- package/dist/factories/traefik/resources/index.js.map +1 -0
- package/dist/factories/traefik/resources/middleware.d.ts +169 -0
- package/dist/factories/traefik/resources/middleware.d.ts.map +1 -0
- package/dist/factories/traefik/resources/middleware.js +225 -0
- package/dist/factories/traefik/resources/middleware.js.map +1 -0
- package/dist/factories/traefik/resources/routing.d.ts +113 -0
- package/dist/factories/traefik/resources/routing.d.ts.map +1 -0
- package/dist/factories/traefik/resources/routing.js +119 -0
- package/dist/factories/traefik/resources/routing.js.map +1 -0
- package/dist/factories/traefik/resources/tls.d.ts +85 -0
- package/dist/factories/traefik/resources/tls.d.ts.map +1 -0
- package/dist/factories/traefik/resources/tls.js +93 -0
- package/dist/factories/traefik/resources/tls.js.map +1 -0
- package/dist/factories/traefik/types.d.ts +2690 -0
- package/dist/factories/traefik/types.d.ts.map +1 -0
- package/dist/factories/traefik/types.js +1084 -0
- package/dist/factories/traefik/types.js.map +1 -0
- package/dist/factories/traefik/utils/helm-values-mapper.d.ts +198 -0
- package/dist/factories/traefik/utils/helm-values-mapper.d.ts.map +1 -0
- package/dist/factories/traefik/utils/helm-values-mapper.js +372 -0
- package/dist/factories/traefik/utils/helm-values-mapper.js.map +1 -0
- package/dist/factories/traefik/utils/index.d.ts +6 -0
- package/dist/factories/traefik/utils/index.d.ts.map +1 -0
- package/dist/factories/traefik/utils/index.js +6 -0
- package/dist/factories/traefik/utils/index.js.map +1 -0
- package/dist/factories/traefik/utils/middleware-validation.d.ts +42 -0
- package/dist/factories/traefik/utils/middleware-validation.d.ts.map +1 -0
- package/dist/factories/traefik/utils/middleware-validation.js +78 -0
- package/dist/factories/traefik/utils/middleware-validation.js.map +1 -0
- package/dist/factories/webapp/compositions/web-app-with-processing.d.ts.map +1 -1
- package/dist/factories/webapp/compositions/web-app-with-processing.js +10 -0
- package/dist/factories/webapp/compositions/web-app-with-processing.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +9 -1
|
@@ -14,36 +14,242 @@ import { canonicalizeCelResourceAliases } from '../../utils/cel-resource-identif
|
|
|
14
14
|
import { isCelExpression, isKubernetesRef } from '../../utils/type-guards.js';
|
|
15
15
|
import { isValuesMergeExpression } from '../aspects/values-merge.js';
|
|
16
16
|
import { remapVariableNames } from '../composition/nested-status-cel.js';
|
|
17
|
+
import { celLineCommentEnd, celStringLiteralEnd, collectCelLambdaScopes, maskClosedCelLiteralsAndComments, } from '../references/cel-lexical-scanner.js';
|
|
18
|
+
import { ConversionError } from '../errors.js';
|
|
19
|
+
import { isStrictCelDiagnosticsEnabled } from '../expressions/analysis/strict-cel.js';
|
|
17
20
|
import { getComponentLogger } from '../logging/index.js';
|
|
18
21
|
import { copyResourceMetadata } from '../metadata/index.js';
|
|
19
22
|
const logger = getComponentLogger('cel-references');
|
|
20
23
|
function escapeRegExpLiteral(value) {
|
|
21
24
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
22
25
|
}
|
|
23
|
-
|
|
26
|
+
/** CEL `IDENT ::= [_a-zA-Z][_a-zA-Z0-9]*`, widened by `$` as the marker charset spells it. */
|
|
27
|
+
const CEL_INDEX_TARGET_START = /[A-Za-z_$]/;
|
|
28
|
+
const CEL_INDEX_TARGET_CHARACTER = /[A-Za-z0-9_$]/;
|
|
29
|
+
/**
|
|
30
|
+
* What may follow a `.<digits>` run without making it part of a longer token.
|
|
31
|
+
*
|
|
32
|
+
* Read against the CEL lexis punctuation set. The class holds every CEL
|
|
33
|
+
* punctuation character that can legally follow a complete index, plus
|
|
34
|
+
* whitespace and end-of-text:
|
|
35
|
+
*
|
|
36
|
+
* - `[` — an index may be taken of an index. `a.0[1]` and `a.0["k"]` are the
|
|
37
|
+
* dotted spelling of `a[0][1]` and `a[0]["k"]`; leaving `[` out meant those
|
|
38
|
+
* were not converted and stayed invalid CEL.
|
|
39
|
+
* - `%` — the modulo operator, alongside the `+ - * /` already here. `a.0 % 2`
|
|
40
|
+
* only converted because of the whitespace; `a.0%2` did not.
|
|
41
|
+
* - `.` `]` `)` `}` — a following select, a closing index, call or aggregate.
|
|
42
|
+
* - `? :` `,` `< > = ! & |` — the conditional, argument lists, and the
|
|
43
|
+
* relational, equality and logical operators.
|
|
44
|
+
*
|
|
45
|
+
* `(` is deliberately NOT here: `a.0(` is not CEL under any reading, because a
|
|
46
|
+
* call target is an `IDENT` and an index is not callable — converting it would
|
|
47
|
+
* invent a program rather than respell one. `;` `@` `#` are not CEL punctuation
|
|
48
|
+
* at all, so text containing them is not an expression this sweep should touch.
|
|
49
|
+
*/
|
|
50
|
+
const CEL_INDEX_RUN_TERMINATOR = /[.[\])}%\s?:,+\-*/<>=!&|]/;
|
|
51
|
+
/**
|
|
52
|
+
* Does the text emitted so far end with something an index can be taken of?
|
|
53
|
+
*
|
|
54
|
+
* That is a whole CEL identifier — `[A-Za-z_$][A-Za-z0-9_$]*` — or a `]` that
|
|
55
|
+
* closed an earlier index. Requiring a whole IDENTIFIER rather than merely an
|
|
56
|
+
* identifier CHARACTER is what separates `v2.0` (the identifier `v2`, indexed)
|
|
57
|
+
* from `1.0` (a NUMBER literal whose fraction must not be touched): both end in
|
|
58
|
+
* a digit, but only the former's digit run is anchored by an identifier start.
|
|
59
|
+
*/
|
|
60
|
+
function endsWithIndexableTarget(emitted) {
|
|
61
|
+
let index = emitted.length - 1;
|
|
62
|
+
if (index < 0)
|
|
63
|
+
return false;
|
|
64
|
+
if (emitted[index] === ']')
|
|
65
|
+
return true;
|
|
66
|
+
while (index >= 0 && CEL_INDEX_TARGET_CHARACTER.test(emitted[index]))
|
|
67
|
+
index -= 1;
|
|
68
|
+
const start = emitted[index + 1];
|
|
69
|
+
return start !== undefined && CEL_INDEX_TARGET_START.test(start);
|
|
70
|
+
}
|
|
71
|
+
/** The `STRING_LIT`/`BYTES_LIT` prefix letters, in either case and either order. */
|
|
72
|
+
const CEL_STRING_PREFIX_LETTER = /[rRbB]/;
|
|
73
|
+
/**
|
|
74
|
+
* End of the CEL string literal that starts at `index`, or `index` for none.
|
|
75
|
+
*
|
|
76
|
+
* Both scans below walk this module's text one character at a time, so each
|
|
77
|
+
* needs the same question answered at each step: *does a `STRING_LIT` or
|
|
78
|
+
* `BYTES_LIT` token begin here, and where does it end?* The answer comes from
|
|
79
|
+
* {@link celStringLiteralEnd}, the branch's spec-complete lexer — `r`/`R`/`b`/`B`
|
|
80
|
+
* prefixes in either order, the two triple-quoted forms, escapes in the non-raw
|
|
81
|
+
* forms only — rather than from a hand-rolled `"…"`/`'…'` loop, which mis-lexed
|
|
82
|
+
* every one of those: `r"a\"` is a COMPLETE raw literal (raw forms have no
|
|
83
|
+
* escape alternative), and `"""…"""` closes only at its matching triple.
|
|
84
|
+
*
|
|
85
|
+
* The STRING-ONLY entry point is deliberate. Its whole-text sibling
|
|
86
|
+
* {@link maskClosedCelLiteralsAndComments} masks comments too, and running THAT
|
|
87
|
+
* over a KRO MIXED TEMPLATE is wrong, because a template's literal part
|
|
88
|
+
* routinely contains `http://…` and a whole-text comment walk would read that
|
|
89
|
+
* `//` as a `COMMENT` and blank the rest of the line — the region markers
|
|
90
|
+
* included. Comment awareness is still owed to the parts of the text that
|
|
91
|
+
* really are CEL; it is supplied there, region-locally, by
|
|
92
|
+
* {@link celRegionCommentEnd}, which the two scans below ask ALONGSIDE this one
|
|
93
|
+
* at each index. Only the string half of the lexis is wanted from this helper.
|
|
94
|
+
*
|
|
95
|
+
* `previous` is the input character before `index`. A prefix letter opens a
|
|
96
|
+
* literal only when it does not merely CONTINUE an identifier, so that `myr"x"`
|
|
97
|
+
* lexes as the identifier `myr` followed by a plain string rather than as a raw
|
|
98
|
+
* one — the same boundary rule the scanner's own source-ordered walk gets from
|
|
99
|
+
* consuming an `IDENT` whole. A quote needs no such guard: `a"x"` opens a
|
|
100
|
+
* literal whatever precedes it.
|
|
101
|
+
*
|
|
102
|
+
* An UNTERMINATED literal is not a token: `index` comes back, and the caller
|
|
103
|
+
* steps over the opening quote as ordinary text. That is what keeps a bare
|
|
104
|
+
* apostrophe in marker-laden template text from swallowing everything after it.
|
|
105
|
+
*/
|
|
106
|
+
function celStringLiteralEndAt(expr, index, previous) {
|
|
107
|
+
const character = expr[index];
|
|
108
|
+
if (character === undefined)
|
|
109
|
+
return index;
|
|
110
|
+
if (character !== '"' && character !== "'") {
|
|
111
|
+
if (!CEL_STRING_PREFIX_LETTER.test(character))
|
|
112
|
+
return index;
|
|
113
|
+
if (previous !== undefined && CEL_INDEX_TARGET_CHARACTER.test(previous))
|
|
114
|
+
return index;
|
|
115
|
+
}
|
|
116
|
+
return celStringLiteralEnd(expr, index);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* End of the CEL `COMMENT ::= '//' ~NEWLINE*` that starts at `index`, or
|
|
120
|
+
* `index` for none — asked ONLY of text that is itself CEL.
|
|
121
|
+
*
|
|
122
|
+
* **Why a comment is a token here.** Inside a `${ … }` region — and in a
|
|
123
|
+
* genuinely bare CEL expression — the text IS CEL, and CEL's lexis has line
|
|
124
|
+
* comments. A `//` there that is not inside a STRING_LIT begins one, and
|
|
125
|
+
* everything up to the newline is commentary: it holds no braces that can close
|
|
126
|
+
* the region and no paths that can be rewritten. Without this, the `{` of
|
|
127
|
+
* `${a.0 // {` + newline + ` + b.1}` was counted as a nested brace, so
|
|
128
|
+
* {@link celTemplateRegionEnd} never found the region's close, returned `-1`,
|
|
129
|
+
* and the caller copied the whole remainder through — leaving the invalid `.0`
|
|
130
|
+
* and `.1` accesses the sweep exists to remove. The rewrite walk had the
|
|
131
|
+
* mirror-image fault, respelling a `.<digits>` that was only ever commentary.
|
|
132
|
+
*
|
|
133
|
+
* **Where this does NOT apply, and why that is not the round-3 `http://`
|
|
134
|
+
* problem.** That evidence was about LITERAL TEMPLATE TEXT — the part of a KRO
|
|
135
|
+
* mixed template outside every `${ … }`, which KRO emits verbatim and which is
|
|
136
|
+
* not CEL at all. In `` `http://${string(a.spec.ports.0.port)}` `` the `//`
|
|
137
|
+
* belongs to a URL scheme, and reading it as a comment would blank the region
|
|
138
|
+
* that follows it. Literal text is still never comment-scanned: this helper is
|
|
139
|
+
* asked only between a region's braces, and in the whole-string branch that is
|
|
140
|
+
* reached only when the text is bare CEL. A URL that appears INSIDE a region
|
|
141
|
+
* lives in a string literal (`${"http://x" + a.0}`), and
|
|
142
|
+
* {@link celStringLiteralEndAt} skips that literal WHOLE before this question is
|
|
143
|
+
* ever reached at the `//`.
|
|
144
|
+
*
|
|
145
|
+
* **Ordering.** Both callers ask this question and the string question at the
|
|
146
|
+
* same index in one source-ordered walk — the rule the scanner's own
|
|
147
|
+
* `celLiteralRegions` encodes: a `//` inside a literal opens no comment, and a
|
|
148
|
+
* quote inside a comment opens no literal, so `${a.0 // "` + newline +
|
|
149
|
+
* ` + b.1}` still rewrites both runs instead of letting the commented-out quote
|
|
150
|
+
* swallow the rest of the region.
|
|
151
|
+
*/
|
|
152
|
+
function celRegionCommentEnd(expr, index) {
|
|
153
|
+
return celLineCommentEnd(expr, index);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Find the `}` that closes the KRO `${ … }` region whose body starts at
|
|
157
|
+
* `start`.
|
|
158
|
+
*
|
|
159
|
+
* Brace-balanced, string-literal aware and COMMENT aware. A CEL region may
|
|
160
|
+
* contain a `{` of its own (a map literal), and may contain a `{` or `}` that
|
|
161
|
+
* does not open or end anything — inside a STRING_LIT, as in `${"}" + a.0}`, or
|
|
162
|
+
* inside a `//` comment ({@link celRegionCommentEnd}). Scanning for the first
|
|
163
|
+
* `}` would cut the region short there and hand the rest of the expression to
|
|
164
|
+
* the literal-text path. Returns the index of the closing brace, or `-1` when the
|
|
165
|
+
* region is never closed (in which case the caller copies the remainder
|
|
166
|
+
* through unchanged rather than guessing where it ended).
|
|
167
|
+
*
|
|
168
|
+
* Every literal is skipped WHOLE, via {@link celStringLiteralEndAt}, and brace
|
|
169
|
+
* counting resumes after it. Deciding string-ness delimiter by delimiter got
|
|
170
|
+
* the whole prefixed and triple-quoted family wrong in both directions: in
|
|
171
|
+
* `${r"\" + a.0}` the `\"` read as an escape, so the region ran past its real
|
|
172
|
+
* `}` and the remainder — `a.0` included — was copied through unnormalised,
|
|
173
|
+
* while in `${"""a"}b""" + c.0}` the quotes paired off two at a time and the
|
|
174
|
+
* region ended at a `}` that is inside the triple-quoted literal.
|
|
175
|
+
*
|
|
176
|
+
* A comment is skipped WHOLE the same way, up to but not including its newline,
|
|
177
|
+
* so neither the `{` of `${a.0 // {` nor the `}` of `${a.0 // }` reaches the
|
|
178
|
+
* depth counter.
|
|
179
|
+
*/
|
|
180
|
+
function celTemplateRegionEnd(expr, start) {
|
|
181
|
+
let depth = 1;
|
|
182
|
+
for (let i = start; i < expr.length; i++) {
|
|
183
|
+
const char = expr[i];
|
|
184
|
+
if (!char)
|
|
185
|
+
continue;
|
|
186
|
+
const commentEnd = celRegionCommentEnd(expr, i);
|
|
187
|
+
if (commentEnd > i) {
|
|
188
|
+
i = commentEnd - 1;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const literalEnd = celStringLiteralEndAt(expr, i, expr[i - 1]);
|
|
192
|
+
if (literalEnd > i) {
|
|
193
|
+
i = literalEnd - 1;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (char === '{') {
|
|
197
|
+
depth += 1;
|
|
198
|
+
}
|
|
199
|
+
else if (char === '}') {
|
|
200
|
+
depth -= 1;
|
|
201
|
+
if (depth === 0)
|
|
202
|
+
return i;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return -1;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Does this text carry a `__KUBERNETES_REF_…__` marker?
|
|
209
|
+
*
|
|
210
|
+
* Built from the shared marker grammar — `KUBERNETES_REF_MARKER_SOURCE` in
|
|
211
|
+
* `shared/brands.ts`, the single source of truth for marker syntax — rather
|
|
212
|
+
* than from a hand-written `__KUBERNETES_REF_` prefix, so that a change to the
|
|
213
|
+
* resource-id or field-path charset reaches this test too. Deliberately NOT
|
|
214
|
+
* global: `test` on a `/g` regex carries `lastIndex` between calls.
|
|
215
|
+
*/
|
|
216
|
+
const CARRIES_KUBERNETES_REF_MARKER = new RegExp(KUBERNETES_REF_MARKER_SOURCE);
|
|
217
|
+
/**
|
|
218
|
+
* Apply the dotted-numeric-run rule to ONE CEL region — a `${ … }` body, or a
|
|
219
|
+
* whole bare CEL expression. See {@link normalizeCelArrayIndexPaths} for the
|
|
220
|
+
* rule itself, and {@link celStringLiteralEndAt} for why the string scan is the
|
|
221
|
+
* STRING-ONLY lexer rather than the shared comment-aware mask.
|
|
222
|
+
*
|
|
223
|
+
* A literal is copied through WHOLE and the walk resumes after it, so quoted
|
|
224
|
+
* data is never rewritten and text outside a literal is never protected by one.
|
|
225
|
+
* The delimiter-by-delimiter scan this replaced got both halves wrong on the
|
|
226
|
+
* prefixed forms: `r"a\".b.0"` is the complete raw literal `r"a\"` followed by
|
|
227
|
+
* `.b.0"`, whose `.0` is an ordinary index, yet the `\"` read as an escape and
|
|
228
|
+
* protected it; `rb'x\' + c.0` lost its `c.0` the same way.
|
|
229
|
+
*
|
|
230
|
+
* A `//` COMMENT is copied through whole for the same reason
|
|
231
|
+
* ({@link celRegionCommentEnd}): its text is commentary, not a path, so the
|
|
232
|
+
* `c.1` of `a.0 // c.1` + newline + `+ b.1` stays as written while the two real
|
|
233
|
+
* accesses either side of it become `a[0]` and `b[1]`. The newline is left to
|
|
234
|
+
* the ordinary walk, which also means a comment can never lend its last
|
|
235
|
+
* identifier as the left context of the next line's run.
|
|
236
|
+
*/
|
|
237
|
+
function rewriteCelIndexPathsInRegion(expr) {
|
|
24
238
|
let result = '';
|
|
25
|
-
let quote = null;
|
|
26
|
-
let escaped = false;
|
|
27
239
|
for (let i = 0; i < expr.length; i++) {
|
|
28
240
|
const char = expr[i];
|
|
29
241
|
if (!char)
|
|
30
242
|
continue;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
else if (char === '\\') {
|
|
37
|
-
escaped = true;
|
|
38
|
-
}
|
|
39
|
-
else if (char === quote) {
|
|
40
|
-
quote = null;
|
|
41
|
-
}
|
|
243
|
+
const commentEnd = celRegionCommentEnd(expr, i);
|
|
244
|
+
if (commentEnd > i) {
|
|
245
|
+
result += expr.slice(i, commentEnd);
|
|
246
|
+
i = commentEnd - 1;
|
|
42
247
|
continue;
|
|
43
248
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
result +=
|
|
249
|
+
const literalEnd = celStringLiteralEndAt(expr, i, expr[i - 1]);
|
|
250
|
+
if (literalEnd > i) {
|
|
251
|
+
result += expr.slice(i, literalEnd);
|
|
252
|
+
i = literalEnd - 1;
|
|
47
253
|
continue;
|
|
48
254
|
}
|
|
49
255
|
if (char === '.') {
|
|
@@ -52,11 +258,10 @@ export function normalizeCelArrayIndexPaths(expr) {
|
|
|
52
258
|
while (digitEnd < expr.length && /\d/.test(expr[digitEnd] ?? '')) {
|
|
53
259
|
digitEnd++;
|
|
54
260
|
}
|
|
55
|
-
const previous = expr[i - 1] ?? '';
|
|
56
261
|
const next = expr[digitEnd] ?? '';
|
|
57
262
|
if (digitEnd > digitStart &&
|
|
58
|
-
|
|
59
|
-
(next === '' ||
|
|
263
|
+
endsWithIndexableTarget(result) &&
|
|
264
|
+
(next === '' || CEL_INDEX_RUN_TERMINATOR.test(next))) {
|
|
60
265
|
result += `[${expr.slice(digitStart, digitEnd)}]`;
|
|
61
266
|
i = digitEnd - 1;
|
|
62
267
|
continue;
|
|
@@ -66,6 +271,136 @@ export function normalizeCelArrayIndexPaths(expr) {
|
|
|
66
271
|
}
|
|
67
272
|
return result;
|
|
68
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* Rewrite every dotted numeric path segment as a CEL index: `items.0` → `items[0]`.
|
|
276
|
+
*
|
|
277
|
+
* CEL has no `.0` field select — a `SELECT` takes an `IDENT`, and an `IDENT` may
|
|
278
|
+
* not begin with a digit — so a dotted numeric segment is a parse error on both
|
|
279
|
+
* evaluation engines and `[0]` is the only spelling of that access. Paths reach
|
|
280
|
+
* this module in the dotted form from several producers (`extractNestedStatusCel`
|
|
281
|
+
* builds an array element's key as `` `${fieldPath}.${index}` ``, and the marker
|
|
282
|
+
* field-path charset admits that form too), so this is the sweep that makes them
|
|
283
|
+
* valid.
|
|
284
|
+
*
|
|
285
|
+
* **The rule.** A `.` followed by one or more digits becomes `[<digits>]` when
|
|
286
|
+
* both contexts allow it:
|
|
287
|
+
*
|
|
288
|
+
* 1. *Left* — the text EMITTED SO FAR ends with a CEL identifier or with a `]`
|
|
289
|
+
* ({@link endsWithIndexableTarget}). Reading the left context off the OUTPUT
|
|
290
|
+
* rather than the input is what lets a run of numeric segments chain: in
|
|
291
|
+
* `a.0.1.b` the first run emits `a[0]`, so the second run sees the `]` this
|
|
292
|
+
* function has just written and the whole path comes out as `a[0][1].b`. Read
|
|
293
|
+
* off the INPUT the second run would see the digit `0`, stop, and leave the
|
|
294
|
+
* equally invalid `a[0].1.b` behind. This is the same semantics
|
|
295
|
+
* {@link splitFieldPathSegments} applies to a field path — a whole-digit
|
|
296
|
+
* segment is an index on the segment before it — now agreed on by both.
|
|
297
|
+
*
|
|
298
|
+
* NUMBER literals are left alone by that same test: in `1.0` and `2.5e3` the
|
|
299
|
+
* digits before the `.` are not anchored by an identifier start, so the
|
|
300
|
+
* fraction survives, and a leading `.5` has no left context at all. Where the
|
|
301
|
+
* digits ARE the tail of an identifier, the index reading is the only one CEL
|
|
302
|
+
* has, because `a1.5` is not a float literal — a NUMBER may not begin with an
|
|
303
|
+
* identifier character — which is why `v2.0` becomes `v2[0]` while `v2.name`,
|
|
304
|
+
* having no digit run to convert, is untouched.
|
|
305
|
+
*
|
|
306
|
+
* `)` is deliberately NOT a left context: a postfix that lands after a
|
|
307
|
+
* parenthesised nested-status expansion is normalised at its source, by
|
|
308
|
+
* {@link splitFieldPathSegments}, where the segment structure is still known.
|
|
309
|
+
*
|
|
310
|
+
* 2. *Right* — the run ends the text, or is followed by a character that can
|
|
311
|
+
* neither continue a number nor start an identifier
|
|
312
|
+
* ({@link CEL_INDEX_RUN_TERMINATOR}). This is what keeps the `5` of `2.5e3`
|
|
313
|
+
* out independently of the left-context test.
|
|
314
|
+
*
|
|
315
|
+
* Quoted data is skipped, and skipped with the WHOLE `STRING_LIT`/`BYTES_LIT`
|
|
316
|
+
* lexis: `r`/`R`/`b`/`B` prefixes in either order, the two triple-quoted forms,
|
|
317
|
+
* and escapes in the non-raw forms only ({@link celStringLiteralEndAt}).
|
|
318
|
+
* `//` COMMENTS are skipped whole too, but only where the text is CEL — see
|
|
319
|
+
* {@link celRegionCommentEnd}, and the boundary in the next paragraph. What is
|
|
320
|
+
* deliberately NOT used is a WHOLE-TEXT mask,
|
|
321
|
+
* {@link maskClosedCelLiteralsAndComments} or `celStringLiteralSpans`: this
|
|
322
|
+
* function runs over KRO MIXED-TEMPLATE text rather than over CEL alone, its
|
|
323
|
+
* inputs include values like `` `http://${string(service.spec.ports.0.port)}` ``,
|
|
324
|
+
* and a whole-text walk would read that URL scheme's `//` — which sits in
|
|
325
|
+
* LITERAL text, where CEL's lexis does not reach — as a `COMMENT` and blank the
|
|
326
|
+
* template that follows, silently dropping this rewrite.
|
|
327
|
+
*
|
|
328
|
+
* **Where the rewrite applies.** The inputs above are KRO MIXED TEMPLATES: a
|
|
329
|
+
* `${ … }` CEL region embedded in LITERAL text that KRO emits verbatim. Only
|
|
330
|
+
* the CEL regions are rewritten; literal text is copied through untouched. A
|
|
331
|
+
* `.<digits>` run after an identifier is a perfectly ordinary thing for literal
|
|
332
|
+
* text to contain — the `v1.2` of a URL path, an `image:tag.1`, a `file.txt.1`,
|
|
333
|
+
* a version string like `alpha.3` — and none of those is a CEL index. Applying
|
|
334
|
+
* the rule to the whole string corrupted them into `v1[2]` and the like,
|
|
335
|
+
* changing text KRO would have emitted as written.
|
|
336
|
+
*
|
|
337
|
+
* That split is also the boundary of CEL's COMMENT rule. A region's body and a
|
|
338
|
+
* bare CEL expression are CEL, so a `//` outside a string literal there begins a
|
|
339
|
+
* comment and is copied through whole; literal template text is not CEL, is
|
|
340
|
+
* never comment-scanned, and keeps its `http://`. The two never collide: a URL
|
|
341
|
+
* that appears inside a region is inside a STRING_LIT, which is skipped whole
|
|
342
|
+
* before the `//` is ever reached.
|
|
343
|
+
*
|
|
344
|
+
* A region's end is found by brace balancing that is STRING-LITERAL AND COMMENT
|
|
345
|
+
* AWARE ({@link celTemplateRegionEnd}), because a CEL region may legitimately
|
|
346
|
+
* contain a `{` or `}` that closes nothing — inside a map literal, inside a
|
|
347
|
+
* STRING_LIT as in `${"}" + a.0}`, or inside a comment as in `${a.0 // {`.
|
|
348
|
+
* Each region is rewritten independently, from an empty left context, so a run
|
|
349
|
+
* never chains across the literal text between two regions.
|
|
350
|
+
*
|
|
351
|
+
* Text with no `${` is rewritten whole only when it is GENUINELY BARE CEL, and
|
|
352
|
+
* that takes a second condition: no `__KUBERNETES_REF_…__` marker either. Bare
|
|
353
|
+
* CEL is the form `getInnerCelPath` and `markerToCelPath` build, and the form
|
|
354
|
+
* the marker and nested-status resolvers hand over. Being CEL, it is read with
|
|
355
|
+
* the comment rule too.
|
|
356
|
+
*
|
|
357
|
+
* MARKER-LADEN TEXT is the other thing that arrives without a `${`: a string
|
|
358
|
+
* derived from a template literal whose interpolations coerced to markers, as
|
|
359
|
+
* in `http://__KUBERNETES_REF___schema___spec.name__:8080/api/v1.2`. That is
|
|
360
|
+
* literal text with references embedded in it, not an expression — its `v1.2`
|
|
361
|
+
* is a URL path segment — yet the whole-string branch rewrote it to `v1[2]`,
|
|
362
|
+
* the very corruption the region split above exists to prevent, arriving by the
|
|
363
|
+
* other door. So text carrying a marker is copied through untouched
|
|
364
|
+
* ({@link CARRIES_KUBERNETES_REF_MARKER}), and nothing is lost by that: a
|
|
365
|
+
* marker's OWN dotted digits are normalised when the marker itself is converted,
|
|
366
|
+
* by {@link markerToCelPath}, which calls this function on the bare
|
|
367
|
+
* `<resourceId>.<fieldPath>` path — which, carrying no marker, still takes the
|
|
368
|
+
* whole-string branch.
|
|
369
|
+
*
|
|
370
|
+
* A text may hold BOTH markers and `${ … }` regions. The region split already
|
|
371
|
+
* gets that right — regions are rewritten, the literal text between them,
|
|
372
|
+
* markers included, is copied — so the marker test guards only the whole-string
|
|
373
|
+
* branch.
|
|
374
|
+
*/
|
|
375
|
+
export function normalizeCelArrayIndexPaths(expr) {
|
|
376
|
+
if (!expr.includes('${')) {
|
|
377
|
+
// Literal template text that merely embeds references — not an expression.
|
|
378
|
+
if (CARRIES_KUBERNETES_REF_MARKER.test(expr))
|
|
379
|
+
return expr;
|
|
380
|
+
// No `${` and no marker — a bare CEL expression, rewritten whole.
|
|
381
|
+
return rewriteCelIndexPathsInRegion(expr);
|
|
382
|
+
}
|
|
383
|
+
let result = '';
|
|
384
|
+
let index = 0;
|
|
385
|
+
while (index < expr.length) {
|
|
386
|
+
const open = expr.indexOf('${', index);
|
|
387
|
+
if (open === -1) {
|
|
388
|
+
result += expr.slice(index);
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
// Literal text before the region: emitted verbatim, never rewritten.
|
|
392
|
+
result += expr.slice(index, open);
|
|
393
|
+
const close = celTemplateRegionEnd(expr, open + 2);
|
|
394
|
+
if (close === -1) {
|
|
395
|
+
// Unterminated region — copy the remainder through untouched.
|
|
396
|
+
result += expr.slice(open);
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
result += `\${${rewriteCelIndexPathsInRegion(expr.slice(open + 2, close))}}`;
|
|
400
|
+
index = close + 1;
|
|
401
|
+
}
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
69
404
|
// ---------------------------------------------------------------------------
|
|
70
405
|
// Primitive helpers
|
|
71
406
|
// ---------------------------------------------------------------------------
|
|
@@ -178,11 +513,16 @@ function generateCelExpression(ref, context) {
|
|
|
178
513
|
const isNestedComp = ref.__nestedComposition === true;
|
|
179
514
|
if (isNestedComp && context?.nestedStatusCel) {
|
|
180
515
|
const fieldName = ref.fieldPath.replace(/^status\./, '');
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (
|
|
185
|
-
|
|
516
|
+
// A ref path can reach past the mapping key (`status.addr.ip` against an
|
|
517
|
+
// `addr` entry); the remainder is re-attached to the finalized expression.
|
|
518
|
+
const resolution = resolveNestedField(ref.resourceId, fieldName, context.nestedStatusCel, context.resourceIds?.has(ref.resourceId) !== true, false);
|
|
519
|
+
if (resolution !== undefined) {
|
|
520
|
+
// Seed the entry we just looked up so a self-referential mapping stops
|
|
521
|
+
// at its concrete resource reference instead of expanding again.
|
|
522
|
+
const finalized = finalizeCelForKro(resolution.entry.expression, context.nestedStatusCel, context, true, resolution.entry.key);
|
|
523
|
+
const withPostfix = applyKroSegmentPostfix(finalized, resolution.postfix);
|
|
524
|
+
if (withPostfix !== undefined)
|
|
525
|
+
return withPostfix;
|
|
186
526
|
}
|
|
187
527
|
}
|
|
188
528
|
const expression = normalizeCelArrayIndexPaths(`${resolveResourceIdAlias(ref.resourceId, context)}.${ref.fieldPath}`);
|
|
@@ -357,17 +697,30 @@ export function isStaticExpression(expr, nestedStatusCel, resourceIds, resolveKn
|
|
|
357
697
|
// Nested composition reference resolution
|
|
358
698
|
// ---------------------------------------------------------------------------
|
|
359
699
|
/**
|
|
360
|
-
* Maximum
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
700
|
+
* Maximum nesting depth when resolving nested composition references in
|
|
701
|
+
* {@link resolveNestedCompositionRefs}. One level is consumed per
|
|
702
|
+
* `(resourceId, field)` entry expanded on the current recursion path, so a
|
|
703
|
+
* three-level composition uses three levels. Cycles are caught by the
|
|
704
|
+
* in-progress set rather than by this limit, which therefore only fires for
|
|
705
|
+
* genuinely deep — acyclic — composition chains.
|
|
365
706
|
*
|
|
366
707
|
* Hitting this limit indicates a real bug in the resolution table — most
|
|
367
|
-
* likely
|
|
368
|
-
* composition shape
|
|
708
|
+
* likely an unexpectedly deep chain introduced by a faulty alias entry — not
|
|
709
|
+
* a legitimate composition shape, so under strict CEL diagnostics it fails
|
|
710
|
+
* the serialization instead of emitting a partially-resolved expression.
|
|
369
711
|
*/
|
|
370
712
|
const NESTED_REF_RESOLUTION_DEPTH_LIMIT = 16;
|
|
713
|
+
/**
|
|
714
|
+
* Pattern matching a `<id>.status.<fieldPath>` nested-composition token.
|
|
715
|
+
*
|
|
716
|
+
* The fieldPath capture is greedy on dots so paths like `components.app` are
|
|
717
|
+
* captured whole — that's the form `nestedStatusCel` keys use after recursive
|
|
718
|
+
* extraction. Always instantiate a fresh `RegExp` from this source: the
|
|
719
|
+
* substitution walker is re-entrant (its replacer recurses into another
|
|
720
|
+
* `String.replace`), so a shared `/g` regex would have its `lastIndex`
|
|
721
|
+
* clobbered mid-scan.
|
|
722
|
+
*/
|
|
723
|
+
const NESTED_STATUS_TOKEN_SOURCE = String.raw `\b([a-zA-Z_$][\w$]*)\.status\.([a-zA-Z_$][\w$.]*)`;
|
|
371
724
|
/**
|
|
372
725
|
* Look up a nested composition's analyzed expression by `(resourceId, fieldName)`.
|
|
373
726
|
*
|
|
@@ -375,7 +728,7 @@ const NESTED_REF_RESOLUTION_DEPTH_LIMIT = 16;
|
|
|
375
728
|
* every code path that needs to find an inner expression from a
|
|
376
729
|
* `nestedStatusCel` table — both the structured-ref paths
|
|
377
730
|
* (`generateCelExpression`, `serializeStatusMappingsToCel`) and the
|
|
378
|
-
* string-resolver path (`
|
|
731
|
+
* string-resolver path (`substituteNestedRefsInText`,
|
|
379
732
|
* `resolveNestedRefMarkers`).
|
|
380
733
|
*
|
|
381
734
|
* Returns `undefined` when no match is found, leaving the caller to
|
|
@@ -402,12 +755,21 @@ const NESTED_REF_RESOLUTION_DEPTH_LIMIT = 16;
|
|
|
402
755
|
* Ambiguous matches (multiple candidates from the prefix or field-name
|
|
403
756
|
* strategies) emit a warning log and return `undefined` so the caller
|
|
404
757
|
* can fall through to its own error handling.
|
|
758
|
+
*
|
|
759
|
+
* Returns the matched entry together with the key it is stored under, which
|
|
760
|
+
* strategies 2-4 can reach through an alias. See {@link NestedStatusEntry}.
|
|
405
761
|
*/
|
|
406
|
-
|
|
762
|
+
function lookupNestedEntry(resourceId, fieldName, nestedStatusCel, allowFieldFallback = true) {
|
|
763
|
+
const entryFor = (key) => {
|
|
764
|
+
if (key === undefined)
|
|
765
|
+
return undefined;
|
|
766
|
+
const expression = nestedStatusCel[key];
|
|
767
|
+
return expression === undefined ? undefined : { key, expression };
|
|
768
|
+
};
|
|
407
769
|
// Strategy 1: exact match.
|
|
408
770
|
const exactKey = `__nestedStatus:${resourceId}:${fieldName}`;
|
|
409
771
|
if (Object.hasOwn(nestedStatusCel, exactKey)) {
|
|
410
|
-
return
|
|
772
|
+
return entryFor(exactKey);
|
|
411
773
|
}
|
|
412
774
|
// Gather all entries for the requested field name once — strategies
|
|
413
775
|
// 2-4 all need this list.
|
|
@@ -427,8 +789,7 @@ export function lookupNestedExpression(resourceId, fieldName, nestedStatusCel, a
|
|
|
427
789
|
const refBase = resourceId.replace(/\d+$/, '');
|
|
428
790
|
const baseNameMatches = fieldMatches.filter((m) => m.baseId.replace(/\d+$/, '') === refBase);
|
|
429
791
|
if (baseNameMatches.length === 1) {
|
|
430
|
-
|
|
431
|
-
return match ? nestedStatusCel[match.key] : undefined;
|
|
792
|
+
return entryFor(baseNameMatches[0]?.key);
|
|
432
793
|
}
|
|
433
794
|
// Strategy 3: unambiguous camelCase / case-insensitive prefix.
|
|
434
795
|
const prefixMatches = fieldMatches.filter((m) => {
|
|
@@ -448,8 +809,7 @@ export function lookupNestedExpression(resourceId, fieldName, nestedStatusCel, a
|
|
|
448
809
|
baseStem.toLowerCase() === resourceId.toLowerCase());
|
|
449
810
|
});
|
|
450
811
|
if (prefixMatches.length === 1) {
|
|
451
|
-
|
|
452
|
-
return match ? nestedStatusCel[match.key] : undefined;
|
|
812
|
+
return entryFor(prefixMatches[0]?.key);
|
|
453
813
|
}
|
|
454
814
|
if (prefixMatches.length > 1) {
|
|
455
815
|
logger.warn('Ambiguous nested composition prefix match', {
|
|
@@ -463,8 +823,7 @@ export function lookupNestedExpression(resourceId, fieldName, nestedStatusCel, a
|
|
|
463
823
|
if (!allowFieldFallback)
|
|
464
824
|
return undefined;
|
|
465
825
|
if (fieldMatches.length === 1) {
|
|
466
|
-
|
|
467
|
-
return match ? nestedStatusCel[match.key] : undefined;
|
|
826
|
+
return entryFor(fieldMatches[0]?.key);
|
|
468
827
|
}
|
|
469
828
|
// Field-only fallback is intentionally best-effort and fully silent on
|
|
470
829
|
// ambiguity. Prefix/base-name ambiguity still logs above, but this final
|
|
@@ -472,6 +831,244 @@ export function lookupNestedExpression(resourceId, fieldName, nestedStatusCel, a
|
|
|
472
831
|
// like `ready` that appear across many unrelated nested compositions.
|
|
473
832
|
return undefined;
|
|
474
833
|
}
|
|
834
|
+
/**
|
|
835
|
+
* Look up a nested composition's analyzed expression by `(resourceId, fieldName)`.
|
|
836
|
+
*
|
|
837
|
+
* Thin projection of {@link lookupNestedEntry} for callers that only need the
|
|
838
|
+
* expression text. Anything resolving a DOTTED path wants
|
|
839
|
+
* {@link resolveNestedField} instead.
|
|
840
|
+
*/
|
|
841
|
+
export function lookupNestedExpression(resourceId, fieldName, nestedStatusCel, allowFieldFallback = true) {
|
|
842
|
+
return lookupNestedEntry(resourceId, fieldName, nestedStatusCel, allowFieldFallback)?.expression;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Resolve the longest dotted prefix of `fieldPath` that names a mapping entry
|
|
846
|
+
* for `id`, returning the unmatched remainder as a postfix.
|
|
847
|
+
*
|
|
848
|
+
* `nestedStatusCel` keys are field PATHS — `ready`, but also `components.app`
|
|
849
|
+
* — so the token capture is greedy on dots and routinely reaches past the key
|
|
850
|
+
* it should match: `svc.status.addr.ip` when the key is `addr`,
|
|
851
|
+
* `svc.status.items.size()` when the key is `items`,
|
|
852
|
+
* `svc.status.phase.startsWith("x")` when the key is `phase`. Looking the whole
|
|
853
|
+
* captured path up either finds nothing (leaving a virtual id in the emitted
|
|
854
|
+
* RGD) or lets the field-name fallback match an unrelated entry. Resolving the
|
|
855
|
+
* longest prefix instead, and re-attaching the remainder to the parenthesized
|
|
856
|
+
* substitution, preserves the postfix operation: `(inner).ip`, `(inner).size()`,
|
|
857
|
+
* `(inner).startsWith("x")`.
|
|
858
|
+
*
|
|
859
|
+
* A path segment can also carry an INDEX chain — `items[0].name`,
|
|
860
|
+
* `ports["http"].port`, `a.b[0][1].c` — so the path is lexed by
|
|
861
|
+
* {@link splitFieldPathSegments} rather than split on `.`. Keys are matched on
|
|
862
|
+
* the bare names; the index chain of the last matched segment plus every
|
|
863
|
+
* remaining segment rides along verbatim as the postfix, giving `(inner)[0].name`
|
|
864
|
+
* and `(inner)["http"].port`.
|
|
865
|
+
*
|
|
866
|
+
* A list index reaches this function in EITHER of two spellings — bracketed
|
|
867
|
+
* (`items[0].name`, what a proxy field path carries) or DOTTED
|
|
868
|
+
* (`items.0.name`, what `extractNestedStatusCel` builds its keys from). The
|
|
869
|
+
* lexer folds the dotted form into the bracketed one, and
|
|
870
|
+
* {@link keyPrefixCandidates} spells a numeric index back out as `.0` when it
|
|
871
|
+
* builds a key path, so the two spellings resolve identically: the emitted CEL
|
|
872
|
+
* always uses `[0]` (`.0` is not a field select and parses on neither engine)
|
|
873
|
+
* and key matching always uses `.0` (the spelling keys are stored under).
|
|
874
|
+
*
|
|
875
|
+
* Exact `__nestedStatus:<id>:<prefix>` keys are checked at every prefix length
|
|
876
|
+
* BEFORE the alias ladder runs at any length, so a real key (`addr`) always
|
|
877
|
+
* beats a fuzzy match on a longer path (`addr.ip`).
|
|
878
|
+
*
|
|
879
|
+
* `trailingSegmentIsMethodName` reports that the captured path is immediately
|
|
880
|
+
* followed by `(` in the source text, which makes its last segment a method
|
|
881
|
+
* name rather than a field — never a candidate key. It is ignored for a
|
|
882
|
+
* single-segment path, which has no shorter prefix to fall back to and so keeps
|
|
883
|
+
* its existing behaviour.
|
|
884
|
+
*/
|
|
885
|
+
function resolveNestedField(id, fieldPath, nestedStatusCel, allowFieldFallback, trailingSegmentIsMethodName) {
|
|
886
|
+
const candidates = keyPrefixCandidates(splitFieldPathSegments(fieldPath), trailingSegmentIsMethodName);
|
|
887
|
+
// Pass 1: exact keys only, longest prefix first.
|
|
888
|
+
for (const candidate of candidates) {
|
|
889
|
+
const key = `__nestedStatus:${id}:${candidate.keyPath}`;
|
|
890
|
+
const expression = nestedStatusCel[key];
|
|
891
|
+
if (expression !== undefined) {
|
|
892
|
+
return { entry: { key, expression }, postfix: candidate.postfix };
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
// Pass 2: the full alias ladder, longest prefix first.
|
|
896
|
+
for (const candidate of candidates) {
|
|
897
|
+
const entry = lookupNestedEntry(id, candidate.keyPath, nestedStatusCel, allowFieldFallback);
|
|
898
|
+
if (entry !== undefined)
|
|
899
|
+
return { entry, postfix: candidate.postfix };
|
|
900
|
+
}
|
|
901
|
+
return undefined;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Every prefix of `segments` that could name a `__nestedStatus:<id>:<path>`
|
|
905
|
+
* key, longest first.
|
|
906
|
+
*
|
|
907
|
+
* A prefix may end at a name (`items`, postfix `[0].name`) or after any leading
|
|
908
|
+
* run of its NUMERIC indexes (`items.0`, postfix `.name`) — a numeric index is
|
|
909
|
+
* spellable in a key, because {@link extractNestedStatusCel} builds an array
|
|
910
|
+
* element's key as `` `${fieldPath}.${index}` ``. A NON-numeric index
|
|
911
|
+
* (`["http"]`) has no key spelling, so it ends key candidacy: neither it nor
|
|
912
|
+
* anything past it can be part of a key, and matching a longer prefix across it
|
|
913
|
+
* would silently drop the index from the emitted expression.
|
|
914
|
+
*/
|
|
915
|
+
function keyPrefixCandidates(segments, trailingSegmentIsMethodName) {
|
|
916
|
+
const nameLimit = trailingSegmentIsMethodName && segments.length > 1 ? segments.length - 1 : segments.length;
|
|
917
|
+
/** The path past `count` segments plus `depth` of the last one's indexes. */
|
|
918
|
+
const postfixFrom = (count, depth) => (segments[count - 1]?.indexes.slice(depth).join('') ?? '') +
|
|
919
|
+
segments
|
|
920
|
+
.slice(count)
|
|
921
|
+
.map((segment) => `.${segment.name}${segment.indexes.join('')}`)
|
|
922
|
+
.join('');
|
|
923
|
+
// Built shortest-first so each key path extends the previous one, then
|
|
924
|
+
// reversed — the caller wants the longest prefix tried first.
|
|
925
|
+
const candidates = [];
|
|
926
|
+
let keyPath = '';
|
|
927
|
+
for (let count = 1; count <= nameLimit; count += 1) {
|
|
928
|
+
const segment = segments[count - 1];
|
|
929
|
+
if (segment === undefined)
|
|
930
|
+
break;
|
|
931
|
+
keyPath = count === 1 ? segment.name : `${keyPath}.${segment.name}`;
|
|
932
|
+
candidates.push({ keyPath, postfix: postfixFrom(count, 0) });
|
|
933
|
+
let depth = 0;
|
|
934
|
+
for (const index of segment.indexes) {
|
|
935
|
+
const digits = NUMERIC_INDEX_PATTERN.exec(index)?.[1];
|
|
936
|
+
if (digits === undefined)
|
|
937
|
+
break;
|
|
938
|
+
depth += 1;
|
|
939
|
+
keyPath = `${keyPath}.${digits}`;
|
|
940
|
+
candidates.push({ keyPath, postfix: postfixFrom(count, depth) });
|
|
941
|
+
}
|
|
942
|
+
// A non-numeric index was reached: no longer prefix can name a key.
|
|
943
|
+
if (depth < segment.indexes.length)
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
return candidates.reverse();
|
|
947
|
+
}
|
|
948
|
+
/** A whole path segment that is nothing but digits — a DOTTED list index. */
|
|
949
|
+
const DOTTED_NUMERIC_SEGMENT_PATTERN = /^\d+$/;
|
|
950
|
+
/** An index operation that is a plain integer, capturing its digits. */
|
|
951
|
+
const NUMERIC_INDEX_PATTERN = /^\[(\d+)\]$/;
|
|
952
|
+
/**
|
|
953
|
+
* Lex a status field path into `name` + index-chain segments.
|
|
954
|
+
*
|
|
955
|
+
* `split('.')` is wrong for any path carrying an index: it yields `items[0]`,
|
|
956
|
+
* which never equals the mapping key `items`, so the longest-prefix search in
|
|
957
|
+
* {@link resolveNestedField} finds nothing and a virtual id survives into the
|
|
958
|
+
* emitted RGD. Both non-regex callers can hand over such a path — a marker
|
|
959
|
+
* field path admits `[\d+]` (`KUBERNETES_REF_MARKER_FIELD_PATH_SOURCE`, e.g.
|
|
960
|
+
* `status.ports[0].port`) and a `KubernetesRef.fieldPath` reaches this function
|
|
961
|
+
* unmodified — so the shape has to be lexed rather than assumed away. The regex
|
|
962
|
+
* caller cannot: its capture stops at `[`, which leaves the index chain outside
|
|
963
|
+
* the match and therefore untouched in the surrounding text.
|
|
964
|
+
*
|
|
965
|
+
* **An all-digit segment is an INDEX on the segment before it**, not a name:
|
|
966
|
+
* `items.0.name` ≡ `items[0].name`. Both spellings are live here — a proxy
|
|
967
|
+
* renders a numeric key as `[0]` (`schema-proxy.ts`) but
|
|
968
|
+
* `extractNestedStatusCel` builds an array element's mapping key as
|
|
969
|
+
* `` `${fieldPath}.${index}` `` — and the marker charset admits the dotted form
|
|
970
|
+
* too. Folding it into the previous segment's index chain is what makes the two
|
|
971
|
+
* spellings one thing: without it `.0` is lexed as a NAME, so the emitted
|
|
972
|
+
* postfix is `.0.name` (`(inner).0.name` is not CEL — `.0` is not a field
|
|
973
|
+
* select — and parses on neither engine), and a key stored under one spelling
|
|
974
|
+
* cannot be reached from the other. Only a WHOLE segment of digits folds:
|
|
975
|
+
* `v2`, `ip4` and `_0` are identifiers and are left alone.
|
|
976
|
+
*
|
|
977
|
+
* A LEADING numeric segment has no predecessor to index, so it stays a name: a
|
|
978
|
+
* field path is rooted at a field, an index there is not a spelling of anything
|
|
979
|
+
* valid, and leaving it verbatim resolves it exactly as before rather than
|
|
980
|
+
* inventing a meaning for it. (The regex token path cannot produce one — its
|
|
981
|
+
* capture must start with `[a-zA-Z_$]`.)
|
|
982
|
+
*
|
|
983
|
+
* Brackets are matched with a depth counter and quotes are honoured, so neither
|
|
984
|
+
* a `.` nor a `]` inside a map key is read as structure. A path this lexer
|
|
985
|
+
* cannot account for — an unbalanced bracket, an empty segment — is handed back
|
|
986
|
+
* as ONE unsplit segment, so such a path resolves exactly as it did before
|
|
987
|
+
* (whole-path key lookup, no prefix search) instead of being guessed at.
|
|
988
|
+
*/
|
|
989
|
+
function splitFieldPathSegments(fieldPath) {
|
|
990
|
+
const unsplit = [{ name: fieldPath, indexes: [] }];
|
|
991
|
+
const segments = [];
|
|
992
|
+
let index = 0;
|
|
993
|
+
for (;;) {
|
|
994
|
+
const nameStart = index;
|
|
995
|
+
while (index < fieldPath.length && fieldPath[index] !== '.' && fieldPath[index] !== '[') {
|
|
996
|
+
index += 1;
|
|
997
|
+
}
|
|
998
|
+
const name = fieldPath.slice(nameStart, index);
|
|
999
|
+
if (name === '')
|
|
1000
|
+
return unsplit;
|
|
1001
|
+
const indexes = [];
|
|
1002
|
+
while (fieldPath[index] === '[') {
|
|
1003
|
+
const close = closingIndexBracket(fieldPath, index);
|
|
1004
|
+
if (close === undefined)
|
|
1005
|
+
return unsplit;
|
|
1006
|
+
indexes.push(fieldPath.slice(index, close + 1));
|
|
1007
|
+
index = close + 1;
|
|
1008
|
+
}
|
|
1009
|
+
const previous = segments[segments.length - 1];
|
|
1010
|
+
if (previous !== undefined && DOTTED_NUMERIC_SEGMENT_PATTERN.test(name)) {
|
|
1011
|
+
previous.indexes.push(`[${name}]`, ...indexes);
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
segments.push({ name, indexes });
|
|
1015
|
+
}
|
|
1016
|
+
if (index === fieldPath.length)
|
|
1017
|
+
return segments;
|
|
1018
|
+
if (fieldPath[index] !== '.')
|
|
1019
|
+
return unsplit;
|
|
1020
|
+
index += 1;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
/** Offset of the `]` closing the `[` at `open`, or `undefined` when unbalanced. */
|
|
1024
|
+
function closingIndexBracket(text, open) {
|
|
1025
|
+
let depth = 0;
|
|
1026
|
+
let quote;
|
|
1027
|
+
let escaped = false;
|
|
1028
|
+
for (let index = open; index < text.length; index += 1) {
|
|
1029
|
+
const character = text[index];
|
|
1030
|
+
if (quote !== undefined) {
|
|
1031
|
+
if (escaped)
|
|
1032
|
+
escaped = false;
|
|
1033
|
+
else if (character === '\\')
|
|
1034
|
+
escaped = true;
|
|
1035
|
+
else if (character === quote)
|
|
1036
|
+
quote = undefined;
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
if (character === '"' || character === "'") {
|
|
1040
|
+
quote = character;
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
if (character === '[') {
|
|
1044
|
+
depth += 1;
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
if (character === ']') {
|
|
1048
|
+
depth -= 1;
|
|
1049
|
+
if (depth === 0)
|
|
1050
|
+
return index;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return undefined;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Re-attach a {@link NestedFieldResolution} postfix to an already-finalized KRO
|
|
1057
|
+
* segment.
|
|
1058
|
+
*
|
|
1059
|
+
* Only a segment that is exactly one `${—}` expression can carry a postfix. A
|
|
1060
|
+
* mixed template (literal text interleaved with several `${—}` segments) has no
|
|
1061
|
+
* single expression to qualify, so the caller keeps the original reference and
|
|
1062
|
+
* lets downstream validation flag it, exactly as for an unresolvable one.
|
|
1063
|
+
*/
|
|
1064
|
+
function applyKroSegmentPostfix(segment, postfix) {
|
|
1065
|
+
if (postfix === '')
|
|
1066
|
+
return segment;
|
|
1067
|
+
const body = /^\$\{([\s\S]*)\}$/.exec(segment)?.[1];
|
|
1068
|
+
if (body === undefined || body === '' || body.includes('${'))
|
|
1069
|
+
return undefined;
|
|
1070
|
+
return `\${(${body})${postfix}}`;
|
|
1071
|
+
}
|
|
475
1072
|
/**
|
|
476
1073
|
* Transitively substitute nested-composition references inline from a
|
|
477
1074
|
* lookup table.
|
|
@@ -489,67 +1086,261 @@ export function lookupNestedExpression(resourceId, fieldName, nestedStatusCel, a
|
|
|
489
1086
|
* {@link containsNoNonSchemaRefs} (or just call
|
|
490
1087
|
* {@link isStaticExpression} which composes both steps).
|
|
491
1088
|
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
1089
|
+
* **Resolution is recursive, never re-scanning.** Each `<id>.status.<field>`
|
|
1090
|
+
* token found in the ORIGINAL text is substituted exactly once; the inner
|
|
1091
|
+
* expression it was replaced with is then walked on its own, so substituted
|
|
1092
|
+
* output is never handed back to the scanner. The result is therefore linear
|
|
1093
|
+
* in the total size of the reachable mapping instead of doubling on every
|
|
1094
|
+
* pass (see #200 — repeated whole-string passes produced a 6 MB expression
|
|
1095
|
+
* from a two-level fixture).
|
|
1096
|
+
*
|
|
1097
|
+
* **Cycles are terminal, not fatal.** A token whose mapping is already being
|
|
1098
|
+
* expanded further up the current path — directly self-referential, or via a
|
|
1099
|
+
* cycle — keeps its concrete `<id>.status.<field>` reference. For a flattened
|
|
1100
|
+
* child id that is also a real graph resource (the common case: an inner
|
|
1101
|
+
* composition's `phase` mapping reads `<flattenedId>.status.phase`), that
|
|
1102
|
+
* concrete reference IS the correct answer.
|
|
496
1103
|
*
|
|
497
|
-
* **
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
1104
|
+
* **Known resource ids are substituted only inside an expanded nested
|
|
1105
|
+
* boundary.** `resolveKnownNestedResourceRefs` governs the ORIGINAL text
|
|
1106
|
+
* only. Inside an inner expression that has already been proven to come from
|
|
1107
|
+
* a nested composition, exact mappings for concrete resource ids are
|
|
1108
|
+
* authoritative, so substitution there is always allowed (strict lookup —
|
|
1109
|
+
* no field-name fallback).
|
|
1110
|
+
*
|
|
1111
|
+
* Nesting deeper than {@link NESTED_REF_RESOLUTION_DEPTH_LIMIT} levels stops
|
|
1112
|
+
* expanding and is reported by {@link reportNestedRefDepthExceeded}.
|
|
1113
|
+
*
|
|
1114
|
+
* **Lambda variables are skipped, within their scope.** When the resolved
|
|
1115
|
+
* `<id>` is a CEL macro lambda variable like the `c` in
|
|
1116
|
+
* `.exists(c, c.status == "Ready")`, the substitution does NOT fire — the
|
|
1117
|
+
* variable refers to the macro's iteration element, not a nested composition.
|
|
1118
|
+
* The shield is LEXICAL: it covers references inside the macro's body and
|
|
1119
|
+
* nothing else, so the second `svc` in
|
|
1120
|
+
* `list.map(svc, svc.status.x) && svc.status.phase` is still a nested id.
|
|
1121
|
+
* A variable whose scope encloses the point an inner expression was inlined at
|
|
1122
|
+
* stays in scope throughout that inner expression.
|
|
1123
|
+
*
|
|
1124
|
+
* `seedKey` marks a mapping as already being expanded by the caller, given as
|
|
1125
|
+
* the entry's CANONICAL `nestedStatusCel` key. Entry points that look an entry
|
|
1126
|
+
* up themselves and then resolve its text ({@link generateCelExpression},
|
|
1127
|
+
* {@link resolveNestedRefMarkers}, `serializeStatusMappingsToCel`) pass it so a
|
|
1128
|
+
* self-referential mapping is recognized as terminal there too — the resolver
|
|
1129
|
+
* behaves identically no matter which entry point reached it.
|
|
501
1130
|
*/
|
|
502
|
-
function resolveNestedCompositionRefs(expr, nestedStatusCel, resourceIds, resolveKnownNestedResourceRefs = true) {
|
|
1131
|
+
function resolveNestedCompositionRefs(expr, nestedStatusCel, resourceIds, resolveKnownNestedResourceRefs = true, seedKey) {
|
|
1132
|
+
return runNestedRefResolution(expr, nestedStatusCel, resourceIds, resolveKnownNestedResourceRefs, seedKey).text;
|
|
1133
|
+
}
|
|
1134
|
+
/** One resolution pass plus the bookkeeping it accumulated. */
|
|
1135
|
+
function runNestedRefResolution(expr, nestedStatusCel, resourceIds, resolveKnownNestedResourceRefs, seedKey) {
|
|
503
1136
|
if (!nestedStatusCel || Object.keys(nestedStatusCel).length === 0) {
|
|
504
|
-
return expr;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
1137
|
+
return { text: expr, stats: { cycleHits: 0, depthExceeded: false, textPasses: 0, memoHits: 0 } };
|
|
1138
|
+
}
|
|
1139
|
+
const state = {
|
|
1140
|
+
nestedStatusCel,
|
|
1141
|
+
resourceIds,
|
|
1142
|
+
inProgress: new Set(seedKey === undefined ? [] : [seedKey]),
|
|
1143
|
+
memo: new Map(),
|
|
1144
|
+
cycleHits: 0,
|
|
1145
|
+
depthExceeded: false,
|
|
1146
|
+
textPasses: 0,
|
|
1147
|
+
memoHits: 0,
|
|
1148
|
+
};
|
|
1149
|
+
const text = substituteNestedRefsInText(expr, state, resolveKnownNestedResourceRefs, EMPTY_LAMBDA_VARS);
|
|
1150
|
+
if (state.depthExceeded) {
|
|
1151
|
+
reportNestedRefDepthExceeded(expr);
|
|
1152
|
+
}
|
|
1153
|
+
return {
|
|
1154
|
+
text,
|
|
1155
|
+
stats: {
|
|
1156
|
+
cycleHits: state.cycleHits,
|
|
1157
|
+
depthExceeded: state.depthExceeded,
|
|
1158
|
+
textPasses: state.textPasses,
|
|
1159
|
+
memoHits: state.memoHits,
|
|
1160
|
+
},
|
|
1161
|
+
};
|
|
524
1162
|
}
|
|
525
1163
|
export function inlineNestedStatusRefs(expr, nestedStatusCel, resourceIds) {
|
|
526
1164
|
return resolveNestedCompositionRefs(expr, nestedStatusCel, resourceIds);
|
|
527
1165
|
}
|
|
528
1166
|
/**
|
|
529
|
-
*
|
|
1167
|
+
* {@link inlineNestedStatusRefs} plus the resolver's internal counters.
|
|
1168
|
+
*
|
|
1169
|
+
* @internal Test-only. See {@link NestedRefResolutionStats}.
|
|
1170
|
+
*/
|
|
1171
|
+
export function inlineNestedStatusRefsWithStats(expr, nestedStatusCel, resourceIds) {
|
|
1172
|
+
return runNestedRefResolution(expr, nestedStatusCel, resourceIds, true, undefined);
|
|
1173
|
+
}
|
|
1174
|
+
/** Ambient lambda-variable scope for a top-level resolution. */
|
|
1175
|
+
const EMPTY_LAMBDA_VARS = new Set();
|
|
1176
|
+
/**
|
|
1177
|
+
* Kro's implicit element variable for a `forEach` collection's `readyWhen`
|
|
1178
|
+
* body. Unlike a CEL macro variable it has no binder in the expression text —
|
|
1179
|
+
* Kro supplies it to the whole body (`yaml.ts` emits `each` as the base id for
|
|
1180
|
+
* a `forEach` resource) — so it has no lexical scope to be inside of and is
|
|
1181
|
+
* shielded everywhere. `cel-validator.ts` reserves it the same way.
|
|
1182
|
+
*/
|
|
1183
|
+
const KRO_FOR_EACH_ELEMENT_VAR = 'each';
|
|
1184
|
+
/**
|
|
1185
|
+
* The macro-bound identifiers that shield a reference AT `offset`.
|
|
1186
|
+
*
|
|
1187
|
+
* A lambda variable is bound by ONE macro and means something only inside that
|
|
1188
|
+
* macro's body: in `list.map(svc, svc.status.x) && svc.status.phase` the first
|
|
1189
|
+
* `svc` is the iteration element and the second is a real nested-composition
|
|
1190
|
+
* id. Shielding every occurrence in the text — which is what collecting the
|
|
1191
|
+
* names alone does — leaves that second reference unexpanded and a virtual id
|
|
1192
|
+
* in the emitted RGD. `ambientLambdaVars` are the scopes that enclosed the
|
|
1193
|
+
* point where THIS text was inlined, and enclose all of it.
|
|
1194
|
+
*
|
|
1195
|
+
* The macro list is `all`/`exists`/`exists_one`/`map`/`filter`, each binding
|
|
1196
|
+
* exactly one variable as its first argument — the standard CEL macro set, and
|
|
1197
|
+
* the whole of what cel-js 0.8.2 implements for direct mode. cel-go's
|
|
1198
|
+
* two-variable comprehensions are an opt-in extension neither engine has here,
|
|
1199
|
+
* so there is no second variable to bind.
|
|
1200
|
+
*/
|
|
1201
|
+
function lambdaVarsAt(offset, ambientLambdaVars, scopes) {
|
|
1202
|
+
const enclosing = scopes.filter((scope) => offset >= scope.bodyStart && offset < scope.bodyEnd);
|
|
1203
|
+
if (enclosing.length === 0)
|
|
1204
|
+
return ambientLambdaVars;
|
|
1205
|
+
const vars = new Set(ambientLambdaVars);
|
|
1206
|
+
for (const scope of enclosing)
|
|
1207
|
+
vars.add(scope.variable);
|
|
1208
|
+
return vars;
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Substitute every nested-composition token in `text` exactly once. See
|
|
530
1212
|
* {@link resolveNestedCompositionRefs} for the full contract.
|
|
1213
|
+
*
|
|
1214
|
+
* `ambientLambdaVars` carries the macro-bound identifiers that were in scope AT
|
|
1215
|
+
* THE POINT this text was inlined, so a lambda variable stays shielded no
|
|
1216
|
+
* matter how deep the expression it appears in was inlined from. Scopes opened
|
|
1217
|
+
* by this text itself are per-match, not per-text: see {@link lambdaVarsAt}.
|
|
531
1218
|
*/
|
|
532
|
-
function
|
|
533
|
-
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
1219
|
+
function substituteNestedRefsInText(text, state, allowKnownResourceSubstitution, ambientLambdaVars) {
|
|
1220
|
+
state.textPasses += 1;
|
|
1221
|
+
// Scan a copy with every closed CEL string literal AND every `//` line
|
|
1222
|
+
// comment blanked out, so a `<id>.status.<field>`-shaped run of characters
|
|
1223
|
+
// that is really quoted DATA — a log message, a URL, an error string — or
|
|
1224
|
+
// commented-out expression text is never rewritten. Both are masked in one
|
|
1225
|
+
// source-ordered pass, so a `//` inside a string stays string and a quote
|
|
1226
|
+
// inside a comment stays comment. The mask preserves length and offsets
|
|
1227
|
+
// exactly (newlines included), so each replacement splices back into the
|
|
1228
|
+
// ORIGINAL text at the offsets the match reported.
|
|
1229
|
+
const scanned = maskClosedCelLiteralsAndComments(text);
|
|
1230
|
+
// Scopes are read off the MASKED copy — a `.map(` inside quoted data or a
|
|
1231
|
+
// comment binds nothing — and their offsets therefore index `text` too.
|
|
1232
|
+
const lambdaScopes = collectCelLambdaScopes(scanned);
|
|
1233
|
+
const pattern = new RegExp(NESTED_STATUS_TOKEN_SOURCE, 'g');
|
|
1234
|
+
let result = '';
|
|
1235
|
+
let copiedUpTo = 0;
|
|
1236
|
+
let match = pattern.exec(scanned);
|
|
1237
|
+
while (match !== null) {
|
|
1238
|
+
const token = match[0];
|
|
1239
|
+
const tokenEnd = match.index + token.length;
|
|
1240
|
+
const replacement = substituteNestedRefToken(token, match[1] ?? '', match[2] ?? '',
|
|
1241
|
+
// A captured path butted straight against `(` ends in a method name.
|
|
1242
|
+
scanned[tokenEnd] === '(', state, allowKnownResourceSubstitution, lambdaVarsAt(match.index, ambientLambdaVars, lambdaScopes));
|
|
1243
|
+
if (replacement !== token) {
|
|
1244
|
+
result += text.slice(copiedUpTo, match.index) + replacement;
|
|
1245
|
+
copiedUpTo = tokenEnd;
|
|
548
1246
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
1247
|
+
match = pattern.exec(scanned);
|
|
1248
|
+
}
|
|
1249
|
+
return copiedUpTo === 0 ? text : result + text.slice(copiedUpTo);
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Resolve one `<id>.status.<fieldPath>` token to its replacement text, or back
|
|
1253
|
+
* to `token` itself when it must be left alone.
|
|
1254
|
+
*/
|
|
1255
|
+
function substituteNestedRefToken(token, id, fieldPath, trailingSegmentIsMethodName, state, allowKnownResourceSubstitution,
|
|
1256
|
+
/** Macro-bound identifiers in scope at THIS token — see {@link lambdaVarsAt}. */
|
|
1257
|
+
lambdaVars) {
|
|
1258
|
+
if (id === 'schema')
|
|
1259
|
+
return token;
|
|
1260
|
+
if (id === KRO_FOR_EACH_ELEMENT_VAR)
|
|
1261
|
+
return token;
|
|
1262
|
+
if (lambdaVars.has(id))
|
|
1263
|
+
return token;
|
|
1264
|
+
const isKnownResource = state.resourceIds?.has(id) === true;
|
|
1265
|
+
if (isKnownResource && !allowKnownResourceSubstitution)
|
|
1266
|
+
return token;
|
|
1267
|
+
// A concrete graph resource only ever matches an exact nested mapping —
|
|
1268
|
+
// the field-name fallback would let an unrelated composition's field
|
|
1269
|
+
// hijack a real resource reference.
|
|
1270
|
+
const resolution = resolveNestedField(id, fieldPath, state.nestedStatusCel, !isKnownResource, trailingSegmentIsMethodName);
|
|
1271
|
+
if (resolution === undefined)
|
|
1272
|
+
return token;
|
|
1273
|
+
const resolvedInner = expandNestedEntry(resolution.entry, state, lambdaVars);
|
|
1274
|
+
if (resolvedInner === undefined)
|
|
1275
|
+
return token;
|
|
1276
|
+
// Parenthesize to preserve operator precedence in compound expressions, then
|
|
1277
|
+
// re-attach whatever the token reached past the mapping key so a postfix
|
|
1278
|
+
// field access or method call survives the substitution.
|
|
1279
|
+
return `(${resolvedInner})${resolution.postfix}`;
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Resolve one `(id, field)` mapping to its fully-substituted text.
|
|
1283
|
+
*
|
|
1284
|
+
* Returns `undefined` when the mapping must NOT be expanded — because it is
|
|
1285
|
+
* already being expanded on the current path (a cycle, so the concrete
|
|
1286
|
+
* reference is the answer) or because the depth guard tripped. Callers keep
|
|
1287
|
+
* the original `<id>.status.<field>` token in that case.
|
|
1288
|
+
*/
|
|
1289
|
+
function expandNestedEntry(entry, state, ambientLambdaVars) {
|
|
1290
|
+
// Both sets key off the entry's CANONICAL mapping key. Keying off the token's
|
|
1291
|
+
// own `(id, field)` would let a cycle that turns a corner through an alias
|
|
1292
|
+
// spelling expand the same mapping a second time instead of terminating here,
|
|
1293
|
+
// and would miss the memo for every spelling but the first.
|
|
1294
|
+
const entryKey = entry.key;
|
|
1295
|
+
if (state.inProgress.has(entryKey)) {
|
|
1296
|
+
state.cycleHits++;
|
|
1297
|
+
return undefined;
|
|
1298
|
+
}
|
|
1299
|
+
const memoKey = `${[...ambientLambdaVars].sort().join(',')}\n${entryKey}`;
|
|
1300
|
+
const memoized = state.memo.get(memoKey);
|
|
1301
|
+
if (memoized !== undefined) {
|
|
1302
|
+
state.memoHits++;
|
|
1303
|
+
return memoized;
|
|
1304
|
+
}
|
|
1305
|
+
if (state.inProgress.size >= NESTED_REF_RESOLUTION_DEPTH_LIMIT) {
|
|
1306
|
+
state.depthExceeded = true;
|
|
1307
|
+
return undefined;
|
|
1308
|
+
}
|
|
1309
|
+
const cycleHitsBefore = state.cycleHits;
|
|
1310
|
+
state.inProgress.add(entryKey);
|
|
1311
|
+
let resolved;
|
|
1312
|
+
try {
|
|
1313
|
+
resolved = substituteNestedRefsInText(entry.expression, state, true, ambientLambdaVars);
|
|
1314
|
+
}
|
|
1315
|
+
finally {
|
|
1316
|
+
state.inProgress.delete(entryKey);
|
|
1317
|
+
}
|
|
1318
|
+
// Only cache expansions that did not depend on the in-progress path: a
|
|
1319
|
+
// cycle-truncated result is specific to the path that produced it, so
|
|
1320
|
+
// reusing it elsewhere would under-resolve the expression.
|
|
1321
|
+
if (state.cycleHits === cycleHitsBefore) {
|
|
1322
|
+
state.memo.set(memoKey, resolved);
|
|
1323
|
+
}
|
|
1324
|
+
return resolved;
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Report an expansion stopped by {@link NESTED_REF_RESOLUTION_DEPTH_LIMIT}.
|
|
1328
|
+
*
|
|
1329
|
+
* Under strict CEL diagnostics this fails serialization: the emitted
|
|
1330
|
+
* expression still carries unresolved virtual ids that KRO would reject on
|
|
1331
|
+
* the cluster. By default it stays a warning, matching the lenient posture
|
|
1332
|
+
* the rest of the CEL emission layer takes for unprovable expressions.
|
|
1333
|
+
*/
|
|
1334
|
+
function reportNestedRefDepthExceeded(expr) {
|
|
1335
|
+
if (isStrictCelDiagnosticsEnabled()) {
|
|
1336
|
+
throw new ConversionError(`Nested composition resolution exceeded ${NESTED_REF_RESOLUTION_DEPTH_LIMIT} levels of nesting (strict CEL diagnostics)`, expr, 'unknown', undefined, undefined, [
|
|
1337
|
+
'Check the nested composition status mappings for an unexpectedly deep chain or a faulty alias entry',
|
|
1338
|
+
'Disable strict CEL diagnostics for this factory (strictCelDiagnostics: false) to emit the partially-resolved expression instead',
|
|
1339
|
+
]);
|
|
1340
|
+
}
|
|
1341
|
+
logger.warn('Nested composition resolution depth limit exceeded', {
|
|
1342
|
+
depthLimit: NESTED_REF_RESOLUTION_DEPTH_LIMIT,
|
|
1343
|
+
expressionPreview: expr.slice(0, 200),
|
|
553
1344
|
});
|
|
554
1345
|
}
|
|
555
1346
|
/**
|
|
@@ -575,16 +1366,17 @@ function resolveNestedRefMarkers(str, nestedStatusCel, resourceIds, context) {
|
|
|
575
1366
|
return match;
|
|
576
1367
|
// Strip leading "status." since nestedStatusCel keys use the bare field path.
|
|
577
1368
|
const fieldPath = path.replace(/^status\./, '');
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
1369
|
+
// A marker field path is a dot-separated identifier sequence, so it can
|
|
1370
|
+
// reach past the mapping key (`status.addr.ip` against an `addr` entry) but
|
|
1371
|
+
// never carries a method call — `(` is outside the marker charset.
|
|
1372
|
+
const resolution = resolveNestedField(id, fieldPath, nestedStatusCel, resourceIds?.has(id) !== true, false);
|
|
1373
|
+
if (resolution === undefined)
|
|
582
1374
|
return match;
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
return match;
|
|
1375
|
+
// Seed the entry being expanded so the shared resolver treats a
|
|
1376
|
+
// self-referential mapping as terminal here exactly as it does on the
|
|
1377
|
+
// structured-ref path.
|
|
1378
|
+
const segment = innerExprToYamlSegment(resolution.entry.expression, nestedStatusCel, context, resolution.entry.key);
|
|
1379
|
+
return applyKroSegmentPostfix(segment, resolution.postfix) ?? match;
|
|
588
1380
|
});
|
|
589
1381
|
}
|
|
590
1382
|
/**
|
|
@@ -614,10 +1406,10 @@ const WRAPPED_BARE_LITERAL_PATTERN = /^\$\{\s*(-?\d+(?:\.\d+)?|true|false|null)\
|
|
|
614
1406
|
* `replicas: stack.status.someCount` should preserve the natural numeric
|
|
615
1407
|
* type because the destination field expects a number.
|
|
616
1408
|
*/
|
|
617
|
-
function innerExprToYamlSegment(innerExpr, nestedStatusCel, context) {
|
|
1409
|
+
function innerExprToYamlSegment(innerExpr, nestedStatusCel, context, seedKey) {
|
|
618
1410
|
// Recursively resolve any further nested refs the inner expression itself
|
|
619
1411
|
// contains (multi-level nesting).
|
|
620
|
-
const resolved = resolveNestedCompositionRefs(innerExpr, nestedStatusCel, context?.resourceIds, true);
|
|
1412
|
+
const resolved = resolveNestedCompositionRefs(innerExpr, nestedStatusCel, context?.resourceIds, true, seedKey);
|
|
621
1413
|
if (resolved.includes('__KUBERNETES_REF_')) {
|
|
622
1414
|
// Marker-laden — convert to mixed-template form.
|
|
623
1415
|
return convertKubernetesRefMarkersTocel(resolved, context);
|
|
@@ -657,8 +1449,8 @@ function innerExprToYamlSegment(innerExpr, nestedStatusCel, context) {
|
|
|
657
1449
|
* - Plain-CEL input (raw CEL with no markers or `${…}`) → wrapped in
|
|
658
1450
|
* `${…}` so KRO recognizes it as an expression to evaluate.
|
|
659
1451
|
*/
|
|
660
|
-
export function finalizeCelForKro(expr, nestedStatusCel, context, resolveKnownNestedResourceRefs = true) {
|
|
661
|
-
const resolved = normalizeCelArrayIndexPaths(resolveNestedCompositionRefs(expr, nestedStatusCel, context?.resourceIds, resolveKnownNestedResourceRefs));
|
|
1452
|
+
export function finalizeCelForKro(expr, nestedStatusCel, context, resolveKnownNestedResourceRefs = true, seedKey) {
|
|
1453
|
+
const resolved = normalizeCelArrayIndexPaths(resolveNestedCompositionRefs(expr, nestedStatusCel, context?.resourceIds, resolveKnownNestedResourceRefs, seedKey));
|
|
662
1454
|
if (resolved.includes('__KUBERNETES_REF_')) {
|
|
663
1455
|
return convertKubernetesRefMarkersTocel(resolved, context);
|
|
664
1456
|
}
|
|
@@ -1424,8 +2216,8 @@ export function serializeStatusMappingsToCel(statusMappings, nestedStatusCel, re
|
|
|
1424
2216
|
* Centralized here so the two branches don't drift on what "produce
|
|
1425
2217
|
* KRO status CEL from a resolved expression" means.
|
|
1426
2218
|
*/
|
|
1427
|
-
function statusFieldFromExpression(expr, rewriteSchemaRefs = true, resolveKnownNestedResourceRefs = [...nestedCompositionIds].some((id) => new RegExp(`(^|[^\\w$])${escapeRegExpLiteral(id)}\\s*\\.`).test(expr))) {
|
|
1428
|
-
const resolved = normalizeCelArrayIndexPaths(resolveNestedCompositionRefs(normalizeLocalResourceExpr(expr), normalizedNestedStatusCel, resourceIds, resolveKnownNestedResourceRefs));
|
|
2219
|
+
function statusFieldFromExpression(expr, rewriteSchemaRefs = true, resolveKnownNestedResourceRefs = [...nestedCompositionIds].some((id) => new RegExp(`(^|[^\\w$])${escapeRegExpLiteral(id)}\\s*\\.`).test(expr)), seedKey) {
|
|
2220
|
+
const resolved = normalizeCelArrayIndexPaths(resolveNestedCompositionRefs(normalizeLocalResourceExpr(expr), normalizedNestedStatusCel, resourceIds, resolveKnownNestedResourceRefs, seedKey));
|
|
1429
2221
|
if (resolved.includes('__KUBERNETES_REF_')) {
|
|
1430
2222
|
// Marker-laden — use mixed-template form.
|
|
1431
2223
|
const converted = convertKubernetesRefMarkersTocel(resolved);
|
|
@@ -1447,9 +2239,16 @@ export function serializeStatusMappingsToCel(statusMappings, nestedStatusCel, re
|
|
|
1447
2239
|
// it for KRO status emission.
|
|
1448
2240
|
if (ref.__nestedComposition && normalizedNestedStatusCel) {
|
|
1449
2241
|
const fieldName = ref.fieldPath.replace(/^status\./, '');
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
2242
|
+
// A ref path can reach past the mapping key (`status.addr.ip` against
|
|
2243
|
+
// an `addr` entry); the remainder rides along as a postfix.
|
|
2244
|
+
const resolution = resolveNestedField(ref.resourceId, fieldName, normalizedNestedStatusCel, true, false);
|
|
2245
|
+
if (resolution !== undefined) {
|
|
2246
|
+
// Seed the entry we just looked up: its mapping may reference its
|
|
2247
|
+
// own flattened resource id, which is terminal, not re-expandable.
|
|
2248
|
+
const statusField = statusFieldFromExpression(resolution.entry.expression, true, true, resolution.entry.key);
|
|
2249
|
+
const withPostfix = applyKroSegmentPostfix(statusField, resolution.postfix);
|
|
2250
|
+
if (withPostfix !== undefined)
|
|
2251
|
+
return withPostfix;
|
|
1453
2252
|
}
|
|
1454
2253
|
}
|
|
1455
2254
|
// Unresolved ref — still run through the status-expression finalizer so
|