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
|
@@ -0,0 +1,1139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3-backed ClickHouse storage compilation.
|
|
3
|
+
*
|
|
4
|
+
* Compiles the typed `storage` discriminated union into the three things the
|
|
5
|
+
* Altinity operator needs to make a ClickHouse cluster keep its data in object
|
|
6
|
+
* storage with only a local cache on the node:
|
|
7
|
+
*
|
|
8
|
+
* 1. a `storage_configuration` XML document dropped into the CHI's
|
|
9
|
+
* `configuration.files` under `config.d/storage.xml` (disks + policies),
|
|
10
|
+
* 2. the `merge_tree/storage_policy` default in `configuration.settings`, so
|
|
11
|
+
* tables created by OTHER tooling (HyperDX/OTel goose migrations, SigNoz)
|
|
12
|
+
* land on object storage with NO per-table DDL, and
|
|
13
|
+
* 3. the pod-template patch that gives the server its credentials — an
|
|
14
|
+
* IRSA-annotated ServiceAccount plus `use_environment_credentials`, or
|
|
15
|
+
* Secret-backed env vars referenced from the XML with `from_env`.
|
|
16
|
+
*
|
|
17
|
+
* ## Two disk types, two DIFFERENT durability stories
|
|
18
|
+
*
|
|
19
|
+
* The distinction is the whole point of the discriminated `diskType` — a
|
|
20
|
+
* boolean `s3: true` would hide it:
|
|
21
|
+
*
|
|
22
|
+
* - `s3` (classic): part METADATA lives on the local disk; only the part data
|
|
23
|
+
* goes to the bucket. The bucket is NOT self-describing — losing the node
|
|
24
|
+
* loses the map to the objects. Durability comes from backups, which is why
|
|
25
|
+
* `backup` exists.
|
|
26
|
+
* - `s3_plain_rewritable`: metadata lives in the bucket, so the bucket IS
|
|
27
|
+
* self-describing and node loss is a restart + re-attach. The cost is hard
|
|
28
|
+
* engine restrictions (see below).
|
|
29
|
+
*
|
|
30
|
+
* ## `s3_plain_rewritable` restrictions (verified against the ClickHouse docs)
|
|
31
|
+
*
|
|
32
|
+
* `s3_plain_rewritable` arrived in 24.4, and from **24.5** any object-storage
|
|
33
|
+
* disk can be configured with the `plain_rewritable` metadata type — the
|
|
34
|
+
* explicit `type: object_storage` + `object_storage_type: s3` +
|
|
35
|
+
* `metadata_type: plain_rewritable` form this module emits. It executes merges
|
|
36
|
+
* and supports `INSERT`, but the ClickHouse documentation is explicit that
|
|
37
|
+
* **mutations and table replication are NOT supported**. So:
|
|
38
|
+
* - single replica only (the factory rejects `replicas > 1`),
|
|
39
|
+
* - no `ALTER ... UPDATE/DELETE`, no lightweight deletes,
|
|
40
|
+
* - `ALTER TABLE ... MODIFY TTL` must be issued with
|
|
41
|
+
* `materialize_ttl_after_modify = 0` (the materialization step is a
|
|
42
|
+
* mutation); TTL-driven part expiry itself runs in merges and works.
|
|
43
|
+
*
|
|
44
|
+
* @see https://clickhouse.com/docs/operations/storing-data
|
|
45
|
+
*/
|
|
46
|
+
import { type } from 'arktype';
|
|
47
|
+
import { containsKubernetesRefs } from '../../../utils/type-guards.js';
|
|
48
|
+
import { assertClickHouseIdentifier } from './validation.js';
|
|
49
|
+
import { xmlAttr, xmlText } from './xml.js';
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Defaults and well-known names
|
|
52
|
+
// ============================================================================
|
|
53
|
+
/** Default MergeTree storage policy name created for S3-backed clusters. */
|
|
54
|
+
export const DEFAULT_S3_POLICY_NAME = 's3_main';
|
|
55
|
+
/** Disk name of the raw object-storage disk inside `storage_configuration`. */
|
|
56
|
+
export const S3_DISK_NAME = 's3';
|
|
57
|
+
/** Disk name of the local read-through cache wrapping {@link S3_DISK_NAME}. */
|
|
58
|
+
export const S3_CACHE_DISK_NAME = 's3_cache';
|
|
59
|
+
/** Volume name inside the generated storage policy. */
|
|
60
|
+
export const S3_POLICY_VOLUME_NAME = 'main';
|
|
61
|
+
/**
|
|
62
|
+
* Default filesystem path of the local cache. It sits UNDER the operator's
|
|
63
|
+
* data volume mount (`/var/lib/clickhouse`), so the thin local PVC declared by
|
|
64
|
+
* `storage.size` backs the cache without an extra volume.
|
|
65
|
+
*/
|
|
66
|
+
export const DEFAULT_S3_CACHE_PATH = '/var/lib/clickhouse/disks/s3_cache/';
|
|
67
|
+
/** CHI `configuration.files` key for the rendered storage configuration. */
|
|
68
|
+
export const CHI_STORAGE_CONFIG_FILE = 'config.d/storage.xml';
|
|
69
|
+
/** CHI `configuration.settings` key that makes the S3 policy the default. */
|
|
70
|
+
export const MERGE_TREE_STORAGE_POLICY_SETTING = 'merge_tree/storage_policy';
|
|
71
|
+
/** Env var name the rendered XML reads the access key id from. */
|
|
72
|
+
export const S3_ACCESS_KEY_ID_ENV = 'CLICKHOUSE_S3_ACCESS_KEY_ID';
|
|
73
|
+
/** Env var name the rendered XML reads the secret access key from. */
|
|
74
|
+
export const S3_SECRET_ACCESS_KEY_ENV = 'CLICKHOUSE_S3_SECRET_ACCESS_KEY';
|
|
75
|
+
/** Default Secret key holding the access key id. */
|
|
76
|
+
export const DEFAULT_ACCESS_KEY_ID_SECRET_KEY = 'AWS_ACCESS_KEY_ID';
|
|
77
|
+
/** Default Secret key holding the secret access key. */
|
|
78
|
+
export const DEFAULT_SECRET_ACCESS_KEY_SECRET_KEY = 'AWS_SECRET_ACCESS_KEY';
|
|
79
|
+
/** IRSA annotation the EKS pod identity webhook keys off. */
|
|
80
|
+
export const IRSA_ROLE_ARN_ANNOTATION = 'eks.amazonaws.com/role-arn';
|
|
81
|
+
/** Default object-key prefix for scheduled backups. */
|
|
82
|
+
export const DEFAULT_BACKUP_PREFIX = 'backups';
|
|
83
|
+
/** Default database backed up by the generated backup CronJob. */
|
|
84
|
+
export const DEFAULT_BACKUP_DATABASE = 'default';
|
|
85
|
+
/** `<s3>` credential section name used by `BACKUP ... TO S3(...)`. */
|
|
86
|
+
export const S3_BACKUP_CONFIG_SECTION = 'backup';
|
|
87
|
+
/**
|
|
88
|
+
* Characters a `storage.endpoint` — and the fully COMPOSED disk/backup URL —
|
|
89
|
+
* may use.
|
|
90
|
+
*
|
|
91
|
+
* The unreserved + reserved sets of RFC 3986 MINUS the characters that are an
|
|
92
|
+
* injection somewhere downstream rather than a malformed URL — `&`, `'`, `"`,
|
|
93
|
+
* `<`, `>` and a backslash. Whitespace and control characters are excluded by
|
|
94
|
+
* construction, since the pattern is an allow-list; so is non-ASCII, which
|
|
95
|
+
* means an internationalized host must be passed in its punycode form.
|
|
96
|
+
*
|
|
97
|
+
* Applied to the COMPOSED string, not only to the caller's `endpoint`: the
|
|
98
|
+
* bucket, region and prefix are appended AFTER the endpoint is checked, so an
|
|
99
|
+
* allow-list that only saw the endpoint would leave three other doors into the
|
|
100
|
+
* same `S3('<url>')` string literal. See {@link composeS3EndpointUrl}.
|
|
101
|
+
*/
|
|
102
|
+
const ENDPOINT_URL_CHARACTERS = /^[A-Za-z0-9._~:/?#@!$()*+,;=%[\]-]+$/;
|
|
103
|
+
/**
|
|
104
|
+
* Shape of an S3 bucket name, per AWS's general-purpose bucket naming rules.
|
|
105
|
+
*
|
|
106
|
+
* 3-63 characters of lowercase letters, digits, dots and hyphens, starting and
|
|
107
|
+
* ending alphanumeric. Two further AWS rules are not expressible in one
|
|
108
|
+
* readable pattern and are checked separately in {@link assertS3BucketName}:
|
|
109
|
+
* no `..`, and not IP-address-shaped.
|
|
110
|
+
*
|
|
111
|
+
* The length bound lives in the pattern (`{1,61}` between the two anchored
|
|
112
|
+
* alphanumerics) so the pattern alone is the whole character rule.
|
|
113
|
+
*
|
|
114
|
+
* @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html
|
|
115
|
+
*/
|
|
116
|
+
export const S3_BUCKET_NAME_PATTERN = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
|
|
117
|
+
/** Bucket names AWS rejects as ambiguous with an IP address. */
|
|
118
|
+
const IP_SHAPED_BUCKET_NAME = /^[0-9]{1,3}(?:\.[0-9]{1,3}){3}$/;
|
|
119
|
+
/**
|
|
120
|
+
* Bucket-name PREFIXES AWS reserves, with the reason each is reserved.
|
|
121
|
+
*
|
|
122
|
+
* These are the published general-purpose-bucket rules, not TypeKro policy —
|
|
123
|
+
* a name carrying one of these is rejected by S3's own CreateBucket, so
|
|
124
|
+
* accepting it here would only move the failure to the first write. `sthree-`
|
|
125
|
+
* subsumes the separately documented `sthree-configurator`.
|
|
126
|
+
*
|
|
127
|
+
* @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html
|
|
128
|
+
*/
|
|
129
|
+
const RESERVED_BUCKET_NAME_PREFIXES = [
|
|
130
|
+
{ prefix: 'xn--', why: 'AWS reserves it for punycode-encoded names' },
|
|
131
|
+
{ prefix: 'sthree-', why: 'AWS reserves it (including sthree-configurator)' },
|
|
132
|
+
{ prefix: 'amzn-s3-demo-', why: 'AWS reserves it for documentation examples' },
|
|
133
|
+
];
|
|
134
|
+
/** Bucket-name SUFFIXES AWS reserves, with the reason each is reserved. */
|
|
135
|
+
const RESERVED_BUCKET_NAME_SUFFIXES = [
|
|
136
|
+
{ suffix: '-s3alias', why: 'AWS reserves it for S3 access-point aliases' },
|
|
137
|
+
{ suffix: '--ol-s3', why: 'AWS reserves it for Object Lambda access points' },
|
|
138
|
+
{ suffix: '.mrap', why: 'AWS reserves it for multi-region access points' },
|
|
139
|
+
{ suffix: '--x-s3', why: 'AWS reserves it for S3 Express directory buckets' },
|
|
140
|
+
];
|
|
141
|
+
/**
|
|
142
|
+
* Region shape accepted for `storage.region`.
|
|
143
|
+
*
|
|
144
|
+
* Two lowercase letters (the geography — `us`, `eu`, `ap`, `cn`), one or more
|
|
145
|
+
* lowercase words, then a single digit: `us-east-2`, `eu-central-1`,
|
|
146
|
+
* `ap-southeast-3`, and the three-part partitions `us-gov-west-1` /
|
|
147
|
+
* `cn-north-1`. Deliberately a SHAPE rather than an enumeration of today's
|
|
148
|
+
* regions — a new region must not need a TypeKro release — but tight enough
|
|
149
|
+
* that the value cannot carry a quote, a dot or a slash into the composed URL,
|
|
150
|
+
* where it sits inside the host name.
|
|
151
|
+
*/
|
|
152
|
+
export const AWS_REGION_PATTERN = /^[a-z]{2}(-[a-z]+)+-\d$/;
|
|
153
|
+
/**
|
|
154
|
+
* Characters one segment of an object-key prefix may use.
|
|
155
|
+
*
|
|
156
|
+
* The RFC 3986 `pchar` set minus `&`, the quotes, the percent (a prefix is
|
|
157
|
+
* written into the URL verbatim, so a `%` there would be a half-finished
|
|
158
|
+
* escape) and the slash, which is the segment SEPARATOR and is handled by
|
|
159
|
+
* splitting rather than by the character class.
|
|
160
|
+
*/
|
|
161
|
+
const KEY_PREFIX_SEGMENT_CHARACTERS = /^[A-Za-z0-9._~!$()*+,;=:@-]+$/;
|
|
162
|
+
/**
|
|
163
|
+
* Minimum ClickHouse version this factory accepts for
|
|
164
|
+
* `diskType: 's3_plain_rewritable'`.
|
|
165
|
+
*
|
|
166
|
+
* 24.4 introduced the `s3_plain_rewritable` disk; 24.5 generalized it to the
|
|
167
|
+
* `metadata_type: plain_rewritable` form emitted here, so 24.5 is the floor.
|
|
168
|
+
*/
|
|
169
|
+
export const MIN_S3_PLAIN_REWRITABLE_VERSION = '24.5';
|
|
170
|
+
/**
|
|
171
|
+
* The {@link MIN_S3_PLAIN_REWRITABLE_VERSION} floor, expressed as a PATTERN so
|
|
172
|
+
* it can travel into a generated KRO schema.
|
|
173
|
+
*
|
|
174
|
+
* WHY A PATTERN AND NOT ONLY A BUILD-TIME COMPARISON. `storage.diskType` is a
|
|
175
|
+
* build-time choice, but `version` is per-INSTANCE runtime spec: in kro mode it
|
|
176
|
+
* arrives at construction as a schema reference, so no build-time comparison
|
|
177
|
+
* can see the value an instance will actually carry. Encoding the floor as a
|
|
178
|
+
* pattern lets `makeClickHouseCluster` put it on `spec.version` in the
|
|
179
|
+
* generated RGD (`string | pattern="…"`), so the API server / KRO REJECT an
|
|
180
|
+
* instance that selects an unsupported server — the case the build-time check
|
|
181
|
+
* structurally cannot reach.
|
|
182
|
+
*
|
|
183
|
+
* Reads as "major.minor >= 24.5": `24.5`-`24.9` and `24.10`+, any `25`-`99`
|
|
184
|
+
* major, and any three-or-more-digit major. A trailing `.patch.build`, or a
|
|
185
|
+
* `-`/`_` suffix, is accepted after the minor because ClickHouse tags look
|
|
186
|
+
* like `25.7`, `25.12.5`, and `24.8.14.39`.
|
|
187
|
+
*
|
|
188
|
+
* RE2-compatible on purpose (no lookaround, no backreferences): the same
|
|
189
|
+
* source becomes the `pattern=` marker of the generated KRO schema, and
|
|
190
|
+
* Kubernetes validates OpenAPI patterns with RE2.
|
|
191
|
+
*/
|
|
192
|
+
export const S3_PLAIN_REWRITABLE_VERSION_PATTERN = /^v?(?:24\.(?:[5-9]|[1-9][0-9]+)|2[5-9]\.[0-9]+|[3-9][0-9]\.[0-9]+|[1-9][0-9]{2,}\.[0-9]+)(?:[._-][0-9A-Za-z._-]*)?$/;
|
|
193
|
+
/**
|
|
194
|
+
* ArkType schema for a `version` that may back
|
|
195
|
+
* `diskType: 's3_plain_rewritable'`.
|
|
196
|
+
*
|
|
197
|
+
* `makeClickHouseCluster` uses this as the spec-schema type of `version`
|
|
198
|
+
* whenever the build-time topology selects `s3_plain_rewritable`, so the floor
|
|
199
|
+
* travels into the generated RGD and KRO rejects a bad INSTANCE. Compositions
|
|
200
|
+
* that wire `clickHouseInstallation()` by hand should do the same — see
|
|
201
|
+
* {@link assertS3PlainRewritableVersion} for why a build-time check alone is
|
|
202
|
+
* not enough there.
|
|
203
|
+
*/
|
|
204
|
+
export const ClickHouseS3PlainRewritableVersionSchema = type(S3_PLAIN_REWRITABLE_VERSION_PATTERN);
|
|
205
|
+
/** Keys that only ever make sense on the `s3` storage mode. */
|
|
206
|
+
const S3_ONLY_STORAGE_KEYS = [
|
|
207
|
+
'bucket',
|
|
208
|
+
'prefix',
|
|
209
|
+
'region',
|
|
210
|
+
'endpoint',
|
|
211
|
+
'diskType',
|
|
212
|
+
'cache',
|
|
213
|
+
'policyName',
|
|
214
|
+
'auth',
|
|
215
|
+
'backup',
|
|
216
|
+
];
|
|
217
|
+
// ============================================================================
|
|
218
|
+
// Resolved shapes
|
|
219
|
+
//
|
|
220
|
+
// SCHEMA-FIRST, exactly like the user-facing shapes in `../types.ts`: the
|
|
221
|
+
// RESULT of the resolution is an ArkType schema and its TypeScript type is
|
|
222
|
+
// INFERRED from it (`typeof X.infer`), so there is one description of a
|
|
223
|
+
// resolved storage and it is one a runtime check can actually enforce.
|
|
224
|
+
//
|
|
225
|
+
// WHY THE RESULT SHAPE NEEDS A SCHEMA AT ALL. `resolveClickHouseStorage()` is
|
|
226
|
+
// not the only way a resolved value reaches the renderers: the module exports
|
|
227
|
+
// `renderStorageConfigurationXml()`, `clickHouseS3ContainerEnv()` and
|
|
228
|
+
// `clickHouseS3BackupCronJob()`, each of which takes a resolved storage as a
|
|
229
|
+
// plain argument. A hand-written interface describes that argument to the
|
|
230
|
+
// COMPILER only; downstream code that field-selects `bucket`, `diskType`,
|
|
231
|
+
// `auth.kind` or `backup.endpointUrl` out of it needs a description that
|
|
232
|
+
// exists at RUN time too — otherwise the structure is claimed (by a cast or a
|
|
233
|
+
// type predicate) rather than established.
|
|
234
|
+
// ============================================================================
|
|
235
|
+
/**
|
|
236
|
+
* What `storage` has to be, quoted into the ArkType error on a bad value.
|
|
237
|
+
*
|
|
238
|
+
* It is attached to the `mode` DISCRIMINANT of
|
|
239
|
+
* {@link ResolvedClickHouseS3StorageSchema} (via ArkType's `.describe()`), so a
|
|
240
|
+
* value that never came out of the S3 branch of the resolver — a PVC
|
|
241
|
+
* resolution, an unrelated object — is reported against `storage.mode` with
|
|
242
|
+
* this sentence, while every other field keeps its own precise, path-named
|
|
243
|
+
* error.
|
|
244
|
+
*/
|
|
245
|
+
export const RESOLVED_S3_STORAGE_REQUIREMENT = "a resolved S3 storage (mode: 's3'), as returned by resolveClickHouseStorage()";
|
|
246
|
+
/**
|
|
247
|
+
* Credentials as resolved for rendering (never carries key material).
|
|
248
|
+
*
|
|
249
|
+
* A real ArkType union rather than a `kind` field plus optional siblings: the
|
|
250
|
+
* two transports have DISJOINT payloads, and the union is what makes
|
|
251
|
+
* `{ kind: 'irsa', secretName: … }` a validation error instead of a value the
|
|
252
|
+
* renderer silently reads the wrong half of.
|
|
253
|
+
*/
|
|
254
|
+
export const ResolvedClickHouseS3AuthSchema = type({
|
|
255
|
+
/** Transport discriminant; selects `use_environment_credentials`. */
|
|
256
|
+
kind: '"irsa"',
|
|
257
|
+
/** IAM role ARN annotated onto the ServiceAccount. */
|
|
258
|
+
roleArn: 'string > 0',
|
|
259
|
+
/** ServiceAccount name; defaulted by the composition when absent. */
|
|
260
|
+
'serviceAccountName?': 'string > 0',
|
|
261
|
+
}).or({
|
|
262
|
+
/** Transport discriminant; selects `from_env` credential elements. */
|
|
263
|
+
kind: '"secretRef"',
|
|
264
|
+
/** Secret holding the access keys, in the CHI namespace. */
|
|
265
|
+
secretName: 'string > 0',
|
|
266
|
+
/** Secret key holding the access key id (defaulted by the resolver). */
|
|
267
|
+
accessKeyIdKey: 'string > 0',
|
|
268
|
+
/** Secret key holding the secret access key (defaulted by the resolver). */
|
|
269
|
+
secretAccessKeyKey: 'string > 0',
|
|
270
|
+
});
|
|
271
|
+
/** Backup schedule with every default applied. */
|
|
272
|
+
export const ResolvedClickHouseS3BackupSchema = type({
|
|
273
|
+
/** Cron schedule of the generated CronJob. */
|
|
274
|
+
schedule: 'string > 0',
|
|
275
|
+
/** Backup bucket (the disk's bucket unless overridden). */
|
|
276
|
+
bucket: 'string > 0',
|
|
277
|
+
/** Normalized, non-empty backup key prefix. */
|
|
278
|
+
prefix: 'string > 0',
|
|
279
|
+
/** Database to back up — a bare SQL identifier, checked by the resolver. */
|
|
280
|
+
database: 'string > 0',
|
|
281
|
+
/** Backup object-key base URL (`.../<prefix>/`), used by `BACKUP TO S3`. */
|
|
282
|
+
endpointUrl: 'string > 0',
|
|
283
|
+
/** Days of backups to keep; absent means keep everything. */
|
|
284
|
+
'retentionDays?': 'number.integer > 0',
|
|
285
|
+
/** ClickHouse credentials the CronJob connects with. */
|
|
286
|
+
'auth?': {
|
|
287
|
+
/** Secret holding the ClickHouse user and password. */
|
|
288
|
+
secretName: 'string > 0',
|
|
289
|
+
/** Secret key holding the user name. */
|
|
290
|
+
usernameKey: 'string > 0',
|
|
291
|
+
/** Secret key holding the password. */
|
|
292
|
+
passwordKey: 'string > 0',
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
/** Fully defaulted, validated S3 storage configuration. */
|
|
296
|
+
export const ResolvedClickHouseS3StorageSchema = type({
|
|
297
|
+
/** Mode discriminant — see {@link RESOLVED_S3_STORAGE_REQUIREMENT}. */
|
|
298
|
+
mode: type('"s3"').describe(RESOLVED_S3_STORAGE_REQUIREMENT),
|
|
299
|
+
/** Bucket backing the MergeTree disk. */
|
|
300
|
+
bucket: 'string > 0',
|
|
301
|
+
/** Normalized key prefix without leading/trailing slashes ('' when absent). */
|
|
302
|
+
prefix: 'string',
|
|
303
|
+
/** AWS region; absent when a custom `endpoint` supplies the target. */
|
|
304
|
+
'region?': 'string > 0',
|
|
305
|
+
/** Custom S3-compatible base endpoint (MinIO); absent for real AWS. */
|
|
306
|
+
'endpoint?': 'string > 0',
|
|
307
|
+
/** Disk type — the durability choice (see the module docs). */
|
|
308
|
+
diskType: '"s3" | "s3_plain_rewritable"',
|
|
309
|
+
/** MergeTree storage policy name; rendered in XML ELEMENT-NAME position. */
|
|
310
|
+
policyName: 'string > 0',
|
|
311
|
+
/** Local read-through cache cap, already converted to bytes. */
|
|
312
|
+
cacheMaxSizeBytes: 'number.integer > 0',
|
|
313
|
+
/** Local cache directory, under the operator's data volume mount. */
|
|
314
|
+
cachePath: 'string > 0',
|
|
315
|
+
/** Credential transport for the disk. */
|
|
316
|
+
auth: ResolvedClickHouseS3AuthSchema,
|
|
317
|
+
/** Disk endpoint URL, always with a trailing slash. */
|
|
318
|
+
endpointUrl: 'string > 0',
|
|
319
|
+
/** Scheduled backups, when the caller asked for them. */
|
|
320
|
+
'backup?': ResolvedClickHouseS3BackupSchema,
|
|
321
|
+
});
|
|
322
|
+
/** Fully defaulted, validated PVC storage configuration (today's behaviour). */
|
|
323
|
+
export const ResolvedClickHousePvcStorageSchema = type({
|
|
324
|
+
/** Mode discriminant. */
|
|
325
|
+
mode: '"pvc"',
|
|
326
|
+
});
|
|
327
|
+
/** Discriminated resolution of the `storage` input. */
|
|
328
|
+
export const ResolvedClickHouseStorageSchema = ResolvedClickHousePvcStorageSchema.or(ResolvedClickHouseS3StorageSchema);
|
|
329
|
+
// ============================================================================
|
|
330
|
+
// Small validated parsers
|
|
331
|
+
// ============================================================================
|
|
332
|
+
/**
|
|
333
|
+
* Byte multiplier per Kubernetes quantity suffix.
|
|
334
|
+
*
|
|
335
|
+
* A `Map` rather than an object literal: the suffix is taken from CALLER input,
|
|
336
|
+
* and an object lookup keyed on caller input reads `Object.prototype` on a miss
|
|
337
|
+
* (`'constructor'`, `'__proto__'`), returning something that is not a number
|
|
338
|
+
* and is not `undefined` either. The regex in {@link parseByteQuantity} already
|
|
339
|
+
* bounds the suffix to this exact set, so the two guards are independent — the
|
|
340
|
+
* lookup does not depend on the pattern staying that tight.
|
|
341
|
+
*/
|
|
342
|
+
const BYTE_SUFFIXES = new Map([
|
|
343
|
+
['', 1],
|
|
344
|
+
['k', 1000],
|
|
345
|
+
['K', 1000],
|
|
346
|
+
['M', 1000 ** 2],
|
|
347
|
+
['G', 1000 ** 3],
|
|
348
|
+
['T', 1000 ** 4],
|
|
349
|
+
['Ki', 1024],
|
|
350
|
+
['Mi', 1024 ** 2],
|
|
351
|
+
['Gi', 1024 ** 3],
|
|
352
|
+
['Ti', 1024 ** 4],
|
|
353
|
+
]);
|
|
354
|
+
/**
|
|
355
|
+
* Parse a Kubernetes-style quantity into bytes.
|
|
356
|
+
*
|
|
357
|
+
* ClickHouse's cache `max_size` wants a byte count, while every other size in
|
|
358
|
+
* this factory family is a Kubernetes quantity — so the input stays a quantity
|
|
359
|
+
* and the conversion happens here (loudly) instead of the caller guessing.
|
|
360
|
+
*
|
|
361
|
+
* @param context - Entry point name for the error message
|
|
362
|
+
* @param field - Offending field path (e.g. `storage.cache.size`)
|
|
363
|
+
* @param value - Quantity string such as `'100Gi'`, `'50G'`, or `'1048576'`
|
|
364
|
+
* @returns The value in bytes
|
|
365
|
+
* @throws Error when the quantity cannot be parsed or is not positive
|
|
366
|
+
*/
|
|
367
|
+
export function parseByteQuantity(context, field, value) {
|
|
368
|
+
const match = /^([0-9]+(?:\.[0-9]+)?)\s*(|k|K|M|G|T|Ki|Mi|Gi|Ti)$/.exec(value.trim());
|
|
369
|
+
const suffix = match?.[2];
|
|
370
|
+
const digits = match?.[1];
|
|
371
|
+
if (digits === undefined || suffix === undefined) {
|
|
372
|
+
throw new Error(`${context}: '${field}' must be a Kubernetes-style byte quantity ` +
|
|
373
|
+
`(e.g. '100Gi', '50G', '1048576') — got ${JSON.stringify(value)}.`);
|
|
374
|
+
}
|
|
375
|
+
const bytes = Math.floor(Number(digits) * (BYTE_SUFFIXES.get(suffix) ?? 1));
|
|
376
|
+
if (!Number.isFinite(bytes) || bytes < 1) {
|
|
377
|
+
throw new Error(`${context}: '${field}' must resolve to at least one byte — got ${JSON.stringify(value)}.`);
|
|
378
|
+
}
|
|
379
|
+
return bytes;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Extract `{ major, minor }` from a ClickHouse version tag.
|
|
383
|
+
*
|
|
384
|
+
* Returns `undefined` for tags this function cannot read (`'latest'`, a digest
|
|
385
|
+
* pin, a schema reference) so callers can skip version gating rather than
|
|
386
|
+
* reject a legitimate but unparseable pin.
|
|
387
|
+
*/
|
|
388
|
+
export function parseClickHouseVersion(version) {
|
|
389
|
+
if (typeof version !== 'string')
|
|
390
|
+
return undefined;
|
|
391
|
+
const match = /^v?([0-9]{1,4})\.([0-9]{1,4})(?:[._-].*)?$/.exec(version.trim());
|
|
392
|
+
const major = match?.[1];
|
|
393
|
+
const minor = match?.[2];
|
|
394
|
+
if (major === undefined || minor === undefined)
|
|
395
|
+
return undefined;
|
|
396
|
+
return { major: Number(major), minor: Number(minor) };
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Assert the server version can run `s3_plain_rewritable` MergeTree writes.
|
|
400
|
+
*
|
|
401
|
+
* TWO ENFORCEMENT SITES, ONE FLOOR. A CONCRETE version is checked here and
|
|
402
|
+
* rejected loudly — including a version this function cannot READ (`'latest'`,
|
|
403
|
+
* a digest pin), because "unreadable" is not evidence that the server supports
|
|
404
|
+
* the `plain_rewritable` metadata type, and silently accepting it was the gap
|
|
405
|
+
* this replaces. A version that is NOT a concrete string is a per-instance
|
|
406
|
+
* schema reference: its value does not exist at construction, so no check here
|
|
407
|
+
* can see it, and the floor must instead travel into the generated schema as
|
|
408
|
+
* {@link ClickHouseS3PlainRewritableVersionSchema} — which
|
|
409
|
+
* `makeClickHouseCluster` does automatically, so KRO rejects an instance that
|
|
410
|
+
* selects an older server. A composition that wires `clickHouseInstallation()`
|
|
411
|
+
* by hand with a schema-reference `version` MUST use that schema for its own
|
|
412
|
+
* `version` field; nothing else can enforce the floor for it.
|
|
413
|
+
*
|
|
414
|
+
* @param context - Entry point name for the error message
|
|
415
|
+
* @param version - The configured ClickHouse server version tag, or a schema
|
|
416
|
+
* reference (skipped — see above)
|
|
417
|
+
* @throws Error when a concrete version is below
|
|
418
|
+
* {@link MIN_S3_PLAIN_REWRITABLE_VERSION} or cannot be parsed as
|
|
419
|
+
* `major.minor`
|
|
420
|
+
*/
|
|
421
|
+
export function assertS3PlainRewritableVersion(context, version) {
|
|
422
|
+
// Not a concrete string: a schema/resource reference whose value only exists
|
|
423
|
+
// per instance. Enforced by the generated schema's pattern instead.
|
|
424
|
+
if (typeof version !== 'string')
|
|
425
|
+
return;
|
|
426
|
+
const parsed = parseClickHouseVersion(version);
|
|
427
|
+
if (parsed === undefined) {
|
|
428
|
+
throw new Error(`${context}: storage.diskType 's3_plain_rewritable' requires a CONCRETE ClickHouse ` +
|
|
429
|
+
`version of at least ${MIN_S3_PLAIN_REWRITABLE_VERSION}, and ` +
|
|
430
|
+
`${JSON.stringify(version)} cannot be read as 'major.minor'. A moving tag or a ` +
|
|
431
|
+
`digest pin is not evidence that the server supports the metadata_type: ` +
|
|
432
|
+
`plain_rewritable form this factory emits, so it is refused rather than assumed — ` +
|
|
433
|
+
`pin an explicit version (e.g. '25.7'), or use diskType: 's3' with a backup schedule.`);
|
|
434
|
+
}
|
|
435
|
+
const floor = parseClickHouseVersion(MIN_S3_PLAIN_REWRITABLE_VERSION);
|
|
436
|
+
// Unreachable: the constant is a literal this function can parse. Kept as a
|
|
437
|
+
// total branch rather than a non-null assertion.
|
|
438
|
+
if (floor === undefined)
|
|
439
|
+
return;
|
|
440
|
+
const below = parsed.major < floor.major || (parsed.major === floor.major && parsed.minor < floor.minor);
|
|
441
|
+
if (below) {
|
|
442
|
+
throw new Error(`${context}: storage.diskType 's3_plain_rewritable' requires ClickHouse ` +
|
|
443
|
+
`>= ${MIN_S3_PLAIN_REWRITABLE_VERSION} (got '${String(version)}'). The ` +
|
|
444
|
+
`metadata_type: plain_rewritable form this factory emits was generalized in ` +
|
|
445
|
+
`24.5; earlier servers cannot run MergeTree writes/merges on it. Use ` +
|
|
446
|
+
`diskType: 's3' with a backup schedule on older servers.`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Assert a value is a legal S3 bucket name.
|
|
451
|
+
*
|
|
452
|
+
* ONE validator for both bucket-shaped options — `storage.bucket` and the
|
|
453
|
+
* `storage.backup.bucket` override — because both are appended into the same
|
|
454
|
+
* composed URL and therefore into the same `BACKUP … TO S3('<url>')` string
|
|
455
|
+
* literal. A second, hand-inlined copy for the override is exactly how the
|
|
456
|
+
* override came to be unchecked in the first place.
|
|
457
|
+
*
|
|
458
|
+
* @param context - Entry point name for the error message
|
|
459
|
+
* @param field - Offending option path (e.g. `storage.backup.bucket`)
|
|
460
|
+
* @param value - The bucket name received
|
|
461
|
+
* @throws Error naming the option and the value when the name breaks AWS's
|
|
462
|
+
* naming rules
|
|
463
|
+
*/
|
|
464
|
+
export function assertS3BucketName(context, field, value) {
|
|
465
|
+
const invalid = (why) => {
|
|
466
|
+
throw new Error(`${context}: '${field}' must be a valid S3 bucket name — ${why} (got ` +
|
|
467
|
+
`${JSON.stringify(value)}). AWS requires 3-63 characters of lowercase letters, ` +
|
|
468
|
+
`digits, '.' and '-', starting and ending alphanumeric, with no '..', no ` +
|
|
469
|
+
`IP-address shape, and none of the reserved prefixes ` +
|
|
470
|
+
`(${RESERVED_BUCKET_NAME_PREFIXES.map((entry) => entry.prefix).join(', ')}) or ` +
|
|
471
|
+
`suffixes (${RESERVED_BUCKET_NAME_SUFFIXES.map((entry) => entry.suffix).join(', ')}). ` +
|
|
472
|
+
`The name is appended into the disk endpoint URL that the server's ` +
|
|
473
|
+
`\`config.d/storage.xml\` and the \`BACKUP … TO S3('<url>')\` statement both read, so ` +
|
|
474
|
+
`it is constrained rather than escaped.`);
|
|
475
|
+
};
|
|
476
|
+
if (typeof value !== 'string')
|
|
477
|
+
invalid('it is not a string');
|
|
478
|
+
const name = value;
|
|
479
|
+
if (!S3_BUCKET_NAME_PATTERN.test(name)) {
|
|
480
|
+
invalid(`it does not match ${S3_BUCKET_NAME_PATTERN.source}`);
|
|
481
|
+
}
|
|
482
|
+
// The rules below are the rest of AWS's published general-purpose-bucket
|
|
483
|
+
// naming rules — every one a real CreateBucket rejection rather than TypeKro
|
|
484
|
+
// policy. They are separate checks because folding them into one pattern
|
|
485
|
+
// makes it unreadable, and because each one gets to name itself in the error.
|
|
486
|
+
if (name.includes('..'))
|
|
487
|
+
invalid("it contains '..'");
|
|
488
|
+
if (IP_SHAPED_BUCKET_NAME.test(name))
|
|
489
|
+
invalid('it is formatted as an IP address');
|
|
490
|
+
for (const { prefix, why } of RESERVED_BUCKET_NAME_PREFIXES) {
|
|
491
|
+
if (name.startsWith(prefix))
|
|
492
|
+
invalid(`it starts with the reserved '${prefix}' — ${why}`);
|
|
493
|
+
}
|
|
494
|
+
for (const { suffix, why } of RESERVED_BUCKET_NAME_SUFFIXES) {
|
|
495
|
+
if (name.endsWith(suffix))
|
|
496
|
+
invalid(`it ends with the reserved '${suffix}' — ${why}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Assert a value is an AWS region identifier.
|
|
501
|
+
*
|
|
502
|
+
* The region is interpolated into the HOST of the composed endpoint
|
|
503
|
+
* (`<bucket>.s3.<region>.amazonaws.com`), so an unconstrained value reaches the
|
|
504
|
+
* XML and the backup SQL literal exactly like the bucket does.
|
|
505
|
+
*
|
|
506
|
+
* @param context - Entry point name for the error message
|
|
507
|
+
* @param field - Offending option path (e.g. `storage.region`)
|
|
508
|
+
* @param value - The region received
|
|
509
|
+
* @throws Error naming the option and the value when the region is not
|
|
510
|
+
* {@link AWS_REGION_PATTERN}-shaped
|
|
511
|
+
*/
|
|
512
|
+
export function assertAwsRegion(context, field, value) {
|
|
513
|
+
if (typeof value === 'string' && AWS_REGION_PATTERN.test(value))
|
|
514
|
+
return;
|
|
515
|
+
throw new Error(`${context}: '${field}' must be an AWS region identifier matching ` +
|
|
516
|
+
`${AWS_REGION_PATTERN.source} — two lowercase letters, one or more lowercase words, ` +
|
|
517
|
+
`then a digit (e.g. 'us-east-2', 'eu-central-1', 'us-gov-west-1'); got ` +
|
|
518
|
+
`${JSON.stringify(value)}. The value becomes part of the endpoint HOST ` +
|
|
519
|
+
`(<bucket>.s3.<region>.amazonaws.com), which is rendered into the server's storage ` +
|
|
520
|
+
`XML and into the \`BACKUP … TO S3('<url>')\` string literal.`);
|
|
521
|
+
}
|
|
522
|
+
// ============================================================================
|
|
523
|
+
// Storage resolution
|
|
524
|
+
// ============================================================================
|
|
525
|
+
/**
|
|
526
|
+
* True when the storage input selects the object-storage mode.
|
|
527
|
+
*
|
|
528
|
+
* The predicate claims exactly what the check ESTABLISHES and no more: `mode`
|
|
529
|
+
* is `'s3'`, so the S3-only options are the ones that MAY be present — hence
|
|
530
|
+
* `Loosen<…>`, which leaves every one of them optional. Claiming the full
|
|
531
|
+
* `ClickHouseS3StorageOptions` here would assert `bucket: string`,
|
|
532
|
+
* `cache: { size: string }` and a valid `auth` on the strength of one
|
|
533
|
+
* discriminant, and the compiler would then stop asking
|
|
534
|
+
* {@link resolveClickHouseStorage} to check them — which is precisely the job
|
|
535
|
+
* that function exists to do.
|
|
536
|
+
*/
|
|
537
|
+
export function isS3Storage(storage) {
|
|
538
|
+
return storage.mode === 's3';
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Host shape accepted for a custom `storage.endpoint`.
|
|
542
|
+
*
|
|
543
|
+
* A DNS name (`minio.minio.svc.cluster.local`, `s3.example.com`, `minio`) or a
|
|
544
|
+
* dotted-quad IPv4 literal. Labels are the RFC 1123 shape the Kubernetes API
|
|
545
|
+
* uses for a Service/DNS name. An IPv6 literal in brackets is deliberately not
|
|
546
|
+
* accepted: ClickHouse's `<endpoint>` parser and the `BACKUP … TO S3()` URL
|
|
547
|
+
* both take the value as a plain string, and a bracketed authority has not been
|
|
548
|
+
* verified against either — an explicit refusal beats a silent surprise.
|
|
549
|
+
*/
|
|
550
|
+
const ENDPOINT_HOST_PATTERN = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i;
|
|
551
|
+
/**
|
|
552
|
+
* Parse and VALIDATE a custom S3-compatible endpoint, returning it normalized
|
|
553
|
+
* with no trailing slash so the caller can append `/<bucket>/<prefix>/`.
|
|
554
|
+
*
|
|
555
|
+
* STRUCTURE, not just characters. The character allow-list on the composed URL
|
|
556
|
+
* ({@link ENDPOINT_URL_CHARACTERS}) stops injection into the storage XML and
|
|
557
|
+
* the `BACKUP … TO S3('<url>')` SQL literal, but it happily accepts strings
|
|
558
|
+
* that are not usable endpoints — a userinfo section that would write
|
|
559
|
+
* credentials into `config.d/storage.xml`, a query string or fragment that the
|
|
560
|
+
* appended bucket path turns into nonsense, or an out-of-range port. Each of
|
|
561
|
+
* those is a different mistake and gets its own message.
|
|
562
|
+
*
|
|
563
|
+
* @param context - Entry point name for the error message
|
|
564
|
+
* @param endpoint - The caller's `storage.endpoint` value
|
|
565
|
+
* @returns The endpoint as `<scheme>://<host>[:<port>][<path>]`, no trailing
|
|
566
|
+
* slash
|
|
567
|
+
* @throws Error naming the specific part of the URL that is unusable
|
|
568
|
+
*/
|
|
569
|
+
export function parseS3EndpointUrl(context, endpoint) {
|
|
570
|
+
const invalid = (why) => {
|
|
571
|
+
throw new Error(`${context}: 'storage.endpoint' must be an absolute http(s) URL of the ` +
|
|
572
|
+
`S3-compatible service (e.g. 'http://minio.minio.svc.cluster.local:9000') — ${why} ` +
|
|
573
|
+
`(got ${JSON.stringify(endpoint)}). The bucket and prefix are appended by the ` +
|
|
574
|
+
`factory, so do not include them here; the value is rendered into the server's ` +
|
|
575
|
+
`\`config.d/storage.xml\` and into the \`BACKUP … TO S3('<url>')\` string literal.`);
|
|
576
|
+
};
|
|
577
|
+
// Checked before parsing so the message can say `storage.endpoint` rather
|
|
578
|
+
// than point at a composed URL the caller never typed.
|
|
579
|
+
if (!ENDPOINT_URL_CHARACTERS.test(endpoint)) {
|
|
580
|
+
invalid(`it uses characters outside ${ENDPOINT_URL_CHARACTERS.source} — no whitespace, ` +
|
|
581
|
+
`quotes, angle brackets, '&', backslash, control characters or non-ASCII (pass an ` +
|
|
582
|
+
`internationalized host in punycode)`);
|
|
583
|
+
}
|
|
584
|
+
let url;
|
|
585
|
+
try {
|
|
586
|
+
url = new URL(endpoint);
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
return invalid('it is not a parseable absolute URL');
|
|
590
|
+
}
|
|
591
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
592
|
+
invalid(`its scheme is '${url.protocol.replace(/:$/, '')}', not http or https`);
|
|
593
|
+
}
|
|
594
|
+
if (url.username !== '' || url.password !== '') {
|
|
595
|
+
invalid('it carries userinfo credentials — S3 keys come from `storage.auth` (IRSA or a ' +
|
|
596
|
+
'Secret reference), never from the endpoint URL');
|
|
597
|
+
}
|
|
598
|
+
if (url.search !== '')
|
|
599
|
+
invalid('it carries a query string');
|
|
600
|
+
if (url.hash !== '')
|
|
601
|
+
invalid('it carries a fragment');
|
|
602
|
+
if (url.hostname === '')
|
|
603
|
+
invalid('it has no host');
|
|
604
|
+
if (!ENDPOINT_HOST_PATTERN.test(url.hostname)) {
|
|
605
|
+
invalid(`its host ${JSON.stringify(url.hostname)} is not a DNS name or IPv4 literal ` +
|
|
606
|
+
`(matching ${ENDPOINT_HOST_PATTERN.source})`);
|
|
607
|
+
}
|
|
608
|
+
if (url.port !== '') {
|
|
609
|
+
// `new URL` already rejects a non-numeric or >65535 port, and drops the
|
|
610
|
+
// default port for the scheme. Re-check the range so a future parser
|
|
611
|
+
// change cannot let one through, and so 0 is refused explicitly.
|
|
612
|
+
const port = Number(url.port);
|
|
613
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
614
|
+
invalid(`its port ${JSON.stringify(url.port)} is not in 1-65535`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
// A base PATH is legitimate (a gateway mounted under a prefix), but it is
|
|
618
|
+
// joined with `/<bucket>/<prefix>/`, so it must be a clean path: no traversal
|
|
619
|
+
// segment, no empty segment that would double a slash.
|
|
620
|
+
const path = url.pathname === '/' ? '' : url.pathname.replace(/\/+$/, '');
|
|
621
|
+
if (path !== '') {
|
|
622
|
+
const segments = path.slice(1).split('/');
|
|
623
|
+
if (segments.some((segment) => segment === '' || segment === '.' || segment === '..')) {
|
|
624
|
+
invalid(`its path ${JSON.stringify(url.pathname)} has an empty or traversal segment; a base ` +
|
|
625
|
+
`path is allowed but must be a plain '/a/b' prefix`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return `${url.protocol}//${url.host}${path}`;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Normalize and VALIDATE an object-key prefix.
|
|
632
|
+
*
|
|
633
|
+
* Takes the option path as `field` because both `storage.prefix` and
|
|
634
|
+
* `storage.backup.prefix` come through here and the error has to name the one
|
|
635
|
+
* the caller actually set.
|
|
636
|
+
*
|
|
637
|
+
* The character rule is an allow-list per segment, for the same reason the
|
|
638
|
+
* endpoint has one: the prefix is appended verbatim into the composed URL that
|
|
639
|
+
* the `BACKUP … TO S3('<url>')` literal is built from. `..` is refused
|
|
640
|
+
* outright — as a path traversal it is meaningless against an S3 key namespace
|
|
641
|
+
* but it is exactly the shape that walks a backup out of its own prefix if the
|
|
642
|
+
* value is ever handed to something that resolves paths (the prune step's
|
|
643
|
+
* `aws s3 rm --recursive`, for one).
|
|
644
|
+
*/
|
|
645
|
+
function normalizePrefix(context, field, prefix) {
|
|
646
|
+
if (prefix === undefined)
|
|
647
|
+
return '';
|
|
648
|
+
const invalid = (why) => {
|
|
649
|
+
throw new Error(`${context}: '${field}' must be a safe object-key prefix — ${why} (got ` +
|
|
650
|
+
`${JSON.stringify(prefix)}). Each '/'-separated segment must match ` +
|
|
651
|
+
`${KEY_PREFIX_SEGMENT_CHARACTERS.source}: no quotes, whitespace, control ` +
|
|
652
|
+
`characters, '&', '%' or '..'. The prefix is appended verbatim into the endpoint ` +
|
|
653
|
+
`URL that the server's storage XML and the \`BACKUP … TO S3('<url>')\` string ` +
|
|
654
|
+
`literal are both built from.`);
|
|
655
|
+
};
|
|
656
|
+
if (typeof prefix !== 'string')
|
|
657
|
+
invalid('it is not a string');
|
|
658
|
+
const trimmed = prefix.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
659
|
+
if (trimmed.includes('//')) {
|
|
660
|
+
throw new Error(`${context}: '${field}' must not contain empty path segments — got ` +
|
|
661
|
+
`${JSON.stringify(prefix)}.`);
|
|
662
|
+
}
|
|
663
|
+
if (trimmed === '')
|
|
664
|
+
return '';
|
|
665
|
+
if (trimmed.includes('..'))
|
|
666
|
+
invalid("it contains '..'");
|
|
667
|
+
for (const segment of trimmed.split('/')) {
|
|
668
|
+
if (segment === '.')
|
|
669
|
+
invalid("it contains a '.' segment");
|
|
670
|
+
if (!KEY_PREFIX_SEGMENT_CHARACTERS.test(segment)) {
|
|
671
|
+
invalid(`the segment ${JSON.stringify(segment)} uses a character no key prefix may`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return trimmed;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Compose the disk/backup endpoint URL, then validate what was composed.
|
|
678
|
+
*
|
|
679
|
+
* ORDER IS THE POINT. Every component reaching this function is already
|
|
680
|
+
* checked on its own — bucket ({@link assertS3BucketName}), region
|
|
681
|
+
* ({@link assertAwsRegion}), prefix ({@link normalizePrefix}), and the
|
|
682
|
+
* `endpoint` right below — and each of those checks exists so the error can
|
|
683
|
+
* name the OPTION the caller set. But per-component checks are a set of doors
|
|
684
|
+
* that has to be complete, and the string that actually matters is the one the
|
|
685
|
+
* runtime sees: the `<endpoint>` text of `config.d/storage.xml` and the
|
|
686
|
+
* `BACKUP … TO S3('<url>')` literal the CronJob builds, where a `'` is extra
|
|
687
|
+
* SQL rather than a bad URL. So the composed result is re-checked against the
|
|
688
|
+
* same RFC 3986 allow-list as a BACKSTOP — a component check that is ever
|
|
689
|
+
* loosened, or a component that is ever added, cannot open that door quietly.
|
|
690
|
+
*
|
|
691
|
+
* @param context - Entry point name for the error message
|
|
692
|
+
* @param field - Option path prefix for the error (`storage` or
|
|
693
|
+
* `storage.backup`), so a bad backup override does not report `storage.*`
|
|
694
|
+
* @param bucket - Validated bucket name
|
|
695
|
+
* @param prefix - Normalized, validated key prefix ('' for the bucket root)
|
|
696
|
+
* @param region - Validated region; required when `endpoint` is absent
|
|
697
|
+
* @param endpoint - Custom S3-compatible base URL, if any
|
|
698
|
+
* @returns The composed URL, always with a trailing slash
|
|
699
|
+
* @throws Error when the endpoint is not an absolute http(s) URL, when the
|
|
700
|
+
* region is missing for AWS, or when the COMPOSED URL carries a character
|
|
701
|
+
* the allow-list forbids
|
|
702
|
+
*/
|
|
703
|
+
export function composeS3EndpointUrl(context, field, bucket, prefix, region, endpoint) {
|
|
704
|
+
const suffix = prefix === '' ? '' : `${prefix}/`;
|
|
705
|
+
let composed;
|
|
706
|
+
if (endpoint !== undefined) {
|
|
707
|
+
// STRUCTURE first, characters second. An allow-list alone accepts strings
|
|
708
|
+
// that are not URLs at all (`http:///x`, `http://a b`… well, not that one,
|
|
709
|
+
// but `http://user:pw@host`, `http://host?x=1`, `http://host:99999`), and
|
|
710
|
+
// every one of those either silently changes what the server talks to or
|
|
711
|
+
// leaks credentials into `config.d/storage.xml`. So the endpoint is PARSED
|
|
712
|
+
// and each part is checked, and the character allow-list stays as the
|
|
713
|
+
// backstop on the composed result.
|
|
714
|
+
const base = parseS3EndpointUrl(context, endpoint);
|
|
715
|
+
// Path-style addressing: MinIO and most S3-compatible services serve
|
|
716
|
+
// virtual-hosted-style only behind extra DNS configuration.
|
|
717
|
+
composed = `${base}/${bucket}/${suffix}`;
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
if (region === undefined || region.trim() === '') {
|
|
721
|
+
throw new Error(`${context}: 'storage.region' is required for AWS S3 (it forms the disk endpoint ` +
|
|
722
|
+
`https://<bucket>.s3.<region>.amazonaws.com/<prefix>/). Set 'storage.endpoint' ` +
|
|
723
|
+
`instead when targeting a custom S3-compatible service such as MinIO.`);
|
|
724
|
+
}
|
|
725
|
+
// A dot in the bucket name is legal S3 but breaks the VIRTUAL-HOSTED-style
|
|
726
|
+
// URL this branch composes: the wildcard certificate for
|
|
727
|
+
// `*.s3.<region>.amazonaws.com` does not cover a further label, so TLS
|
|
728
|
+
// verification fails. AWS documents this restriction for virtual-hosted
|
|
729
|
+
// access; path-style custom endpoints (the branch above) are unaffected.
|
|
730
|
+
if (bucket.includes('.')) {
|
|
731
|
+
throw new Error(`${context}: '${field}.bucket' ${JSON.stringify(bucket)} contains a '.', which is ` +
|
|
732
|
+
`legal for S3 but not usable over HTTPS in the virtual-hosted-style endpoint this ` +
|
|
733
|
+
`factory composes for AWS (https://<bucket>.s3.<region>.amazonaws.com/): the ` +
|
|
734
|
+
`wildcard certificate does not cover an extra label, so TLS verification fails. ` +
|
|
735
|
+
`Use a dot-free bucket name, or set 'storage.endpoint' to a path-style endpoint.`);
|
|
736
|
+
}
|
|
737
|
+
composed = `https://${bucket}.s3.${region}.amazonaws.com/${suffix}`;
|
|
738
|
+
}
|
|
739
|
+
if (!ENDPOINT_URL_CHARACTERS.test(composed)) {
|
|
740
|
+
throw new Error(`${context}: the composed '${field}' endpoint URL ${JSON.stringify(composed)} must use ` +
|
|
741
|
+
`only URL characters matching ${ENDPOINT_URL_CHARACTERS.source} — no whitespace, ` +
|
|
742
|
+
`quotes, angle brackets, '&', backslash or control characters. Check ` +
|
|
743
|
+
`'${field}.bucket', '${field}.prefix', 'storage.region' and 'storage.endpoint': the ` +
|
|
744
|
+
`composed value is what lands in the server's storage XML and in the ` +
|
|
745
|
+
`\`BACKUP … TO S3('<url>')\` string literal, so it is validated as a whole and not ` +
|
|
746
|
+
`only component by component.`);
|
|
747
|
+
}
|
|
748
|
+
return composed;
|
|
749
|
+
}
|
|
750
|
+
function resolveAuth(context, auth) {
|
|
751
|
+
if (auth === null || typeof auth !== 'object') {
|
|
752
|
+
throw new Error(`${context}: 'storage.auth' is required in S3 mode — use ` +
|
|
753
|
+
`{ irsa: { roleArn } } on EKS or { secretRef: { name } } elsewhere. Access keys are ` +
|
|
754
|
+
`never accepted inline.`);
|
|
755
|
+
}
|
|
756
|
+
const irsa = auth.irsa;
|
|
757
|
+
const secretRef = auth.secretRef;
|
|
758
|
+
if (irsa !== undefined && secretRef !== undefined) {
|
|
759
|
+
throw new Error(`${context}: 'storage.auth' accepts exactly one credential transport — got both ` +
|
|
760
|
+
`'irsa' and 'secretRef'.`);
|
|
761
|
+
}
|
|
762
|
+
if (irsa !== undefined) {
|
|
763
|
+
if (typeof irsa.roleArn !== 'string' || irsa.roleArn.trim() === '') {
|
|
764
|
+
throw new Error(`${context}: 'storage.auth.irsa.roleArn' is required and must be a string.`);
|
|
765
|
+
}
|
|
766
|
+
return {
|
|
767
|
+
kind: 'irsa',
|
|
768
|
+
roleArn: irsa.roleArn,
|
|
769
|
+
...(irsa.serviceAccountName !== undefined && {
|
|
770
|
+
serviceAccountName: irsa.serviceAccountName,
|
|
771
|
+
}),
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
if (secretRef !== undefined) {
|
|
775
|
+
if (typeof secretRef.name !== 'string' || secretRef.name.trim() === '') {
|
|
776
|
+
throw new Error(`${context}: 'storage.auth.secretRef.name' is required.`);
|
|
777
|
+
}
|
|
778
|
+
return {
|
|
779
|
+
kind: 'secretRef',
|
|
780
|
+
secretName: secretRef.name,
|
|
781
|
+
accessKeyIdKey: secretRef.accessKeyIdKey ?? DEFAULT_ACCESS_KEY_ID_SECRET_KEY,
|
|
782
|
+
secretAccessKeyKey: secretRef.secretAccessKeyKey ?? DEFAULT_SECRET_ACCESS_KEY_SECRET_KEY,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
throw new Error(`${context}: 'storage.auth' is required in S3 mode — use ` +
|
|
786
|
+
`{ irsa: { roleArn } } on EKS or { secretRef: { name } } elsewhere. Access keys are ` +
|
|
787
|
+
`never accepted inline.`);
|
|
788
|
+
}
|
|
789
|
+
function resolveBackup(context, backup, diskBucket, region, endpoint) {
|
|
790
|
+
if (typeof backup.schedule !== 'string' || backup.schedule.trim() === '') {
|
|
791
|
+
throw new Error(`${context}: 'storage.backup.schedule' is required and must be a cron expression ` +
|
|
792
|
+
`(e.g. '0 2 * * *').`);
|
|
793
|
+
}
|
|
794
|
+
let retentionDays;
|
|
795
|
+
if (backup.retention !== undefined) {
|
|
796
|
+
const days = backup.retention.days;
|
|
797
|
+
if (typeof days !== 'number' || !Number.isInteger(days) || days < 1) {
|
|
798
|
+
throw new Error(`${context}: 'storage.backup.retention.days' must be a positive integer (got ` +
|
|
799
|
+
`${JSON.stringify(days)}).`);
|
|
800
|
+
}
|
|
801
|
+
retentionDays = days;
|
|
802
|
+
}
|
|
803
|
+
const database = backup.database ?? DEFAULT_BACKUP_DATABASE;
|
|
804
|
+
// The generated CronJob interpolates this into a `BACKUP DATABASE <db>`
|
|
805
|
+
// statement, so it must be a bare SQL identifier — validated here so the
|
|
806
|
+
// script never needs quoting and cannot be turned into extra SQL.
|
|
807
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(database)) {
|
|
808
|
+
throw new Error(`${context}: 'storage.backup.database' must be a bare SQL identifier ` +
|
|
809
|
+
`(letters, digits and underscores, not starting with a digit) — got ` +
|
|
810
|
+
`${JSON.stringify(database)}.`);
|
|
811
|
+
}
|
|
812
|
+
const bucket = backup.bucket ?? diskBucket;
|
|
813
|
+
// The OVERRIDE is validated with the same validator as the primary bucket
|
|
814
|
+
// (`storage.bucket`), naming whichever option the value actually came from.
|
|
815
|
+
// It is appended into the composed backup URL, so "the disk bucket was
|
|
816
|
+
// checked" says nothing about it.
|
|
817
|
+
assertS3BucketName(context, backup.bucket === undefined ? 'storage.bucket' : 'storage.backup.bucket', bucket);
|
|
818
|
+
const prefix = normalizePrefix(context, 'storage.backup.prefix', backup.prefix ?? DEFAULT_BACKUP_PREFIX);
|
|
819
|
+
if (prefix === '') {
|
|
820
|
+
throw new Error(`${context}: 'storage.backup.prefix' must be a non-empty key prefix — backups must not ` +
|
|
821
|
+
`share the bucket root with the data disk.`);
|
|
822
|
+
}
|
|
823
|
+
const secretRef = backup.auth?.secretRef;
|
|
824
|
+
if (backup.auth !== undefined &&
|
|
825
|
+
(secretRef === undefined || typeof secretRef.name !== 'string')) {
|
|
826
|
+
throw new Error(`${context}: 'storage.backup.auth' requires 'secretRef.name' — the CronJob reads the ` +
|
|
827
|
+
`ClickHouse user and password from that Secret.`);
|
|
828
|
+
}
|
|
829
|
+
return {
|
|
830
|
+
schedule: backup.schedule,
|
|
831
|
+
bucket,
|
|
832
|
+
prefix,
|
|
833
|
+
database,
|
|
834
|
+
endpointUrl: composeS3EndpointUrl(context, 'storage.backup', bucket, prefix, region, endpoint),
|
|
835
|
+
...(retentionDays !== undefined && { retentionDays }),
|
|
836
|
+
...(secretRef !== undefined &&
|
|
837
|
+
typeof secretRef.name === 'string' && {
|
|
838
|
+
auth: {
|
|
839
|
+
secretName: secretRef.name,
|
|
840
|
+
usernameKey: secretRef.usernameKey ?? 'username',
|
|
841
|
+
passwordKey: secretRef.passwordKey ?? 'password',
|
|
842
|
+
},
|
|
843
|
+
}),
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Resolve and validate the `storage` input into a discriminated result.
|
|
848
|
+
*
|
|
849
|
+
* The S3 branch is BUILD-TIME data: it compiles into an XML document and
|
|
850
|
+
* selects which resources exist (ServiceAccount, backup CronJob), so a schema
|
|
851
|
+
* reference anywhere inside it can never serialize — this function rejects one
|
|
852
|
+
* loudly rather than emitting `__KUBERNETES_REF__` markers into server config.
|
|
853
|
+
*
|
|
854
|
+
* @param context - Entry point name used in every error message
|
|
855
|
+
* @param storage - The `storage` field as supplied by the caller
|
|
856
|
+
* @param version - The configured ClickHouse version (gates
|
|
857
|
+
* `s3_plain_rewritable`); pass `undefined` to skip the gate
|
|
858
|
+
* @returns The resolved storage configuration
|
|
859
|
+
* @throws Error when PVC and S3 options are mixed, when a required S3 field is
|
|
860
|
+
* missing, or when a value is not build-time concrete
|
|
861
|
+
*/
|
|
862
|
+
export function resolveClickHouseStorage(context, storage, version) {
|
|
863
|
+
const asRecord = storage;
|
|
864
|
+
if (!isS3Storage(storage)) {
|
|
865
|
+
const stray = S3_ONLY_STORAGE_KEYS.filter((key) => asRecord[key] !== undefined);
|
|
866
|
+
if (stray.length > 0) {
|
|
867
|
+
throw new Error(`${context}: storage mode 'pvc' (the default) rejects the S3-only option(s) ` +
|
|
868
|
+
`${stray.map((key) => `'storage.${key}'`).join(', ')}. PVC and S3 storage cannot be ` +
|
|
869
|
+
`mixed — set storage.mode: 's3' to use them, or remove them to keep PVC-backed ` +
|
|
870
|
+
`MergeTree storage.`);
|
|
871
|
+
}
|
|
872
|
+
return { mode: 'pvc' };
|
|
873
|
+
}
|
|
874
|
+
// Every S3 field is build-time (XML text + resource selection). A ref here
|
|
875
|
+
// would serialize into server configuration as a marker string.
|
|
876
|
+
const s3Only = {};
|
|
877
|
+
for (const key of S3_ONLY_STORAGE_KEYS) {
|
|
878
|
+
if (asRecord[key] !== undefined)
|
|
879
|
+
s3Only[key] = asRecord[key];
|
|
880
|
+
}
|
|
881
|
+
if (containsKubernetesRefs(s3Only)) {
|
|
882
|
+
throw new Error(`${context}: 'storage' in S3 mode contains a schema/resource reference. The S3 disk ` +
|
|
883
|
+
`configuration compiles to a storage_configuration XML document and selects which ` +
|
|
884
|
+
`resources exist (ServiceAccount, backup CronJob), so it is fixed at CONSTRUCTION ` +
|
|
885
|
+
`time — pass it to makeClickHouseCluster({ storage: { mode: 's3', ... } }) as concrete ` +
|
|
886
|
+
`values. Only the thin local volume sizing (storage.size / storage.storageClassName) ` +
|
|
887
|
+
`is runtime spec.`);
|
|
888
|
+
}
|
|
889
|
+
if (typeof storage.bucket !== 'string' || storage.bucket.trim() === '') {
|
|
890
|
+
throw new Error(`${context}: 'storage.bucket' is required in S3 mode.`);
|
|
891
|
+
}
|
|
892
|
+
assertS3BucketName(context, 'storage.bucket', storage.bucket);
|
|
893
|
+
// Validated whenever it is set, not only on the AWS path: it also travels to
|
|
894
|
+
// the prune container as `AWS_REGION`, and `storage.endpoint` being present
|
|
895
|
+
// must not turn the region into an unchecked field.
|
|
896
|
+
if (storage.region !== undefined) {
|
|
897
|
+
assertAwsRegion(context, 'storage.region', storage.region);
|
|
898
|
+
}
|
|
899
|
+
if (storage.cache === undefined || typeof storage.cache.size !== 'string') {
|
|
900
|
+
throw new Error(`${context}: 'storage.cache.size' is required in S3 mode — it caps the local ` +
|
|
901
|
+
`read-through cache that keeps queries off the network. It must fit inside ` +
|
|
902
|
+
`'storage.size' (the thin local volume that hosts it).`);
|
|
903
|
+
}
|
|
904
|
+
const diskType = storage.diskType ?? 's3';
|
|
905
|
+
if (diskType !== 's3' && diskType !== 's3_plain_rewritable') {
|
|
906
|
+
throw new Error(`${context}: 'storage.diskType' must be 's3' or 's3_plain_rewritable' — got ` +
|
|
907
|
+
`${JSON.stringify(diskType)}.`);
|
|
908
|
+
}
|
|
909
|
+
if (diskType === 's3_plain_rewritable') {
|
|
910
|
+
assertS3PlainRewritableVersion(context, version);
|
|
911
|
+
}
|
|
912
|
+
const prefix = normalizePrefix(context, 'storage.prefix', storage.prefix);
|
|
913
|
+
const cacheMaxSizeBytes = parseByteQuantity(context, 'storage.cache.size', storage.cache.size);
|
|
914
|
+
const localBytes = typeof storage.size === 'string'
|
|
915
|
+
? parseByteQuantity(context, 'storage.size', storage.size)
|
|
916
|
+
: undefined;
|
|
917
|
+
if (localBytes !== undefined && cacheMaxSizeBytes > localBytes) {
|
|
918
|
+
throw new Error(`${context}: 'storage.cache.size' (${storage.cache.size}) exceeds 'storage.size' ` +
|
|
919
|
+
`(${storage.size}). The cache lives on the local volume, so a ` +
|
|
920
|
+
`cache larger than the volume fills the disk instead of evicting.`);
|
|
921
|
+
}
|
|
922
|
+
// The policy name is rendered as an XML ELEMENT NAME (`<s3_main>`) inside
|
|
923
|
+
// `config.d/storage.xml` and is also the value of the
|
|
924
|
+
// `merge_tree/storage_policy` setting that every table then references, so
|
|
925
|
+
// it has to be a bare identifier — a `<`, a quote or a space there produces
|
|
926
|
+
// a different element or a malformed document, and escaping is not available
|
|
927
|
+
// in element-name position.
|
|
928
|
+
const policyName = storage.policyName ?? DEFAULT_S3_POLICY_NAME;
|
|
929
|
+
assertClickHouseIdentifier(context, 'storage.policyName', policyName);
|
|
930
|
+
const resolved = {
|
|
931
|
+
mode: 's3',
|
|
932
|
+
bucket: storage.bucket,
|
|
933
|
+
prefix,
|
|
934
|
+
...(storage.region !== undefined && { region: storage.region }),
|
|
935
|
+
...(storage.endpoint !== undefined && { endpoint: storage.endpoint }),
|
|
936
|
+
diskType,
|
|
937
|
+
policyName,
|
|
938
|
+
cacheMaxSizeBytes,
|
|
939
|
+
cachePath: storage.cache.path ?? DEFAULT_S3_CACHE_PATH,
|
|
940
|
+
auth: resolveAuth(context, storage.auth),
|
|
941
|
+
endpointUrl: composeS3EndpointUrl(context, 'storage', storage.bucket, prefix, storage.region, storage.endpoint),
|
|
942
|
+
...(storage.backup !== undefined && {
|
|
943
|
+
backup: resolveBackup(context, storage.backup, storage.bucket, storage.region, storage.endpoint),
|
|
944
|
+
}),
|
|
945
|
+
};
|
|
946
|
+
// THE RESOLUTION IS CHECKED AGAINST ITS OWN SCHEMA, at the boundary where it
|
|
947
|
+
// stops being this function's local object and becomes a value the XML
|
|
948
|
+
// renderer, the pod-template patch and the backup CronJob all field-select
|
|
949
|
+
// from. The field-by-field guards above produce specific, actionable
|
|
950
|
+
// messages and stay; this is the total one that cannot go stale — a key
|
|
951
|
+
// added to the schema and forgotten here, or a value that reached the object
|
|
952
|
+
// through a path with no guard of its own, fails HERE rather than several
|
|
953
|
+
// calls deeper in a rendered manifest.
|
|
954
|
+
const validated = ResolvedClickHouseS3StorageSchema(resolved);
|
|
955
|
+
if (validated instanceof type.errors) {
|
|
956
|
+
throw new Error(`${context}: internal error — the resolved S3 storage does not satisfy ` +
|
|
957
|
+
`ResolvedClickHouseS3StorageSchema: ${validated.summary}. This is a bug in ` +
|
|
958
|
+
`resolveClickHouseStorage(), not in the supplied 'storage'.`);
|
|
959
|
+
}
|
|
960
|
+
return validated;
|
|
961
|
+
}
|
|
962
|
+
// ============================================================================
|
|
963
|
+
// XML rendering
|
|
964
|
+
// ============================================================================
|
|
965
|
+
function indent(depth) {
|
|
966
|
+
return ' '.repeat(depth);
|
|
967
|
+
}
|
|
968
|
+
/** Render the credential elements shared by the disk and backup sections. */
|
|
969
|
+
function renderCredentialElements(auth, depth) {
|
|
970
|
+
if (auth.kind === 'irsa') {
|
|
971
|
+
// The AWS SDK inside ClickHouse picks up the projected web-identity token
|
|
972
|
+
// that the EKS pod identity webhook mounts for the annotated
|
|
973
|
+
// ServiceAccount. No key material anywhere in the manifests.
|
|
974
|
+
return [`${indent(depth)}<use_environment_credentials>true</use_environment_credentials>`];
|
|
975
|
+
}
|
|
976
|
+
// `from_env` substitution: the XML names the env var, the pod template
|
|
977
|
+
// supplies it from the Secret. The key material never enters the CHI spec.
|
|
978
|
+
return [
|
|
979
|
+
`${indent(depth)}<access_key_id from_env="${xmlAttr(S3_ACCESS_KEY_ID_ENV)}"></access_key_id>`,
|
|
980
|
+
`${indent(depth)}<secret_access_key from_env="${xmlAttr(S3_SECRET_ACCESS_KEY_ENV)}"></secret_access_key>`,
|
|
981
|
+
];
|
|
982
|
+
}
|
|
983
|
+
/** Render the disk `<type>` family for the resolved disk type. */
|
|
984
|
+
function renderDiskTypeElements(diskType, depth) {
|
|
985
|
+
if (diskType === 's3_plain_rewritable') {
|
|
986
|
+
// Explicit modern form (24.5+): object_storage + s3 + plain_rewritable
|
|
987
|
+
// metadata. Metadata lives IN the bucket, which is what makes the bucket
|
|
988
|
+
// self-describing after node loss.
|
|
989
|
+
return [
|
|
990
|
+
`${indent(depth)}<type>object_storage</type>`,
|
|
991
|
+
`${indent(depth)}<object_storage_type>s3</object_storage_type>`,
|
|
992
|
+
`${indent(depth)}<metadata_type>plain_rewritable</metadata_type>`,
|
|
993
|
+
];
|
|
994
|
+
}
|
|
995
|
+
return [`${indent(depth)}<type>s3</type>`];
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Every value the storage document interpolates as an XML ELEMENT NAME, paired
|
|
999
|
+
* with the option (or constant) it came from so the error can name it.
|
|
1000
|
+
*
|
|
1001
|
+
* Kept as one list rather than checked inline at each `lines.push` so that the
|
|
1002
|
+
* set of element-name sites is auditable in a single place — the point of the
|
|
1003
|
+
* check is that this list and the `<${...}>` interpolations below stay the same
|
|
1004
|
+
* set.
|
|
1005
|
+
*/
|
|
1006
|
+
function elementNameSites(resolved) {
|
|
1007
|
+
return [
|
|
1008
|
+
['storage.policyName', resolved.policyName],
|
|
1009
|
+
['S3_DISK_NAME', S3_DISK_NAME],
|
|
1010
|
+
['S3_CACHE_DISK_NAME', S3_CACHE_DISK_NAME],
|
|
1011
|
+
['S3_POLICY_VOLUME_NAME', S3_POLICY_VOLUME_NAME],
|
|
1012
|
+
['S3_BACKUP_CONFIG_SECTION', S3_BACKUP_CONFIG_SECTION],
|
|
1013
|
+
];
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Render the `storage_configuration` (and, with a backup schedule, the `<s3>`
|
|
1017
|
+
* credential section) XML for the CHI's `configuration.files`.
|
|
1018
|
+
*
|
|
1019
|
+
* The document is fully STATIC: credentials arrive through
|
|
1020
|
+
* `use_environment_credentials` or `from_env`, so no secret material is ever
|
|
1021
|
+
* rendered here.
|
|
1022
|
+
*
|
|
1023
|
+
* @param resolved - Resolved S3 storage configuration
|
|
1024
|
+
* @returns The XML document text, newline-terminated
|
|
1025
|
+
*/
|
|
1026
|
+
export function renderStorageConfigurationXml(resolved) {
|
|
1027
|
+
// Every value below is checked at the XML BOUNDARY, not only at resolve
|
|
1028
|
+
// time. This function is exported and takes a `ResolvedClickHouseS3Storage`,
|
|
1029
|
+
// so a caller can hand it a hand-built object that never went through
|
|
1030
|
+
// `resolveClickHouseStorage`; and re-checking the four module constants
|
|
1031
|
+
// means a later change that makes any of them configurable inherits the rule
|
|
1032
|
+
// instead of quietly bypassing it.
|
|
1033
|
+
for (const [field, name] of elementNameSites(resolved)) {
|
|
1034
|
+
assertClickHouseIdentifier('renderStorageConfigurationXml', field, name);
|
|
1035
|
+
}
|
|
1036
|
+
const lines = ['<clickhouse>', `${indent(1)}<storage_configuration>`];
|
|
1037
|
+
lines.push(`${indent(2)}<disks>`);
|
|
1038
|
+
lines.push(`${indent(3)}<${S3_DISK_NAME}>`);
|
|
1039
|
+
lines.push(...renderDiskTypeElements(resolved.diskType, 4));
|
|
1040
|
+
lines.push(`${indent(4)}<endpoint>${xmlText(resolved.endpointUrl)}</endpoint>`);
|
|
1041
|
+
lines.push(...renderCredentialElements(resolved.auth, 4));
|
|
1042
|
+
lines.push(`${indent(3)}</${S3_DISK_NAME}>`);
|
|
1043
|
+
lines.push(`${indent(3)}<${S3_CACHE_DISK_NAME}>`);
|
|
1044
|
+
lines.push(`${indent(4)}<type>cache</type>`);
|
|
1045
|
+
lines.push(`${indent(4)}<disk>${S3_DISK_NAME}</disk>`);
|
|
1046
|
+
lines.push(`${indent(4)}<path>${xmlText(resolved.cachePath)}</path>`);
|
|
1047
|
+
lines.push(`${indent(4)}<max_size>${xmlText(String(resolved.cacheMaxSizeBytes))}</max_size>`);
|
|
1048
|
+
// Write-through caching: freshly inserted parts stay locally readable, which
|
|
1049
|
+
// is what makes an object-store-backed cluster usable for recent-data
|
|
1050
|
+
// queries (the dominant observability access pattern).
|
|
1051
|
+
lines.push(`${indent(4)}<cache_on_write_operations>true</cache_on_write_operations>`);
|
|
1052
|
+
lines.push(`${indent(3)}</${S3_CACHE_DISK_NAME}>`);
|
|
1053
|
+
lines.push(`${indent(2)}</disks>`);
|
|
1054
|
+
lines.push(`${indent(2)}<policies>`);
|
|
1055
|
+
lines.push(`${indent(3)}<${resolved.policyName}>`);
|
|
1056
|
+
lines.push(`${indent(4)}<volumes>`);
|
|
1057
|
+
lines.push(`${indent(5)}<${S3_POLICY_VOLUME_NAME}>`);
|
|
1058
|
+
lines.push(`${indent(6)}<disk>${S3_CACHE_DISK_NAME}</disk>`);
|
|
1059
|
+
lines.push(`${indent(5)}</${S3_POLICY_VOLUME_NAME}>`);
|
|
1060
|
+
lines.push(`${indent(4)}</volumes>`);
|
|
1061
|
+
lines.push(`${indent(3)}</${resolved.policyName}>`);
|
|
1062
|
+
lines.push(`${indent(2)}</policies>`);
|
|
1063
|
+
lines.push(`${indent(1)}</storage_configuration>`);
|
|
1064
|
+
if (resolved.backup !== undefined) {
|
|
1065
|
+
// `BACKUP ... TO S3(url)` runs SERVER-side, so the destination credentials
|
|
1066
|
+
// must come from the server's `<s3>` config (matched by endpoint prefix) —
|
|
1067
|
+
// that keeps keys out of the query text and therefore out of query_log.
|
|
1068
|
+
lines.push(`${indent(1)}<s3>`);
|
|
1069
|
+
lines.push(`${indent(2)}<${S3_BACKUP_CONFIG_SECTION}>`);
|
|
1070
|
+
lines.push(`${indent(3)}<endpoint>${xmlText(resolved.backup.endpointUrl)}</endpoint>`);
|
|
1071
|
+
lines.push(...renderCredentialElements(resolved.auth, 3));
|
|
1072
|
+
lines.push(`${indent(2)}</${S3_BACKUP_CONFIG_SECTION}>`);
|
|
1073
|
+
lines.push(`${indent(1)}</s3>`);
|
|
1074
|
+
}
|
|
1075
|
+
lines.push('</clickhouse>', '');
|
|
1076
|
+
return lines.join('\n');
|
|
1077
|
+
}
|
|
1078
|
+
// ============================================================================
|
|
1079
|
+
// CHI fragments
|
|
1080
|
+
// ============================================================================
|
|
1081
|
+
/** CHI `configuration.files` entry for the resolved S3 storage. */
|
|
1082
|
+
export function clickHouseS3ConfigurationFiles(resolved) {
|
|
1083
|
+
return { [CHI_STORAGE_CONFIG_FILE]: renderStorageConfigurationXml(resolved) };
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* CHI `configuration.settings` entry that makes the S3 policy the DEFAULT.
|
|
1087
|
+
*
|
|
1088
|
+
* This is what lets tooling outside TypeKro — the ClickStack/HyperDX gateway
|
|
1089
|
+
* collector's goose migrations, SigNoz's migrator — create its tables with no
|
|
1090
|
+
* `SETTINGS storage_policy` clause and still land on object storage.
|
|
1091
|
+
*/
|
|
1092
|
+
export function clickHouseS3ConfigurationSettings(resolved) {
|
|
1093
|
+
return { [MERGE_TREE_STORAGE_POLICY_SETTING]: resolved.policyName };
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Env vars the ClickHouse container needs for Secret-backed credentials.
|
|
1097
|
+
*
|
|
1098
|
+
* Empty for IRSA. `optional: false` so a missing Secret fails the pod loudly
|
|
1099
|
+
* instead of starting a server whose S3 disk silently 403s.
|
|
1100
|
+
*/
|
|
1101
|
+
export function clickHouseS3ContainerEnv(resolved) {
|
|
1102
|
+
if (resolved.auth.kind !== 'secretRef')
|
|
1103
|
+
return [];
|
|
1104
|
+
return [
|
|
1105
|
+
{
|
|
1106
|
+
name: S3_ACCESS_KEY_ID_ENV,
|
|
1107
|
+
valueFrom: {
|
|
1108
|
+
secretKeyRef: {
|
|
1109
|
+
name: resolved.auth.secretName,
|
|
1110
|
+
key: resolved.auth.accessKeyIdKey,
|
|
1111
|
+
optional: false,
|
|
1112
|
+
},
|
|
1113
|
+
},
|
|
1114
|
+
},
|
|
1115
|
+
{
|
|
1116
|
+
name: S3_SECRET_ACCESS_KEY_ENV,
|
|
1117
|
+
valueFrom: {
|
|
1118
|
+
secretKeyRef: {
|
|
1119
|
+
name: resolved.auth.secretName,
|
|
1120
|
+
key: resolved.auth.secretAccessKeyKey,
|
|
1121
|
+
optional: false,
|
|
1122
|
+
},
|
|
1123
|
+
},
|
|
1124
|
+
},
|
|
1125
|
+
];
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* ServiceAccount name the CHI pod template must run as, when IRSA is used.
|
|
1129
|
+
*
|
|
1130
|
+
* @param resolved - Resolved S3 storage configuration
|
|
1131
|
+
* @param installationName - CHI name, used for the derived default
|
|
1132
|
+
* @returns The ServiceAccount name, or `undefined` for non-IRSA credentials
|
|
1133
|
+
*/
|
|
1134
|
+
export function clickHouseS3ServiceAccountName(resolved, installationName) {
|
|
1135
|
+
if (resolved.auth.kind !== 'irsa')
|
|
1136
|
+
return undefined;
|
|
1137
|
+
return resolved.auth.serviceAccountName ?? `${installationName}-s3`;
|
|
1138
|
+
}
|
|
1139
|
+
//# sourceMappingURL=s3-storage.js.map
|