mirror of
https://gitlab.sarex.io/infra/terraform-contour-mirror.git
synced 2026-08-08 21:21:34 +03:00
add prod kafka topics flow
This commit is contained in:
parent
e6c769c26a
commit
8540c3907e
1
.gitignore
vendored
1
.gitignore
vendored
@ -20,6 +20,7 @@ terragrunt.hcl.backup
|
|||||||
|
|
||||||
# Generated files
|
# Generated files
|
||||||
.gitlab-ci.generated.yml
|
.gitlab-ci.generated.yml
|
||||||
|
.generated/
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
@ -25,6 +25,7 @@ generate-pipeline:
|
|||||||
- .gitlab-ci.generated.yml
|
- .gitlab-ci.generated.yml
|
||||||
expire_in: 1 hour
|
expire_in: 1 hour
|
||||||
rules:
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "pipeline"'
|
||||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
- if: '$CI_COMMIT_BRANCH == "stage"'
|
- if: '$CI_COMMIT_BRANCH == "stage"'
|
||||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||||
@ -41,6 +42,9 @@ trigger-downstream-prod:
|
|||||||
- artifact: .gitlab-ci.generated.yml
|
- artifact: .gitlab-ci.generated.yml
|
||||||
job: generate-pipeline
|
job: generate-pipeline
|
||||||
strategy: depend
|
strategy: depend
|
||||||
|
forward:
|
||||||
|
pipeline_variables: true
|
||||||
|
yaml_variables: true
|
||||||
variables:
|
variables:
|
||||||
TERRAFORM_IMAGE_NAME: cr.yandex/crp3ccidau046kdj8g9q/terraform/terragrunt:v9.10
|
TERRAFORM_IMAGE_NAME: cr.yandex/crp3ccidau046kdj8g9q/terraform/terragrunt:v9.10
|
||||||
YC_SERVICE_ACCOUNT_KEY_FILE: ${YC_PROD_SERVICE_ACCOUNT_KEY_FILE}
|
YC_SERVICE_ACCOUNT_KEY_FILE: ${YC_PROD_SERVICE_ACCOUNT_KEY_FILE}
|
||||||
@ -55,6 +59,7 @@ trigger-downstream-prod:
|
|||||||
AWS_SECRET_ACCESS_KEY: ${S3_PROD_SECRET_KEY}
|
AWS_SECRET_ACCESS_KEY: ${S3_PROD_SECRET_KEY}
|
||||||
TARGET_ENV: "prod"
|
TARGET_ENV: "prod"
|
||||||
rules:
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "pipeline"'
|
||||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||||
- if: '$CI_COMMIT_BRANCH =~ /^feature\/.*/'
|
- if: '$CI_COMMIT_BRANCH =~ /^feature\/.*/'
|
||||||
|
|||||||
@ -1,5 +1,22 @@
|
|||||||
environments:
|
environments:
|
||||||
prod:
|
prod:
|
||||||
|
kafka_cluster_refs:
|
||||||
|
prod:
|
||||||
|
cluster_id: c9qks74g084g1419cpts
|
||||||
|
default_partitions: 3
|
||||||
|
default_replication_factor: 3
|
||||||
|
max_replication_factor: 3
|
||||||
|
default_topic_config:
|
||||||
|
cleanup.policy: delete
|
||||||
|
retention.ms: "604800000"
|
||||||
|
min.insync.replicas: "2"
|
||||||
|
|
||||||
|
kafka_policy:
|
||||||
|
allow_create: true
|
||||||
|
allow_delete: false
|
||||||
|
allow_partition_increase: true
|
||||||
|
allow_partition_decrease: false
|
||||||
|
|
||||||
namespaces:
|
namespaces:
|
||||||
- name: pulse
|
- name: pulse
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
73
live/prod/kafka-topics/terragrunt.hcl
Normal file
73
live/prod/kafka-topics/terragrunt.hcl
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
include "root" {
|
||||||
|
path = find_in_parent_folders()
|
||||||
|
}
|
||||||
|
|
||||||
|
include "env" {
|
||||||
|
path = find_in_parent_folders("env.hcl")
|
||||||
|
expose = true
|
||||||
|
merge_strategy = "deep"
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
repo_root = try(get_repo_root(), "${get_terragrunt_dir()}/../../..")
|
||||||
|
infra_config = yamldecode(file("${local.repo_root}/infrastructure.yaml"))
|
||||||
|
env_name = basename(dirname(get_terragrunt_dir()))
|
||||||
|
env_config = local.infra_config.environments[local.env_name]
|
||||||
|
|
||||||
|
manifest_repo = get_env("TOPIC_MANIFEST_REPO", "local")
|
||||||
|
manifest_path = get_env("TOPIC_MANIFEST_PATH", "")
|
||||||
|
manifest_slug = replace(replace(replace(replace("${local.manifest_repo}:${local.manifest_path}", "/", "__"), ":", "__"), ".", "_"), " ", "_")
|
||||||
|
|
||||||
|
topic_manifest_path = get_env(
|
||||||
|
"TOPIC_MANIFEST_LOCAL_PATH",
|
||||||
|
"${local.repo_root}/.generated/kafka-topics/${local.env_name}/manifest.yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
kafka_cluster_refs = {
|
||||||
|
for ref, cfg in local.env_config.kafka_cluster_refs : ref => merge(cfg, {
|
||||||
|
cluster_id = startswith(tostring(cfg.cluster_id), "env:") ? get_env(trimprefix(tostring(cfg.cluster_id), "env:"), "") : tostring(cfg.cluster_id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
terraform {
|
||||||
|
source = "${get_terragrunt_dir()}/../../../modules//kafka-topics-yc"
|
||||||
|
}
|
||||||
|
|
||||||
|
remote_state {
|
||||||
|
backend = "s3"
|
||||||
|
generate = {
|
||||||
|
path = "backend.tf"
|
||||||
|
if_exists = "overwrite_terragrunt"
|
||||||
|
}
|
||||||
|
config = {
|
||||||
|
endpoint = "https://storage.yandexcloud.net"
|
||||||
|
bucket = get_env("S3_STATE_BUCKET", get_env("S3_PROD_STATE_BUCKET", "tfstate-terragrunt-prod"))
|
||||||
|
key = "live/${local.env_name}/kafka-topics/${local.manifest_slug}/terraform.tfstate"
|
||||||
|
region = "ru-central1"
|
||||||
|
access_key = get_env("S3_ACCESS_KEY", get_env("AWS_ACCESS_KEY_ID", ""))
|
||||||
|
secret_key = get_env("S3_SECRET_KEY", get_env("AWS_SECRET_ACCESS_KEY", ""))
|
||||||
|
skip_region_validation = true
|
||||||
|
skip_credentials_validation = true
|
||||||
|
skip_metadata_api_check = true
|
||||||
|
force_path_style = true
|
||||||
|
skip_bucket_root_access = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
environment = local.env_name
|
||||||
|
topic_manifest_path = local.topic_manifest_path
|
||||||
|
kafka_cluster_refs = local.kafka_cluster_refs
|
||||||
|
kafka_policy = local.env_config.kafka_policy
|
||||||
|
create_users = true
|
||||||
|
default_user_roles = ["ACCESS_ROLE_PRODUCER", "ACCESS_ROLE_CONSUMER"]
|
||||||
|
user_password_length = 32
|
||||||
|
|
||||||
|
yc_token = get_env("YC_TOKEN", "")
|
||||||
|
yc_service_account_key_file = get_env("YC_SERVICE_ACCOUNT_KEY_FILE", "")
|
||||||
|
yc_cloud_id = get_env("YC_CLOUD_ID", "")
|
||||||
|
yc_folder_id = get_env("YC_PROD_FOLDER_ID", get_env("YC_FOLDER_ID", ""))
|
||||||
|
kubeconfig_path = get_env("KUBECONFIG", "")
|
||||||
|
kube_context = get_env("KUBE_CONTEXT", "")
|
||||||
|
}
|
||||||
@ -30,7 +30,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
yandex = {
|
yandex = {
|
||||||
source = "yandex-cloud/yandex"
|
source = "yandex-cloud/yandex"
|
||||||
version = "~> 0.100"
|
version = ">= 0.161.0"
|
||||||
}
|
}
|
||||||
kubernetes = {
|
kubernetes = {
|
||||||
source = "hashicorp/kubernetes"
|
source = "hashicorp/kubernetes"
|
||||||
|
|||||||
128
modules/kafka-topics-yc/main.tf
Normal file
128
modules/kafka-topics-yc/main.tf
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
locals {
|
||||||
|
manifest = yamldecode(file(var.topic_manifest_path))
|
||||||
|
|
||||||
|
raw_topics = try(local.manifest.kafka.topics, [])
|
||||||
|
|
||||||
|
topics = {
|
||||||
|
for topic in local.raw_topics : topic.name => {
|
||||||
|
name = topic.name
|
||||||
|
owner = topic.owner
|
||||||
|
cluster_ref = topic.clusterRef
|
||||||
|
cluster = var.kafka_cluster_refs[topic.clusterRef]
|
||||||
|
|
||||||
|
partitions = try(
|
||||||
|
tonumber(topic.partitions[var.environment]),
|
||||||
|
try(tonumber(topic.partitions._default), try(tonumber(topic.partitions), var.kafka_cluster_refs[topic.clusterRef].default_partitions))
|
||||||
|
)
|
||||||
|
|
||||||
|
replication_factor = try(
|
||||||
|
tonumber(topic.replicationFactor[var.environment]),
|
||||||
|
try(tonumber(topic.replicationFactor._default), try(tonumber(topic.replicationFactor), var.kafka_cluster_refs[topic.clusterRef].default_replication_factor))
|
||||||
|
)
|
||||||
|
|
||||||
|
config = {
|
||||||
|
for key, value in merge(
|
||||||
|
try(var.kafka_cluster_refs[topic.clusterRef].default_topic_config, {}),
|
||||||
|
try(topic.config, {})
|
||||||
|
) : key => try(tostring(value[var.environment]), try(tostring(value._default), tostring(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
deletion_policy = try(topic.deletionPolicy, "orphan")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
owner_clusters = {
|
||||||
|
for item in distinct([
|
||||||
|
for topic in values(local.topics) : "${topic.cluster_ref}:${topic.owner}"
|
||||||
|
]) : item => {
|
||||||
|
cluster_ref = split(":", item)[0]
|
||||||
|
owner = split(":", item)[1]
|
||||||
|
cluster_id = var.kafka_cluster_refs[split(":", item)[0]].cluster_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
owner_permissions = {
|
||||||
|
for owner_key, owner in local.owner_clusters : owner_key => flatten([
|
||||||
|
for topic in values(local.topics) : [
|
||||||
|
for role in var.default_user_roles : {
|
||||||
|
topic_name = topic.name
|
||||||
|
role = role
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if topic.cluster_ref == owner.cluster_ref && topic.owner == owner.owner
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "random_password" "kafka_user" {
|
||||||
|
for_each = var.create_users ? local.owner_clusters : {}
|
||||||
|
|
||||||
|
length = var.user_password_length
|
||||||
|
special = false
|
||||||
|
upper = true
|
||||||
|
lower = true
|
||||||
|
numeric = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "yandex_mdb_kafka_topic" "this" {
|
||||||
|
for_each = local.topics
|
||||||
|
|
||||||
|
cluster_id = each.value.cluster.cluster_id
|
||||||
|
name = each.value.name
|
||||||
|
partitions = each.value.partitions
|
||||||
|
replication_factor = each.value.replication_factor
|
||||||
|
|
||||||
|
dynamic "topic_config" {
|
||||||
|
for_each = length(each.value.config) == 0 ? [] : [each.value.config]
|
||||||
|
content {
|
||||||
|
cleanup_policy = (
|
||||||
|
try(topic_config.value["cleanup.policy"], null) == "delete" ? "CLEANUP_POLICY_DELETE" :
|
||||||
|
try(topic_config.value["cleanup.policy"], null) == "compact" ? "CLEANUP_POLICY_COMPACT" :
|
||||||
|
null
|
||||||
|
)
|
||||||
|
compression_type = try(topic_config.value["compression.type"], null)
|
||||||
|
min_insync_replicas = try(
|
||||||
|
tonumber(topic_config.value["min.insync.replicas"]),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
retention_bytes = try(
|
||||||
|
tonumber(topic_config.value["retention.bytes"]),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
retention_ms = try(
|
||||||
|
tonumber(topic_config.value["retention.ms"]),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
segment_bytes = try(
|
||||||
|
tonumber(topic_config.value["segment.bytes"]),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
prevent_destroy = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "yandex_mdb_kafka_user" "this" {
|
||||||
|
for_each = var.create_users ? local.owner_clusters : {}
|
||||||
|
|
||||||
|
cluster_id = each.value.cluster_id
|
||||||
|
name = each.value.owner
|
||||||
|
password = random_password.kafka_user[each.key].result
|
||||||
|
|
||||||
|
dynamic "permission" {
|
||||||
|
for_each = try(local.owner_permissions[each.key], [])
|
||||||
|
content {
|
||||||
|
topic_name = permission.value.topic_name
|
||||||
|
role = permission.value.role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifecycle {
|
||||||
|
prevent_destroy = true
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [yandex_mdb_kafka_topic.this]
|
||||||
|
}
|
||||||
9
modules/kafka-topics-yc/outputs.tf
Normal file
9
modules/kafka-topics-yc/outputs.tf
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
output "topic_names" {
|
||||||
|
description = "Kafka topics managed by this service manifest state."
|
||||||
|
value = keys(yandex_mdb_kafka_topic.this)
|
||||||
|
}
|
||||||
|
|
||||||
|
output "user_names" {
|
||||||
|
description = "Kafka users managed by this service manifest state."
|
||||||
|
value = [for user in yandex_mdb_kafka_user.this : user.name]
|
||||||
|
}
|
||||||
49
modules/kafka-topics-yc/variables.tf
Normal file
49
modules/kafka-topics-yc/variables.tf
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
variable "environment" {
|
||||||
|
description = "Target environment name, for example stage/preprod/prod."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "topic_manifest_path" {
|
||||||
|
description = "Local path to service-owned Kafka topics manifest."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "kafka_cluster_refs" {
|
||||||
|
description = "Environment clusterRef mapping from infrastructure.yaml."
|
||||||
|
type = map(object({
|
||||||
|
cluster_id = string
|
||||||
|
default_partitions = optional(number, 3)
|
||||||
|
default_replication_factor = optional(number, 1)
|
||||||
|
max_replication_factor = optional(number, 1)
|
||||||
|
default_topic_config = optional(map(string), {})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "kafka_policy" {
|
||||||
|
description = "Platform policy for service-owned Kafka declarations."
|
||||||
|
type = object({
|
||||||
|
allow_create = optional(bool, true)
|
||||||
|
allow_delete = optional(bool, false)
|
||||||
|
allow_partition_increase = optional(bool, true)
|
||||||
|
allow_partition_decrease = optional(bool, false)
|
||||||
|
})
|
||||||
|
default = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "create_users" {
|
||||||
|
description = "Create one Kafka user per topic owner."
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "default_user_roles" {
|
||||||
|
description = "Kafka roles granted to generated owner users on their topics."
|
||||||
|
type = list(string)
|
||||||
|
default = ["ACCESS_ROLE_PRODUCER", "ACCESS_ROLE_CONSUMER"]
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "user_password_length" {
|
||||||
|
description = "Generated Kafka user password length."
|
||||||
|
type = number
|
||||||
|
default = 32
|
||||||
|
}
|
||||||
3
modules/kafka-topics-yc/versions.tf
Normal file
3
modules/kafka-topics-yc/versions.tf
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.5.0"
|
||||||
|
}
|
||||||
52
scripts/fetch-topic-manifest.sh
Executable file
52
scripts/fetch-topic-manifest.sh
Executable file
@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
env_name="${TOPIC_ENV:-${ENVIRONMENT:-stage}}"
|
||||||
|
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||||
|
output_path="${TOPIC_MANIFEST_LOCAL_PATH:-${repo_root}/.generated/kafka-topics/${env_name}/manifest.yaml}"
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "${output_path}")"
|
||||||
|
|
||||||
|
if [[ -z "${TOPIC_MANIFEST_PATH:-}" ]]; then
|
||||||
|
echo "TOPIC_MANIFEST_PATH is required for kafka-topics component" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${TOPIC_MANIFEST_REPO:-}" ]]; then
|
||||||
|
if [[ -f "${TOPIC_MANIFEST_PATH}" ]]; then
|
||||||
|
cp "${TOPIC_MANIFEST_PATH}" "${output_path}"
|
||||||
|
echo "Copied local topic manifest to ${output_path}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "TOPIC_MANIFEST_REPO is required when ${TOPIC_MANIFEST_PATH} is not local" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${TOPIC_MANIFEST_SHA:-}" ]]; then
|
||||||
|
echo "TOPIC_MANIFEST_SHA is required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
server_url="${CI_SERVER_URL:-https://gitlab.sarex.io}"
|
||||||
|
encoded_path="${TOPIC_MANIFEST_PATH#/}"
|
||||||
|
urlencode() {
|
||||||
|
python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded_project="$(urlencode "${TOPIC_MANIFEST_REPO}")"
|
||||||
|
encoded_file="$(urlencode "${encoded_path}")"
|
||||||
|
encoded_ref="$(urlencode "${TOPIC_MANIFEST_SHA}")"
|
||||||
|
manifest_url="${server_url}/api/v4/projects/${encoded_project}/repository/files/${encoded_file}/raw?ref=${encoded_ref}"
|
||||||
|
|
||||||
|
curl_args=(-fsSL)
|
||||||
|
if [[ -n "${CI_JOB_TOKEN:-}" ]]; then
|
||||||
|
curl_args+=(-H "JOB-TOKEN: ${CI_JOB_TOKEN}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl "${curl_args[@]}" "${manifest_url}" -o "${output_path}"
|
||||||
|
if grep -qiE '^\s*<!doctype html|^\s*<html' "${output_path}"; then
|
||||||
|
echo "Downloaded topic manifest is HTML, not YAML. Check GitLab API auth for ${TOPIC_MANIFEST_REPO}/${encoded_path}@${TOPIC_MANIFEST_SHA}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Downloaded topic manifest to ${output_path}"
|
||||||
@ -47,6 +47,10 @@ find live -name "terragrunt.hcl" -not -path "*/.terragrunt-cache/*" | sort | whi
|
|||||||
- validate-${env}-database"
|
- validate-${env}-database"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
validate_before_script=" - cd \$TG_ROOT
|
||||||
|
- echo \"Working directory:\" && pwd
|
||||||
|
- ls -la"
|
||||||
|
|
||||||
case "$env" in
|
case "$env" in
|
||||||
stage) folder_var="YC_STAGE_FOLDER_ID" ;;
|
stage) folder_var="YC_STAGE_FOLDER_ID" ;;
|
||||||
prod) folder_var="YC_PROD_FOLDER_ID" ;;
|
prod) folder_var="YC_PROD_FOLDER_ID" ;;
|
||||||
@ -71,6 +75,15 @@ find live -name "terragrunt.hcl" -not -path "*/.terragrunt-cache/*" | sort | whi
|
|||||||
- if: '\$CI_PIPELINE_SOURCE == \"pipeline\" && \$TARGET_ENV == \"stage\"'"
|
- if: '\$CI_PIPELINE_SOURCE == \"pipeline\" && \$TARGET_ENV == \"stage\"'"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ "$component" = "kafka-topics" ]; then
|
||||||
|
validate_before_script=" - apk add --no-cache bash curl python3 py3-yaml >/dev/null 2>&1 || true; ./scripts/fetch-topic-manifest.sh; ./scripts/validate-kafka-topics.sh; cd \$TG_ROOT && echo \"Working directory:\" && pwd && ls -la"
|
||||||
|
branch_rules=" - if: '\$TF_COMPONENT == \"kafka-topics\" && \$TOPIC_MANIFEST_PATH != \"\"'"
|
||||||
|
else
|
||||||
|
branch_rules=" - if: '\$TF_COMPONENT == \"kafka-topics\" && \$TOPIC_MANIFEST_PATH != \"\"'
|
||||||
|
when: never
|
||||||
|
${branch_rules}"
|
||||||
|
fi
|
||||||
|
|
||||||
cat >> "$OUTPUT_FILE" << VALIDATE_JOB
|
cat >> "$OUTPUT_FILE" << VALIDATE_JOB
|
||||||
validate-${job_prefix}:
|
validate-${job_prefix}:
|
||||||
stage: validate
|
stage: validate
|
||||||
@ -82,9 +95,7 @@ ${runner_tags}
|
|||||||
YC_FOLDER_ID: "\$${folder_var}"
|
YC_FOLDER_ID: "\$${folder_var}"
|
||||||
TG_SKIP_DEPENDENCY_OUTPUTS: "true"
|
TG_SKIP_DEPENDENCY_OUTPUTS: "true"
|
||||||
before_script:
|
before_script:
|
||||||
- cd \$TG_ROOT
|
${validate_before_script}
|
||||||
- echo "Working directory:" && pwd
|
|
||||||
- ls -la
|
|
||||||
script:
|
script:
|
||||||
- terragrunt init -upgrade -reconfigure
|
- terragrunt init -upgrade -reconfigure
|
||||||
- terragrunt validate
|
- terragrunt validate
|
||||||
@ -111,6 +122,9 @@ VALIDATE_JOB
|
|||||||
# Для secrets инициализируем зависимости чтобы получить их outputs
|
# Для secrets инициализируем зависимости чтобы получить их outputs
|
||||||
plan_before_script="cd live/${env}/namespace && terragrunt init -upgrade -reconfigure && cd ../s3 && terragrunt init -upgrade -reconfigure && cd ../database && terragrunt init -upgrade -reconfigure && cd ../secrets && ls -la"
|
plan_before_script="cd live/${env}/namespace && terragrunt init -upgrade -reconfigure && cd ../s3 && terragrunt init -upgrade -reconfigure && cd ../database && terragrunt init -upgrade -reconfigure && cd ../secrets && ls -la"
|
||||||
fi
|
fi
|
||||||
|
if [ "$component" = "kafka-topics" ]; then
|
||||||
|
plan_before_script="apk add --no-cache bash curl python3 py3-yaml >/dev/null 2>&1 || true; ./scripts/fetch-topic-manifest.sh; ./scripts/validate-kafka-topics.sh; cd \$TG_ROOT && ls -la"
|
||||||
|
fi
|
||||||
|
|
||||||
cat >> "$OUTPUT_FILE" << PLAN_JOB
|
cat >> "$OUTPUT_FILE" << PLAN_JOB
|
||||||
plan-${job_prefix}:
|
plan-${job_prefix}:
|
||||||
@ -155,13 +169,24 @@ PLAN_JOB
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Apply rules в зависимости от окружения
|
# Apply rules в зависимости от окружения
|
||||||
if [ "$env" = "prod" ]; then
|
if [ "$component" = "kafka-topics" ]; then
|
||||||
apply_rules=" - if: '\$CI_COMMIT_BRANCH == \"master\"'
|
apply_before_script="apk add --no-cache bash curl python3 py3-yaml >/dev/null 2>&1 || true; ./scripts/fetch-topic-manifest.sh; ./scripts/validate-kafka-topics.sh; cd \$TG_ROOT && ls -la"
|
||||||
|
apply_rules=" - if: '\$TF_COMPONENT == \"kafka-topics\" && \$TOPIC_MANIFEST_PATH != \"\" && \$TOPIC_APPLY == \"false\"'
|
||||||
|
when: never
|
||||||
|
- if: '\$TF_COMPONENT == \"kafka-topics\" && \$TOPIC_MANIFEST_PATH != \"\" && \$TOPIC_APPLY != \"false\"'
|
||||||
|
when: on_success
|
||||||
|
- when: never"
|
||||||
|
elif [ "$env" = "prod" ]; then
|
||||||
|
apply_rules=" - if: '\$TF_COMPONENT == \"kafka-topics\" && \$TOPIC_MANIFEST_PATH != \"\"'
|
||||||
|
when: never
|
||||||
|
- if: '\$CI_COMMIT_BRANCH == \"master\"'
|
||||||
when: ${when_clause}
|
when: ${when_clause}
|
||||||
- if: '\$CI_PIPELINE_SOURCE == \"pipeline\" && \$TARGET_ENV == \"prod\"'
|
- if: '\$CI_PIPELINE_SOURCE == \"pipeline\" && \$TARGET_ENV == \"prod\"'
|
||||||
when: ${when_clause}"
|
when: ${when_clause}"
|
||||||
else
|
else
|
||||||
apply_rules=" - if: '\$CI_COMMIT_BRANCH == \"stage\"'
|
apply_rules=" - if: '\$TF_COMPONENT == \"kafka-topics\" && \$TOPIC_MANIFEST_PATH != \"\"'
|
||||||
|
when: never
|
||||||
|
- if: '\$CI_COMMIT_BRANCH == \"stage\"'
|
||||||
when: ${when_clause}
|
when: ${when_clause}
|
||||||
- if: '\$CI_PIPELINE_SOURCE == \"pipeline\" && \$TARGET_ENV == \"stage\"'
|
- if: '\$CI_PIPELINE_SOURCE == \"pipeline\" && \$TARGET_ENV == \"stage\"'
|
||||||
when: ${when_clause}"
|
when: ${when_clause}"
|
||||||
|
|||||||
131
scripts/validate-kafka-topics.sh
Executable file
131
scripts/validate-kafka-topics.sh
Executable file
@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||||
|
env_name="${TOPIC_ENV:-${ENVIRONMENT:-stage}}"
|
||||||
|
manifest_path="${TOPIC_MANIFEST_LOCAL_PATH:-${repo_root}/.generated/kafka-topics/${env_name}/manifest.yaml}"
|
||||||
|
infra_path="${INFRASTRUCTURE_CONFIG_PATH:-${repo_root}/infrastructure.yaml}"
|
||||||
|
|
||||||
|
python3 - "${manifest_path}" "${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}")
|
||||||
|
|
||||||
|
manifest_path, infra_path, env = sys.argv[1:4]
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
manifest = load_yaml(manifest_path)
|
||||||
|
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 = ((manifest.get("kafka") or {}).get("topics"))
|
||||||
|
|
||||||
|
if not isinstance(topics, list):
|
||||||
|
raise SystemExit("kafka.topics must be an array")
|
||||||
|
|
||||||
|
allowed_cleanup = {"delete", "compact"}
|
||||||
|
allowed_deletion = {"orphan", "delete"}
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for idx, topic in enumerate(topics):
|
||||||
|
prefix = f"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")
|
||||||
|
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 manifest is valid: {manifest_path}")
|
||||||
|
PY
|
||||||
Loading…
Reference in New Issue
Block a user