kcreate-scaffolding-gcp-script 0.0.0-stage → 0.2.1

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.
@@ -0,0 +1,596 @@
1
+ #!/usr/bin/env bash
2
+ # Etapa: Construcción.
3
+ # Genera live/<NEW_ENV>/ completo a partir de live/<SOURCE_ENV>/, usando
4
+ # solo sustituciones de texto deterministas (nunca una edición "a mano" por
5
+ # el agente). Cada paso se registra con github-proyect-tracking para poder
6
+ # reanudar si el proceso se interrumpe — pero el tracking es un plus, no un
7
+ # requisito: si falla, el build sigue y queda solo sin reanudación remota
8
+ # (ver Review V1, E-6/E-8).
9
+ #
10
+ # Uso: 10_build_components.sh <NEW_ENV>
11
+ #
12
+ # Termina con exactamente uno de estos mensajes, para que el agente no
13
+ # tenga que interpretar nada:
14
+ # "BUILD_OK: <ruta>" (exit 0)
15
+ # "BUILD_FAILED: <paso> - <detalle>" (exit 1)
16
+ #
17
+ # Es seguro reintentar: cada build_component_* copia SIEMPRE desde
18
+ # SOURCE_DIR (nunca incrementa sobre lo que ya había en NEW_DIR), así que
19
+ # volver a correr este script después de un fallo a medias reconstruye
20
+ # cada componente desde cero con el mismo resultado — no hace falta un
21
+ # motor de reanudación por paso (Review V1, E-1/faltante #1: la versión
22
+ # anterior SÍ moría en el reintento porque un `mkdir -p "$NEW_DIR"` previo
23
+ # dejaba el directorio creado, y una guarda posterior lo trataba como "ya
24
+ # existe, no tocar". Esa guarda se quitó: ya no es necesaria).
25
+
26
+ set -uo pipefail # sin -e: queremos capturar el fallo de un paso y reportarlo via tracking antes de salir
27
+
28
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
29
+ # shellcheck source=lib/common.sh
30
+ source "$SCRIPT_DIR/lib/common.sh"
31
+
32
+ ksc_require_cmd python3 git curl sed
33
+
34
+ NEW_ENV="${1:-}"
35
+ [[ -n "$NEW_ENV" ]] || ksc_die "uso: 10_build_components.sh <NEW_ENV>"
36
+
37
+ REPO_ROOT="$(ksc_repo_root)"
38
+ CONFIG_FILE="$(ksc_config_path "$REPO_ROOT" "$NEW_ENV")"
39
+ [[ -f "$CONFIG_FILE" ]] || ksc_die "no existe $CONFIG_FILE — corre primero 01_init_config.sh y 02_print_config.sh"
40
+ PENDING=$(ksc_config_pending_fields "$CONFIG_FILE" || true)
41
+ [[ -z "$PENDING" ]] || ksc_die "el config todavía tiene campos __PENDIENTE__ — no se construye nada hasta que esté completo y confirmado (02_print_config.sh)."
42
+
43
+ # shellcheck disable=SC1090
44
+ source "$CONFIG_FILE"
45
+
46
+ SOURCE_DIR="$REPO_ROOT/live/$SOURCE_ENV"
47
+ NEW_DIR="$REPO_ROOT/live/$NEW_ENV"
48
+ [[ -d "$NEW_DIR" ]] && ksc_info "$NEW_DIR ya existe (reintento) — se reconstruye cada componente desde $SOURCE_ENV, no se acumula sobre lo que ya había."
49
+
50
+ read -r APP_SOURCE ENTORNO_SOURCE < <(ksc_split_env "$SOURCE_ENV")
51
+ read -r APP_NEW ENTORNO_NEW < <(ksc_split_env "$NEW_ENV")
52
+ APP_NEW_CAP="$(ksc_capitalize "$APP_NEW")"
53
+
54
+ CIDR_FILE="$(ksc_cidr_file_path "$REPO_ROOT" "$NEW_ENV")"
55
+ PLAN_FILE="$(dirname "$CONFIG_FILE")/${NEW_ENV}.plan.json"
56
+ # Archivo propio de la Construcción, SEPARADO del del doctor
57
+ # (ksc_warnings_file_path, que llena 02_print_config.sh) — Review V3
58
+ # (P-3): compartir un solo archivo en modo append hacía que cada
59
+ # reintento de este script repitiera sus propios WARN sobre los de la
60
+ # corrida anterior, inflando el archivo sin límite. Este SÍ se trunca al
61
+ # inicio de cada corrida (no repite sus propios WARN en un retry); el
62
+ # del doctor nunca se toca aquí, así que sigue intacto entre 02 y 10.
63
+ DOCTOR_WARNINGS_FILE="$(ksc_warnings_file_path "$REPO_ROOT" "$NEW_ENV")"
64
+ export KSC_WARNINGS_FILE="$(ksc_build_warnings_file_path "$REPO_ROOT" "$NEW_ENV")"
65
+ : > "$KSC_WARNINGS_FILE"
66
+
67
+ # (El "doctor" de SOURCE_ENV ya corrió en 02_print_config.sh, antes de que
68
+ # el humano confirmara — no se repite aquí.)
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Qué componentes se construyen: la base (intersección real de todos los
72
+ # entornos de live/, sin lista hardcodeada) + lo que EXTRA_COMPONENTS del
73
+ # config pida explícitamente. Antes se copiaba TODO lo que SOURCE_ENV
74
+ # tuviera, incluyendo opcionales (cloud_armor, secrets, etc.) — Review V1
75
+ # señaló que la guía (§1.2) pide no incluirlos salvo pedido explícito.
76
+ # ---------------------------------------------------------------------------
77
+ mapfile -t SOURCE_COMPONENTS < <(ksc_discover_components "$SOURCE_DIR")
78
+ [[ ${#SOURCE_COMPONENTS[@]} -gt 0 ]] || ksc_die "no se encontró ningún componente (subcarpeta con terragrunt.hcl) en $SOURCE_DIR"
79
+
80
+ mapfile -t BASE_COMPONENTS < <(ksc_base_components "$REPO_ROOT" "$NEW_ENV")
81
+ IFS=';' read -ra EXTRA_LIST <<<"${EXTRA_COMPONENTS:-}"
82
+
83
+ COMPONENTS=()
84
+ EXCLUDED=()
85
+ for c in "${SOURCE_COMPONENTS[@]}"; do
86
+ is_base=0; is_extra=0
87
+ for b in "${BASE_COMPONENTS[@]}"; do [[ "$c" == "$b" ]] && is_base=1 && break; done
88
+ for x in "${EXTRA_LIST[@]}"; do [[ -n "$x" && "$c" == "$x" ]] && is_extra=1 && break; done
89
+ if [[ "$is_base" -eq 1 || "$is_extra" -eq 1 ]]; then
90
+ COMPONENTS+=("$c")
91
+ else
92
+ EXCLUDED+=("$c")
93
+ fi
94
+ done
95
+ if [[ ${#EXCLUDED[@]} -gt 0 ]]; then
96
+ ksc_warn "componentes de $SOURCE_ENV excluidos por defecto (no son base y no están en EXTRA_COMPONENTS del config): ${EXCLUDED[*]}. Si NEW_ENV sí los necesita, agrégalos a EXTRA_COMPONENTS en $CONFIG_FILE y vuelve a correr este script."
97
+ fi
98
+
99
+ # vpc debe ir primero si existe: sql_private_access (y en principio
100
+ # cualquier otro componente que use rangos CIDR) depende del archivo que
101
+ # vpc genera, y el orden de descubrimiento es alfabético, no de
102
+ # dependencias.
103
+ if printf '%s\n' "${COMPONENTS[@]}" | grep -qx vpc; then
104
+ REORDERED=(vpc)
105
+ for c in "${COMPONENTS[@]}"; do
106
+ [[ "$c" == "vpc" ]] && continue
107
+ REORDERED+=("$c")
108
+ done
109
+ COMPONENTS=("${REORDERED[@]}")
110
+ fi
111
+
112
+ ksc_info "componentes a construir (orden): ${COMPONENTS[*]}"
113
+
114
+ mkdir -p "$NEW_DIR"
115
+
116
+ # ---------------------------------------------------------------------------
117
+ # Plan de pasos para tracking + arranque/reanudación. El tracking es
118
+ # best-effort: si falla, se avisa y se sigue sin él (Review V1 E-6/E-8 — la
119
+ # versión anterior moría si no podía inicializar tracking, aunque la
120
+ # construcción es 100% local).
121
+ # ---------------------------------------------------------------------------
122
+ {
123
+ printf '['
124
+ printf '"project.hcl",'
125
+ printf '"top-level-files",'
126
+ for c in "${COMPONENTS[@]}"; do printf '"%s",' "$c"; done
127
+ printf '"fmt"'
128
+ printf ']'
129
+ } > "$PLAN_FILE"
130
+
131
+ TRACKING_OK=1
132
+ if ! "$SCRIPT_DIR/run_tracking.sh" resume "$CONFIG_FILE" >/tmp/ksc_resume.$$ 2>&1; then
133
+ if "$SCRIPT_DIR/run_tracking.sh" init-plan "$CONFIG_FILE" "$PLAN_FILE" >/tmp/ksc_init.$$ 2>&1; then
134
+ ksc_info "tracking iniciado."
135
+ else
136
+ TRACKING_OK=0
137
+ ksc_warn "no se pudo inicializar github-proyect-tracking ($(tail -c 300 /tmp/ksc_init.$$ | tr '\n' ' ')) — la construcción sigue de todas formas, sin reanudación remota. Revisa TRACKING_GITHUB_PROJECT_URL/credenciales y vuelve a correr este script más tarde si quieres recuperar el tracking."
138
+ fi
139
+ rm -f /tmp/ksc_init.$$
140
+ else
141
+ ksc_info "reanudando ejecución existente:"
142
+ cat /tmp/ksc_resume.$$
143
+ fi
144
+ rm -f /tmp/ksc_resume.$$
145
+
146
+ step_start() {
147
+ [[ "$TRACKING_OK" -eq 1 ]] || return 0
148
+ "$SCRIPT_DIR/run_tracking.sh" step-start "$CONFIG_FILE" "$1" >/tmp/ksc_track.$$ 2>&1 \
149
+ || ksc_warn "tracking: no se pudo registrar el inicio de '$1' ($(tail -c 200 /tmp/ksc_track.$$ | tr '\n' ' ')) — el paso se ejecuta igual."
150
+ rm -f /tmp/ksc_track.$$
151
+ }
152
+ step_done() {
153
+ [[ "$TRACKING_OK" -eq 1 ]] || return 0
154
+ "$SCRIPT_DIR/run_tracking.sh" step-done "$CONFIG_FILE" "$1" >/tmp/ksc_track.$$ 2>&1 \
155
+ || ksc_warn "tracking: no se pudo registrar el fin de '$1' ($(tail -c 200 /tmp/ksc_track.$$ | tr '\n' ' ')) — el Issue puede quedar desincronizado."
156
+ rm -f /tmp/ksc_track.$$
157
+ }
158
+ step_fail() {
159
+ [[ "$TRACKING_OK" -eq 1 ]] && "$SCRIPT_DIR/run_tracking.sh" step-error "$CONFIG_FILE" "$1" "$2" >/dev/null 2>&1
160
+ echo "BUILD_FAILED: $1 - $2"
161
+ exit 1
162
+ }
163
+
164
+ ksc_copy_component_files() {
165
+ local comp="$1" f
166
+ mkdir -p "$NEW_DIR/$comp"
167
+ for f in Makefile .gitignore terragrunt.hcl; do
168
+ [[ -f "$SOURCE_DIR/$comp/$f" ]] && cp "$SOURCE_DIR/$comp/$f" "$NEW_DIR/$comp/$f"
169
+ done
170
+ # Sustitución estándar aplicada a TODO componente, conocido o no: el
171
+ # token completo del entorno, y el nombre corto de la app en CUALQUIER
172
+ # capitalización (ver ksc_substr_replace_app). Antes esto solo se hacía
173
+ # a mano en algunos componentes — Review V1 (E-5) encontró texto real
174
+ # de SOURCE_ENV filtrado en service_accounts (display_name) y
175
+ # project_iam (namespace de un principal) porque esos dos nunca
176
+ # aplicaban la sustitución de nombre de app, solo la del token
177
+ # completo. Corregido ahí; después, un bug real encontrado usando el
178
+ # skill (no en revisión) mostró que la sustitución de nombre de app
179
+ # EXISTÍA pero solo cubría dos capitalizaciones exactas (minúscula y
180
+ # "Primera-mayúscula") — un nombre de marca estilizado como "XoloShop"
181
+ # (S interna mayúscula) no hacía match con ninguna de las dos y se
182
+ # colaba completo a service_accounts.display_name/description. Ver
183
+ # lessons-learned.md, sección F.
184
+ f="$NEW_DIR/$comp/terragrunt.hcl"
185
+ [[ -f "$f" ]] || return 0
186
+ ksc_substr_replace "$f" "$SOURCE_ENV" "$NEW_ENV"
187
+ if [[ "$APP_SOURCE" != "$APP_NEW" ]]; then
188
+ ksc_substr_replace_app "$f" "$APP_SOURCE" "$APP_NEW"
189
+ fi
190
+ }
191
+
192
+ # ---------------------------------------------------------------------------
193
+ # project.hcl (no es un "componente" con terragrunt.hcl propio)
194
+ # ---------------------------------------------------------------------------
195
+ build_project_hcl() {
196
+ cp "$SOURCE_DIR/project.hcl" "$NEW_DIR/project.hcl"
197
+ local f="$NEW_DIR/project.hcl"
198
+ ksc_set_key "$f" "project_id" "$NEW_ENV"
199
+ ksc_set_key "$f" "project_number" ""
200
+ }
201
+
202
+ # Archivos a nivel raíz de live/<entorno>/ (no de un componente): un
203
+ # Makefile genérico que corre `terragrunt run --all` y un README con la
204
+ # descripción del entorno. Se detectaron ausentes incluso en xoloshop-qa
205
+ # (generado a mano en una sesión anterior) — fácil de olvidar si no queda
206
+ # como un paso explícito de la construcción. Que SOURCE_ENV no tenga
207
+ # alguno de estos 3 archivos es normal, NO un error (Review V1, E-3: la
208
+ # versión anterior fallaba la construcción completa cuando faltaba
209
+ # cualquiera de los 3, justo el caso de xoloshop-qa).
210
+ build_top_level_files() {
211
+ local f
212
+ for f in Makefile README.md .gitignore; do
213
+ if [[ -f "$SOURCE_DIR/$f" ]]; then
214
+ cp "$SOURCE_DIR/$f" "$NEW_DIR/$f" || return 1
215
+ fi
216
+ done
217
+ if [[ -f "$NEW_DIR/README.md" ]]; then
218
+ ksc_substr_replace "$NEW_DIR/README.md" "$SOURCE_ENV" "$NEW_ENV"
219
+ fi
220
+ return 0
221
+ }
222
+
223
+ # ---------------------------------------------------------------------------
224
+ # Componentes con manejo específico (ver referencias/lessons-learned.md
225
+ # para el porqué de cada uno).
226
+ # ---------------------------------------------------------------------------
227
+ build_component_project() {
228
+ ksc_copy_component_files project
229
+ local f="$NEW_DIR/project/terragrunt.hcl"
230
+ ksc_set_key "$f" "project_name" "$PROJECT_NAME"
231
+ ksc_set_key "$f" "org_id" "$ORG_ID"
232
+ ksc_set_key "$f" "org_billing_account" "$ORG_BILLING_ACCOUNT"
233
+ ksc_set_key "$f" "project" "$APP_NEW"
234
+ ksc_set_key "$f" "environment" "$ENTORNO_NEW"
235
+ ksc_set_key "$f" "owner" "$LABEL_OWNER"
236
+ }
237
+
238
+ build_component_project_services() {
239
+ ksc_copy_component_files project_services
240
+ # La lista de APIs se copia tal cual (sin editarla a mano: así se
241
+ # elimina por diseño el bug real de "coma faltante"). El doctor
242
+ # (ksc_check_source_health, corrido antes de construir) ya avisó si a
243
+ # SOURCE_ENV le falta algo que el entorno más reciente de live/ sí
244
+ # tiene — aquí no se vuelve a repetir esa lógica.
245
+ }
246
+
247
+ build_component_org_policies() {
248
+ ksc_copy_component_files org_policies
249
+ # Igual que project_services: se copia tal cual, el doctor ya avisó si
250
+ # SOURCE_ENV tiene menos entradas que el entorno más reciente de live/.
251
+ }
252
+
253
+ build_component_service_accounts() {
254
+ ksc_copy_component_files service_accounts
255
+ local f="$NEW_DIR/service_accounts/terragrunt.hcl"
256
+ local n
257
+ n="$(grep -c 'account_id[[:space:]]*=' "$f")"
258
+ if [[ "$n" -gt 5 ]]; then
259
+ ksc_warn "service_accounts de $SOURCE_ENV tiene $n entradas (más de las 5 estándar: terraform, github-manager, gke-sa-external-secrets, <app>-node-sa, <app>-bastion-sa) — revisa a mano si las SAs extra son específicas de $SOURCE_ENV y no deberían existir en $NEW_ENV."
260
+ fi
261
+ }
262
+
263
+ build_component_project_iam() {
264
+ ksc_copy_component_files project_iam
265
+ local f="$NEW_DIR/project_iam/terragrunt.hcl"
266
+ # Se ignoran líneas de comentario: xoloshop-qa real tiene un comentario
267
+ # explicando por qué NO se puede usar roles/orgpolicy.policyAdmin aquí
268
+ # (precisamente la lección que este chequeo busca aplicar) — ese
269
+ # comentario mencionaba el string textualmente y disparaba un falso
270
+ # BUILD_FAILED real al probar esto con xoloshop-qa como SOURCE_ENV.
271
+ if grep -v '^[[:space:]]*#' "$f" | grep -q "orgpolicy.policyAdmin"; then
272
+ step_fail "project_iam" "el archivo copiado de $SOURCE_ENV incluye roles/orgpolicy.policyAdmin fuera de un comentario, que GCP rechaza a nivel proyecto. Quítalo de live/$SOURCE_ENV/project_iam/terragrunt.hcl primero."
273
+ fi
274
+ # Guía §3.12: dejar un comentario EN EL ARCHIVO, no solo en la consola,
275
+ # para que una revisión futura del propio HCL no asuma que faltó por
276
+ # descuido (Review V1, faltante #12).
277
+ python3 - "$f" "$SOURCE_ENV" <<'PYEOF'
278
+ import sys
279
+ file, source_env = sys.argv[1], sys.argv[2]
280
+ marker = "# NOTA kcreate-scaffolding-gcp-script: este archivo se copió completo de\n# live/" + source_env + "/project_iam/terragrunt.hcl SIN filtrar. Revisa a mano si\n# incluye bindings especificos de esa app/equipo (dominios externos,\n# roles personalizados) que NO deberian existir aqui.\n"
281
+ with open(file) as fh:
282
+ text = fh.read()
283
+ if marker not in text:
284
+ text = text.replace("inputs = {", marker + "inputs = {", 1)
285
+ with open(file, "w") as fh:
286
+ fh.write(text)
287
+ PYEOF
288
+ ksc_warn "project_iam copiado de $SOURCE_ENV sin filtrar — revisa a mano que no haya bindings específicos del equipo/app de $SOURCE_ENV (dominios externos, roles personalizados) antes de confiar en este archivo (queda anotado también dentro del propio archivo)."
289
+ }
290
+
291
+ build_component_vpc() {
292
+ ksc_copy_component_files vpc
293
+ local f="$NEW_DIR/vpc/terragrunt.hcl"
294
+
295
+ local subnet pods svc priv
296
+ if [[ -f "$CIDR_FILE" ]]; then
297
+ # Reintento: ya se había elegido un rango en una corrida anterior —
298
+ # reusarlo en vez de recalcular (determinismo; Review V1 E-1/E-2
299
+ # mencionan la importancia de que un reintento no produzca un
300
+ # resultado distinto).
301
+ ksc_info "reusando rango CIDR ya elegido en $CIDR_FILE (de una corrida anterior)."
302
+ subnet="$(sed -n 1p "$CIDR_FILE")"; pods="$(sed -n 2p "$CIDR_FILE")"
303
+ svc="$(sed -n 3p "$CIDR_FILE")"; priv="$(sed -n 4p "$CIDR_FILE")"
304
+ else
305
+ local existing_file="/tmp/ksc_existing_cidrs.$$"
306
+ # vpc/terragrunt.hcl ya guarda sus 3 rangos en notación CIDR completa
307
+ # ("x.x.x.x/nn"). sql_private_access, en cambio, guarda `address` sin
308
+ # sufijo y `prefix_length` aparte — hay que recombinarlos, si no, el
309
+ # picker nunca ve esos /16 como ocupados y podría asignar algo que
310
+ # colisiona con un sql_private_access existente.
311
+ grep -rhoE '"[0-9]{1,3}(\.[0-9]{1,3}){3}/[0-9]{1,2}"' "$REPO_ROOT"/live/*/vpc/terragrunt.hcl 2>/dev/null \
312
+ | tr -d '"' > "$existing_file"
313
+ local sp_file addr prefix
314
+ for sp_file in "$REPO_ROOT"/live/*/sql_private_access/terragrunt.hcl; do
315
+ [[ -f "$sp_file" ]] || continue
316
+ addr="$(ksc_hcl_get "$sp_file" address)"
317
+ prefix="$(ksc_hcl_get "$sp_file" prefix_length)"
318
+ [[ -n "$addr" && -n "$prefix" ]] && echo "${addr}/${prefix}" >> "$existing_file"
319
+ done
320
+
321
+ local cidrs
322
+ cidrs="$(ksc_cidr_pick_free "$existing_file")" || step_fail "vpc" "no se encontró un rango CIDR libre (ver $existing_file)"
323
+ rm -f "$existing_file"
324
+ subnet="$(sed -n 1p <<<"$cidrs")"; pods="$(sed -n 2p <<<"$cidrs")"
325
+ svc="$(sed -n 3p <<<"$cidrs")"; priv="$(sed -n 4p <<<"$cidrs")"
326
+ printf '%s\n%s\n%s\n%s\n' "$subnet" "$pods" "$svc" "$priv" > "$CIDR_FILE"
327
+ fi
328
+ ksc_ok "rangos CIDR para $NEW_ENV: subred=$subnet pods=$pods svc=$svc sql_private_access=$priv"
329
+
330
+ ksc_replace_nth "$f" 'ip_cidr_range\s*=\s*"([^"]*)"' 1 "$subnet" || return 1
331
+ ksc_replace_nth "$f" 'ip_cidr_range\s*=\s*"([^"]*)"' 2 "$pods" || return 1
332
+ ksc_replace_nth "$f" 'ip_cidr_range\s*=\s*"([^"]*)"' 3 "$svc" || return 1
333
+ }
334
+
335
+ build_component_firewall() {
336
+ ksc_copy_component_files firewall
337
+ }
338
+
339
+ build_component_sql_private_access() {
340
+ ksc_copy_component_files sql_private_access
341
+ local f="$NEW_DIR/sql_private_access/terragrunt.hcl"
342
+ [[ -f "$CIDR_FILE" ]] || step_fail "sql_private_access" "falta $CIDR_FILE — el paso 'vpc' debe correr antes."
343
+ local priv priv_addr priv_prefix
344
+ priv="$(sed -n 4p "$CIDR_FILE")"
345
+ priv_addr="${priv%%/*}"
346
+ priv_prefix="${priv##*/}"
347
+ # El módulo guarda la dirección sin sufijo y el prefijo en un campo
348
+ # separado (ver live/*/sql_private_access/terragrunt.hcl real) — nunca
349
+ # "x.x.x.x/16" junto en un solo campo.
350
+ ksc_set_key "$f" "address" "$priv_addr"
351
+ ksc_set_key "$f" "prefix_length" "$priv_prefix"
352
+ }
353
+
354
+ build_component_sql_postgres() {
355
+ ksc_copy_component_files sql_postgres
356
+ local f="$NEW_DIR/sql_postgres/terragrunt.hcl"
357
+ ksc_set_key "$f" "name" "$APP_NEW"
358
+ ksc_set_key "$f" "label_project" "$APP_NEW"
359
+ ksc_set_key "$f" "label_environment" "$ENTORNO_NEW"
360
+ ksc_set_key "$f" "label_owner" "$LABEL_OWNER"
361
+ ksc_set_key "$f" "database_version" "$DATABASE_VERSION_NEEDS_VERIFICATION"
362
+
363
+ # Guía §3.9: deletion_protection* debe ser "true" en *-prod, confirmar
364
+ # con el humano. Antes se copiaba de SOURCE_ENV sin aviso — si SOURCE_ENV
365
+ # es qa (="false") y NEW_ENV es prod, el generado quedaba "false" en
366
+ # prod sin que nadie lo notara (Review V1, E-4/faltante #11).
367
+ if [[ "$ENTORNO_NEW" == "prod" ]]; then
368
+ local dp dpe
369
+ dp="$(ksc_hcl_get "$f" deletion_protection)"
370
+ dpe="$(ksc_hcl_get "$f" deletion_protection_enabled)"
371
+ if [[ "$dp" == "false" || "$dpe" == "false" ]]; then
372
+ ksc_warn "NEW_ENV es *-prod pero deletion_protection/deletion_protection_enabled quedaron en \"false\" (heredado de $SOURCE_ENV) — la guía pide \"true\" en prod. Confirma con el humano y corrige $f si aplica."
373
+ fi
374
+ fi
375
+
376
+ local dev_ip dev_user
377
+ dev_ip="$(ksc_fresh_public_ip)"
378
+ dev_user="$(id -un)"
379
+ local entries="${FIXED_ADMIN_NETWORKS};dev-${dev_user}=${dev_ip}/32"
380
+ ksc_set_authorized_networks "$f" "$entries" || return 1
381
+ ksc_ok "authorized_networks: fijas=[$FIXED_ADMIN_NETWORKS] + dev-${dev_user}=${dev_ip}/32 (IP obtenida ahora, no reusada)"
382
+
383
+ # dns_record_name/dns_domain_name son opcionales (algunos entornos no los
384
+ # tienen). dns_record_name sigue un patrón seguro de generar siempre
385
+ # (<app>.db, no depende de qué dominio sea dueño nadie). dns_domain_name
386
+ # SÍ codifica un dominio real registrado (ej. "boletera.ninja") — si el
387
+ # humano ya lo confirmó en DNS_DOMAIN (config), se usa ese valor
388
+ # directo, sin WARN; si no, se genera un valor por patrón y SIEMPRE se
389
+ # marca para confirmación (Review V1, mejora #7).
390
+ if grep -q '^[[:space:]]*dns_record_name[[:space:]]*=' "$f"; then
391
+ ksc_set_key "$f" "dns_record_name" "${APP_NEW}.db"
392
+ if [[ -n "${DNS_DOMAIN:-}" ]]; then
393
+ ksc_set_key "$f" "dns_domain_name" "$DNS_DOMAIN"
394
+ ksc_ok "dns_domain_name = \"$DNS_DOMAIN\" (confirmado en DNS_DOMAIN del config, no es un guess)."
395
+ else
396
+ local dns_domain_guess
397
+ if [[ "$ENTORNO_NEW" == "qa" ]]; then
398
+ dns_domain_guess="qa.${APP_NEW}.ninja."
399
+ else
400
+ dns_domain_guess="${APP_NEW}.ninja."
401
+ fi
402
+ ksc_set_key "$f" "dns_domain_name" "$dns_domain_guess"
403
+ ksc_warn "dns_domain_name de $f se generó SOLO por patrón visual ($dns_domain_guess, copiando la convención de $SOURCE_ENV) porque DNS_DOMAIN no está definido en el config. CONFIRMA con el humano que '$APP_NEW' realmente usa ese dominio antes de desplegar — si no, define DNS_DOMAIN en el config y reconstruye."
404
+ fi
405
+ fi
406
+ }
407
+
408
+ build_component_gke_private() {
409
+ ksc_copy_component_files gke_private
410
+ local f="$NEW_DIR/gke_private/terragrunt.hcl"
411
+ ksc_set_key "$f" "label_project" "$APP_NEW"
412
+ ksc_set_key "$f" "label_environment" "$ENTORNO_NEW"
413
+ ksc_set_key "$f" "label_owner" "$LABEL_OWNER"
414
+ ksc_set_key "$f" "gke_version" "$GKE_VERSION_NEEDS_VERIFICATION"
415
+ }
416
+
417
+ build_component_storage_assets() {
418
+ ksc_copy_component_files storage_assets
419
+ local f="$NEW_DIR/storage_assets/terragrunt.hcl"
420
+ ksc_set_key "$f" "label_project" "$APP_NEW"
421
+ ksc_set_key "$f" "label_environment" "$ENTORNO_NEW"
422
+ ksc_set_key "$f" "label_owner" "$LABEL_OWNER"
423
+ }
424
+
425
+ build_component_storage_docs() {
426
+ ksc_copy_component_files storage_docs
427
+ local f="$NEW_DIR/storage_docs/terragrunt.hcl"
428
+ ksc_set_key "$f" "label_project" "$APP_NEW"
429
+ ksc_set_key "$f" "label_environment" "$ENTORNO_NEW"
430
+ ksc_set_key "$f" "label_owner" "$LABEL_OWNER"
431
+ }
432
+
433
+ build_component_artifact_registry() {
434
+ # Se decide ANTES de copiar (ksc_copy_component_files ya sustituye
435
+ # APP_SOURCE->APP_NEW automáticamente): si el archivo de SOURCE_ENV ya
436
+ # era genérico (sin el nombre de la app), la sustitución automática no
437
+ # tiene nada que reemplazar y el resultado seguiría siendo genérico.
438
+ local was_generic=1
439
+ # -i: la app puede aparecer con cualquier capitalización (ver
440
+ # ksc_substr_replace_app) — un chequeo exacto-minúscula aquí marcaría
441
+ # "genérico" un archivo que en realidad sí tiene el nombre de la app
442
+ # (solo que estilizado, ej. "XoloShop"), y la sustitución automática de
443
+ # ksc_copy_component_files ya lo habría corregido de todas formas.
444
+ grep -qi "$APP_SOURCE" "$SOURCE_DIR/artifact_registry/terragrunt.hcl" && was_generic=0
445
+
446
+ ksc_copy_component_files artifact_registry
447
+
448
+ if [[ "$was_generic" -eq 1 ]]; then
449
+ local f="$NEW_DIR/artifact_registry/terragrunt.hcl"
450
+ local template_output id_tpl desc_tpl
451
+ template_output="$(ksc_learn_artifact_registry_template "$REPO_ROOT")"
452
+ id_tpl="$(sed -n '1p' <<<"$template_output")"
453
+ desc_tpl="$(sed -n '2p' <<<"$template_output")"
454
+ if [[ -n "$id_tpl" ]]; then
455
+ # Se "aprendió" el patrón de OTRO entorno real que sí tiene su
456
+ # artifact_registry específico (ej. xoloshop-qa) y se aplica con
457
+ # el nombre de la app nueva — sin hardcodear ningún literal de la
458
+ # organización en el código del skill (Review V2, mejora pendiente
459
+ # de la sección 2.3/4.3).
460
+ local new_id="${id_tpl//\{\{APP\}\}/$APP_NEW}"
461
+ local new_desc="${desc_tpl//\{\{APP_CAP\}\}/$APP_NEW_CAP}"
462
+ new_desc="${new_desc//\{\{APP\}\}/$APP_NEW}"
463
+ ksc_set_key "$f" "repository_id" "$new_id"
464
+ ksc_set_key "$f" "description" "$new_desc"
465
+ ksc_ok "artifact_registry: $SOURCE_ENV tenía valores genéricos; se generaron repository_id/description específicos aprendiendo el patrón de otro entorno real (repository_id=\"$new_id\")."
466
+ else
467
+ ksc_warn "artifact_registry de $SOURCE_ENV ya tiene repository_id/description genéricos (no contienen '$APP_SOURCE'), y ningún otro entorno de live/ tiene uno específico del que aprender el patrón. Se copió sin poder adaptar el nombre de la app: corrígelo a mano con valores específicos de $APP_NEW antes de confiar en él."
468
+ fi
469
+ fi
470
+ }
471
+
472
+ build_component_github_workload_identity() {
473
+ ksc_copy_component_files github_workload_identity
474
+ local f="$NEW_DIR/github_workload_identity/terragrunt.hcl"
475
+
476
+ # El binding de terraform se identifica por contenido
477
+ # (service_account_email que empieza con "terraform@"), nunca por
478
+ # posición — Review V1 (E-5/mejora #8) señaló que editar por posición
479
+ # (la 2.ª coincidencia de principal_set_suffix) corrompería el binding
480
+ # de infra-cloud si el orden en SOURCE_ENV fuera distinto.
481
+ #
482
+ # No todos los entornos reales tienen este binding: cashless-qa real
483
+ # solo tiene el de github-manager. Si falta, no es un error — se usa
484
+ # "infra-cloud" por defecto (el valor que la guía §3.15 pide siempre)
485
+ # y se avisa, en vez de fallar la construcción completa.
486
+ local terraform_suffix
487
+ terraform_suffix="$(python3 -c "
488
+ import re
489
+ with open('$f') as fh:
490
+ text = fh.read()
491
+ m = re.search(r'service_account_email\s*=\s*\"terraform@[^\"]*\"[^}]*principal_set_suffix\s*=\s*\"([^\"]*)\"', text, re.DOTALL)
492
+ print(m.group(1) if m else '')
493
+ ")"
494
+ if [[ -z "$terraform_suffix" ]]; then
495
+ terraform_suffix="infra-cloud"
496
+ ksc_warn "github_workload_identity de $SOURCE_ENV no tenía un binding de terraform@... — se agregó uno nuevo con suffix \"infra-cloud\" (necesario para que el CI/CD de este repo opere sobre $NEW_ENV). Confirma que ese es el repo correcto."
497
+ fi
498
+
499
+ # APP_GITHUB_REPO admite uno o varios repos separados por ';' (apps
500
+ # multi-repo, como boletera-qa con 5 — Review V1 mejora #6). El bloque
501
+ # completo se reconstruye siempre: terraform (preservando su suffix) +
502
+ # un binding github-manager por repo. Esto reemplaza por completo el
503
+ # enfoque anterior de "advertir y no tocar" cuando había más de 2
504
+ # bindings.
505
+ local repo_names="" repo entry
506
+ IFS=';' read -ra _repos <<<"$APP_GITHUB_REPO"
507
+ for entry in "${_repos[@]}"; do
508
+ [[ -z "$entry" ]] && continue
509
+ repo="${entry##*/}"
510
+ repo_names="${repo_names}${repo_names:+;}${repo}"
511
+ done
512
+ [[ -n "$repo_names" ]] || step_fail "github_workload_identity" "APP_GITHUB_REPO está vacío en el config"
513
+
514
+ ksc_set_workload_identity_bindings "$f" "$terraform_suffix" "$repo_names" || return 1
515
+
516
+ local n_repos
517
+ n_repos="$(tr ';' '\n' <<<"$repo_names" | grep -c .)"
518
+ if [[ "$n_repos" -gt 1 ]]; then
519
+ ksc_warn "github_workload_identity de $NEW_ENV quedó con $n_repos bindings github-manager (uno por repo en APP_GITHUB_REPO) — confirma que esa es la lista completa y correcta de repos de la app."
520
+ fi
521
+ }
522
+
523
+ # Fallback genérico para cualquier componente opcional sin manejo
524
+ # específico que de todas formas se incluyó (vía EXTRA_COMPONENTS).
525
+ build_component_generic() {
526
+ local comp="$1"
527
+ ksc_copy_component_files "$comp"
528
+ ksc_warn "'$comp' no tiene manejo específico en este skill (incluido vía EXTRA_COMPONENTS) — se copió con la sustitución estándar de entorno/nombre de app. Revísalo a mano antes de confiar en él."
529
+ }
530
+
531
+ # ---------------------------------------------------------------------------
532
+ # Ejecución del plan
533
+ # ---------------------------------------------------------------------------
534
+ step_start "project.hcl"
535
+ build_project_hcl || step_fail "project.hcl" "fallo copiando/editando project.hcl"
536
+ step_done "project.hcl"
537
+
538
+ step_start "top-level-files"
539
+ build_top_level_files || step_fail "top-level-files" "fallo copiando Makefile/README.md/.gitignore de nivel raíz"
540
+ step_done "top-level-files"
541
+
542
+ for comp in "${COMPONENTS[@]}"; do
543
+ step_start "$comp"
544
+ fn="build_component_${comp}"
545
+ if declare -f "$fn" >/dev/null; then
546
+ "$fn" || step_fail "$comp" "fallo en $fn"
547
+ else
548
+ build_component_generic "$comp" || step_fail "$comp" "fallo en build_component_generic"
549
+ fi
550
+ step_done "$comp"
551
+ done
552
+
553
+ step_start "fmt"
554
+ for comp in "${COMPONENTS[@]}"; do
555
+ tg="$NEW_DIR/$comp/terragrunt.hcl"
556
+ [[ -f "$tg" ]] || continue
557
+ if ! ksc_check_duplicate_keys "$tg" 2>/tmp/ksc_dupe.$$; then
558
+ ksc_warn "$(cat /tmp/ksc_dupe.$$) (heredado de SOURCE_ENV — terragrunt hcl fmt/validate no lo detectan por sí solos)"
559
+ fi
560
+ rm -f /tmp/ksc_dupe.$$
561
+ done
562
+
563
+ if command -v terragrunt >/dev/null 2>&1; then
564
+ (cd "$NEW_DIR" && terragrunt hcl fmt --working-dir . >/tmp/ksc_fmt.$$ 2>&1) \
565
+ || step_fail "fmt" "terragrunt hcl fmt falló: $(cat /tmp/ksc_fmt.$$)"
566
+ rm -f /tmp/ksc_fmt.$$
567
+
568
+ # Guía §3.17: `terragrunt hcl validate` por componente al cerrar la
569
+ # construcción (antes solo se corría `hcl fmt`; Review V1, faltante
570
+ # #10). La excepción conocida de gke_private (falso positivo sobre
571
+ # `dependency.vpc` sin remote state) se tolera; cualquier otro error de
572
+ # sintaxis real sí detiene la construcción.
573
+ for comp in "${COMPONENTS[@]}"; do
574
+ [[ -f "$NEW_DIR/$comp/terragrunt.hcl" ]] || continue
575
+ if ! (cd "$NEW_DIR/$comp" && terragrunt hcl validate >/tmp/ksc_val.$$ 2>&1); then
576
+ if grep -q 'does not have an attribute named "outputs"' /tmp/ksc_val.$$; then
577
+ ksc_info "$comp: falso positivo conocido de 'terragrunt hcl validate' sobre dependency sin remote state (ver lessons-learned.md) — ignorado."
578
+ else
579
+ step_fail "fmt" "$comp: terragrunt hcl validate falló: $(cat /tmp/ksc_val.$$)"
580
+ fi
581
+ fi
582
+ rm -f /tmp/ksc_val.$$
583
+ done
584
+ else
585
+ ksc_warn "terragrunt no está instalado en este entorno — se omite 'hcl fmt'/'hcl validate' (no es bloqueante, pero córrelos manualmente después)."
586
+ fi
587
+ step_done "fmt"
588
+
589
+ if [[ -s "$DOCTOR_WARNINGS_FILE" || -s "$KSC_WARNINGS_FILE" ]]; then
590
+ echo
591
+ echo "=== WARN acumulados (doctor: $DOCTOR_WARNINGS_FILE + construcción: $KSC_WARNINGS_FILE) ==="
592
+ [[ -s "$DOCTOR_WARNINGS_FILE" ]] && cat "$DOCTOR_WARNINGS_FILE"
593
+ [[ -s "$KSC_WARNINGS_FILE" ]] && cat "$KSC_WARNINGS_FILE"
594
+ fi
595
+
596
+ echo "BUILD_OK: $NEW_DIR"