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