mirror of
https://gitlab.sarex.io/infra/terraform-contour-mirror.git
synced 2026-08-05 18:31:00 +03:00
138 lines
5.1 KiB
Bash
Executable File
138 lines
5.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
env_name="${ENVIRONMENT:-$(basename "$(dirname "${TG_ROOT:-live/prod/kafka-topics}")")}"
|
|
infra_path="${INFRASTRUCTURE_CONFIG_PATH:-${repo_root}/infrastructure.yaml}"
|
|
|
|
python3 - "${infra_path}" "${env_name}" <<'PY'
|
|
import os
|
|
import sys
|
|
|
|
try:
|
|
import yaml
|
|
except Exception as exc:
|
|
raise SystemExit(f"pyyaml is required for Kafka topics validation: {exc}")
|
|
|
|
infra_path, env = sys.argv[1:3]
|
|
|
|
def load_yaml(path):
|
|
if not os.path.exists(path):
|
|
raise SystemExit(f"File not found: {path}")
|
|
with open(path, "r", encoding="utf-8") as fh:
|
|
return yaml.safe_load(fh) or {}
|
|
|
|
def env_value(value, env, default=None):
|
|
if value is None:
|
|
return default
|
|
if isinstance(value, dict):
|
|
if env in value:
|
|
return value[env]
|
|
if "_default" in value:
|
|
return value["_default"]
|
|
return default
|
|
return value
|
|
|
|
infra = load_yaml(infra_path)
|
|
|
|
env_cfg = (infra.get("environments") or {}).get(env)
|
|
if not isinstance(env_cfg, dict):
|
|
raise SystemExit(f"Environment '{env}' is not defined in {infra_path}")
|
|
|
|
cluster_refs = env_cfg.get("kafka_cluster_refs") or {}
|
|
policy = env_cfg.get("kafka_policy") or {}
|
|
topics = ((env_cfg.get("kafka") or {}).get("topics"))
|
|
|
|
if topics is None:
|
|
topics = []
|
|
if not isinstance(topics, list):
|
|
raise SystemExit(f"environments.{env}.kafka.topics must be an array")
|
|
|
|
allowed_cleanup = {"delete", "compact"}
|
|
allowed_deletion = {"orphan", "delete"}
|
|
errors = []
|
|
seen_names = set()
|
|
|
|
for idx, topic in enumerate(topics):
|
|
prefix = f"environments.{env}.kafka.topics[{idx}]"
|
|
if not isinstance(topic, dict):
|
|
errors.append(f"{prefix}: must be an object")
|
|
continue
|
|
|
|
name = topic.get("name")
|
|
owner = topic.get("owner")
|
|
cluster_ref = topic.get("clusterRef")
|
|
|
|
if not name:
|
|
errors.append(f"{prefix}.name is required")
|
|
elif name in seen_names:
|
|
errors.append(f"{prefix}.name '{name}' is duplicated")
|
|
else:
|
|
seen_names.add(name)
|
|
|
|
if not owner:
|
|
errors.append(f"{prefix}.owner is required")
|
|
if not cluster_ref:
|
|
errors.append(f"{prefix}.clusterRef is required")
|
|
continue
|
|
if cluster_ref not in cluster_refs:
|
|
errors.append(f"{prefix}.clusterRef '{cluster_ref}' is not defined in infrastructure.yaml for env '{env}'")
|
|
continue
|
|
|
|
cluster = cluster_refs[cluster_ref] or {}
|
|
partitions = env_value(topic.get("partitions"), env, cluster.get("default_partitions"))
|
|
replication = env_value(topic.get("replicationFactor"), env, cluster.get("default_replication_factor"))
|
|
|
|
if partitions is None:
|
|
errors.append(f"{prefix}.partitions is required or cluster default_partitions must be set")
|
|
if replication is None:
|
|
errors.append(f"{prefix}.replicationFactor is required or cluster default_replication_factor must be set")
|
|
|
|
try:
|
|
partitions_num = int(partitions)
|
|
if partitions_num < 1:
|
|
errors.append(f"{prefix}.partitions must be >= 1")
|
|
except Exception:
|
|
errors.append(f"{prefix}.partitions must be a number")
|
|
|
|
try:
|
|
replication_num = int(replication)
|
|
if replication_num < 1:
|
|
errors.append(f"{prefix}.replicationFactor must be >= 1")
|
|
except Exception:
|
|
replication_num = None
|
|
errors.append(f"{prefix}.replicationFactor must be a number")
|
|
|
|
max_replication = cluster.get("max_replication_factor")
|
|
if replication_num is not None and max_replication is not None and replication_num > int(max_replication):
|
|
errors.append(f"{prefix}.replicationFactor={replication_num} exceeds cluster max_replication_factor={max_replication}")
|
|
|
|
deletion_policy = topic.get("deletionPolicy", "orphan")
|
|
if deletion_policy not in allowed_deletion:
|
|
errors.append(f"{prefix}.deletionPolicy must be one of {sorted(allowed_deletion)}")
|
|
if deletion_policy == "delete" and not bool(policy.get("allow_delete", False)):
|
|
errors.append(f"{prefix}.deletionPolicy=delete is forbidden by kafka_policy.allow_delete=false")
|
|
|
|
merged_config = {}
|
|
merged_config.update(cluster.get("default_topic_config") or {})
|
|
merged_config.update(topic.get("config") or {})
|
|
|
|
cleanup_policy = env_value(merged_config.get("cleanup.policy"), env)
|
|
if cleanup_policy is not None and cleanup_policy not in allowed_cleanup:
|
|
errors.append(f"{prefix}.config.cleanup.policy must be one of {sorted(allowed_cleanup)}")
|
|
|
|
min_isr = env_value(merged_config.get("min.insync.replicas"), env)
|
|
if min_isr is not None and replication_num is not None:
|
|
try:
|
|
min_isr_num = int(min_isr)
|
|
if min_isr_num > replication_num:
|
|
errors.append(f"{prefix}.config.min.insync.replicas={min_isr_num} is greater than replicationFactor={replication_num}")
|
|
except Exception:
|
|
errors.append(f"{prefix}.config.min.insync.replicas must be a number")
|
|
|
|
if errors:
|
|
raise SystemExit("Kafka topics validation failed:\n- " + "\n- ".join(errors))
|
|
|
|
print(f"Kafka topics declaration is valid: {infra_path} env={env} topics={len(topics)}")
|
|
PY
|