paramsx 2.5.0__tar.gz → 2.5.2__tar.gz
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.
- {paramsx-2.5.0 → paramsx-2.5.2}/CHANGELOG.md +16 -0
- {paramsx-2.5.0/paramsx.egg-info → paramsx-2.5.2}/PKG-INFO +1 -1
- paramsx-2.5.2/paramsx/__init__.py +1 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx/web/app.py +4 -3
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx/web/ssm.py +66 -7
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx/web/static/paramsx.js +57 -17
- {paramsx-2.5.0 → paramsx-2.5.2/paramsx.egg-info}/PKG-INFO +1 -1
- {paramsx-2.5.0 → paramsx-2.5.2}/tests/test_web.py +84 -0
- paramsx-2.5.0/paramsx/__init__.py +0 -1
- {paramsx-2.5.0 → paramsx-2.5.2}/LICENSE +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/MANIFEST.in +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/README.md +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx/core/__init__.py +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx/core/config.py +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx/core/rutas.py +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx/funcions.py +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx/main.py +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx/paramsx_config.py +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx/text.py +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx/web/__init__.py +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx/web/static/ajustes.js +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx/web/static/app.css +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx/web/static/comun.js +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx/web/templates/index.html +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx.egg-info/SOURCES.txt +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx.egg-info/dependency_links.txt +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx.egg-info/entry_points.txt +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx.egg-info/requires.txt +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/paramsx.egg-info/top_level.txt +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/setup.cfg +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/setup.py +0 -0
- {paramsx-2.5.0 → paramsx-2.5.2}/tests/test_core.py +0 -0
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.5.2
|
|
4
|
+
|
|
5
|
+
### Corregido
|
|
6
|
+
|
|
7
|
+
- **Buscar un parámetro que existe ya no dice que no está**:
|
|
8
|
+
- En «Ruta» vale también el **nombre completo de un parámetro** (antes solo carpetas: «/dev/app/clave» no encontraba nada) y los parámetros **sin carpeta** (`MiParametro`).
|
|
9
|
+
- El buscador de «Ruta» sugiere también los **parámetros**, no solo las carpetas.
|
|
10
|
+
- Con **más de 2.000 parámetros** en la cuenta, el buscador y el filtro de Explorar preguntan también a AWS lo que se escribe; antes solo buscaban en los 2.000 primeros.
|
|
11
|
+
- Explorar muestra los parámetros sin carpeta.
|
|
12
|
+
|
|
13
|
+
## 2.5.1
|
|
14
|
+
|
|
15
|
+
### Corregido
|
|
16
|
+
|
|
17
|
+
- **Un borrado ya no pisa el cambio de otro**: justo antes de borrar se vuelve a leer la versión de cada parámetro (sin valores). Si alguien lo ha cambiado desde la revisión (también mientras se creaba la copia de un renombrado), no se borra y se avisa; en un renombrado, se dice que la copia se creó con el valor de antes. Si el rol no puede leer la versión, se borra como antes; si la lectura falla por otro motivo, no se borra.
|
|
18
|
+
|
|
3
19
|
## 2.5.0
|
|
4
20
|
|
|
5
21
|
### Añadido
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.5.2"
|
|
@@ -354,13 +354,14 @@ def crear_app(config_path=None, token=None, sesion_factory=None, perfiles_aws=No
|
|
|
354
354
|
|
|
355
355
|
@app.get("/api/explorar")
|
|
356
356
|
def api_explorar():
|
|
357
|
-
return jsonify(ssm_web.explorar(ssm(), request.args.get("prefijo") or "/"
|
|
357
|
+
return jsonify(ssm_web.explorar(ssm(), request.args.get("prefijo") or "/",
|
|
358
|
+
str(request.args.get("contiene") or "")[:200]))
|
|
358
359
|
|
|
359
360
|
@app.post("/api/cargar")
|
|
360
361
|
def api_cargar():
|
|
361
362
|
ruta = str(cuerpo().get("ruta") or "").strip()
|
|
362
|
-
if not ruta
|
|
363
|
-
raise ErrorWeb("
|
|
363
|
+
if not ruta:
|
|
364
|
+
raise ErrorWeb("Escribe una ruta (o el nombre de un parámetro).")
|
|
364
365
|
return jsonify(ssm_web.cargar(ssm(), ruta))
|
|
365
366
|
|
|
366
367
|
@app.post("/api/plan")
|
|
@@ -81,8 +81,11 @@ def _meta(p):
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
def describir(ssm, ruta, nombres=None):
|
|
85
|
-
"""{nombre: metadatos} (sin valores) de una ruta (recursivo)
|
|
84
|
+
def describir(ssm, ruta, nombres=None, contiene=""):
|
|
85
|
+
"""{nombre: metadatos} (sin valores) de una ruta (recursivo), de unos nombres concretos o de los
|
|
86
|
+
que llevan un texto (lo filtra AWS, distingue mayúsculas)."""
|
|
87
|
+
if contiene:
|
|
88
|
+
return _describir_filtro(ssm, [{"Key": "Name", "Option": "Contains", "Values": [contiene]}])
|
|
86
89
|
if nombres is not None:
|
|
87
90
|
out = {}
|
|
88
91
|
# El filtro Name admite hasta 50 valores
|
|
@@ -170,11 +173,34 @@ def etiquetas(ssm, nombres):
|
|
|
170
173
|
return dict(pool.map(una, nombres))
|
|
171
174
|
|
|
172
175
|
|
|
176
|
+
def _uno(ssm, nombre):
|
|
177
|
+
"""Lo escrito en «Ruta» es un parámetro, no una carpeta (o uno sin «/», que no está en
|
|
178
|
+
ninguna): (meta, valores) o None si no existe."""
|
|
179
|
+
try:
|
|
180
|
+
meta = describir(ssm, "", nombres=[nombre])
|
|
181
|
+
except ClientError as exc:
|
|
182
|
+
if not denegado(exc):
|
|
183
|
+
raise ParametrosError(motivo(exc)) from None
|
|
184
|
+
meta = None
|
|
185
|
+
try:
|
|
186
|
+
valores = valores_por_nombre(ssm, [nombre])
|
|
187
|
+
except ClientError as exc:
|
|
188
|
+
raise ParametrosError(motivo(exc)) from None
|
|
189
|
+
if not meta and valores.get(nombre, {}).get("no_existe"):
|
|
190
|
+
return None
|
|
191
|
+
return meta, valores
|
|
192
|
+
|
|
193
|
+
|
|
173
194
|
def cargar(ssm, ruta):
|
|
174
195
|
"""Los parámetros de una ruta (recursivo) con valor, metadatos y etiquetas. Lo que el rol no
|
|
175
|
-
puede leer va con `bloqueado`. ParametrosError si no puede ni listar ni leer la ruta.
|
|
176
|
-
ruta
|
|
196
|
+
puede leer va con `bloqueado`. ParametrosError si no puede ni listar ni leer la ruta. Si la
|
|
197
|
+
ruta es el nombre de un parámetro (o uno sin «/»), carga ese."""
|
|
177
198
|
avisos = []
|
|
199
|
+
if not ruta.strip().startswith("/"): # parámetro sin carpeta (p. ej. «MiParametro»)
|
|
200
|
+
nombre = ruta.strip()
|
|
201
|
+
meta, valores = _uno(ssm, nombre) or ({}, {})
|
|
202
|
+
return _montar(ssm, nombre, meta, valores, avisos)
|
|
203
|
+
ruta = "/" + ruta.strip("/") if ruta.strip("/") else "/"
|
|
178
204
|
try:
|
|
179
205
|
meta = describir(ssm, ruta)
|
|
180
206
|
except ClientError as exc:
|
|
@@ -200,6 +226,14 @@ def cargar(ssm, ruta):
|
|
|
200
226
|
# La ruta entera no se deja (o un descifrado tumba la página): uno a uno
|
|
201
227
|
valores = valores_por_nombre(ssm, sorted(meta))
|
|
202
228
|
|
|
229
|
+
if not meta and not valores and ruta != "/": # nada debajo: ¿es el nombre de un parámetro?
|
|
230
|
+
uno = _uno(ssm, ruta)
|
|
231
|
+
if uno:
|
|
232
|
+
meta, valores = uno
|
|
233
|
+
return _montar(ssm, ruta, meta, valores, avisos)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _montar(ssm, ruta, meta, valores, avisos):
|
|
203
237
|
nombres = sorted(n for n in set(meta or {}) | set(valores)
|
|
204
238
|
if not valores.get(n, {}).get("no_existe"))
|
|
205
239
|
legibles = [n for n in nombres if "bloqueado" not in valores.get(n, {})]
|
|
@@ -221,10 +255,11 @@ def cargar(ssm, ruta):
|
|
|
221
255
|
return {"ruta": ruta, "parametros": parametros, "avisos": avisos}
|
|
222
256
|
|
|
223
257
|
|
|
224
|
-
def explorar(ssm, prefijo="/"):
|
|
225
|
-
"""Nombres (sin valores) para el árbol de «Explorar» y la búsqueda de Ajustes.
|
|
258
|
+
def explorar(ssm, prefijo="/", contiene=""):
|
|
259
|
+
"""Nombres (sin valores) para el árbol de «Explorar» y la búsqueda de Ajustes. Con `contiene`,
|
|
260
|
+
solo los que llevan ese texto: así se encuentran también cuando hay más de MAX_LISTADO."""
|
|
226
261
|
try:
|
|
227
|
-
meta = describir(ssm, prefijo or "/")
|
|
262
|
+
meta = describir(ssm, prefijo or "/", contiene=contiene)
|
|
228
263
|
except ClientError as exc:
|
|
229
264
|
raise ParametrosError(
|
|
230
265
|
"Tu rol de IAM no puede listar parámetros (ssm:DescribeParameters)."
|
|
@@ -440,6 +475,30 @@ def aplicar(ssm, items, cfg):
|
|
|
440
475
|
borrar.remove(b)
|
|
441
476
|
resultados.append(b)
|
|
442
477
|
|
|
478
|
+
# Justo antes de borrar, otra vez la versión de cada uno (sin valores ni descifrar): si alguien lo ha cambiado desde
|
|
479
|
+
# la revisión (también mientras se creaban los renombrados), no se borra. SSM no tiene «borrar solo si sigue en la
|
|
480
|
+
# versión N»: entre esta lectura y el borrado queda una ventana de milisegundos que no se puede cerrar desde aquí.
|
|
481
|
+
esperada = {it.get("name"): it.get("version") for it in items if it.get("accion") == "borrar" and it.get("version")}
|
|
482
|
+
copia_de = {it.get("de"): it.get("name") for it in items if it.get("accion") == "nuevo" and it.get("de")}
|
|
483
|
+
try:
|
|
484
|
+
ahora = describir(ssm, "", [b["name"] for b in borrar]) if borrar else {}
|
|
485
|
+
except (ClientError, BotoCoreError) as exc:
|
|
486
|
+
if not denegado(exc):
|
|
487
|
+
# Otro fallo (red, límite de llamadas...): sin comprobar no se borra, pero lo ya aplicado se devuelve
|
|
488
|
+
for b in borrar:
|
|
489
|
+
b["error"] = f"No se pudo comprobar la versión antes de borrar ({motivo(exc)}): no se borra."
|
|
490
|
+
resultados += borrar
|
|
491
|
+
return resultados
|
|
492
|
+
ahora = None # sin permiso para describir: se borra como antes (ya se comprobó al revisar)
|
|
493
|
+
for b in [b for b in borrar if ahora is not None and b["name"] in ahora and b["name"] in esperada
|
|
494
|
+
and ahora[b["name"]].get("version") != esperada[b["name"]]]:
|
|
495
|
+
b["error"] = (f"Ha cambiado en AWS mientras se aplicaba (versión {esperada[b['name']]} → "
|
|
496
|
+
f"{ahora[b['name']].get('version')}): no se borra. "
|
|
497
|
+
+ (f"Su copia «{copia_de[b['name']]}» se creó con el valor de antes: revisa cuál dejar."
|
|
498
|
+
if b["name"] in copia_de else "Recarga el grupo."))
|
|
499
|
+
borrar.remove(b)
|
|
500
|
+
resultados.append(b)
|
|
501
|
+
|
|
443
502
|
# Borrados de 10 en 10 (el máximo de DeleteParameters)
|
|
444
503
|
for i in range(0, len(borrar), 10):
|
|
445
504
|
lote = borrar[i:i + 10]
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
PX.oyentes.push(st => {
|
|
56
56
|
const donde = st.config ? `${st.config.profile_name}|${st.config.region_name}` : '';
|
|
57
57
|
if (donde !== S.donde) { // otro perfil o región: lo cargado era de otra cuenta
|
|
58
|
-
S.donde = donde; S.items = []; S.carpetas = null; S.ruta = ''; $('pBarra').hidden = true; $('pLista').innerHTML = '';
|
|
58
|
+
S.donde = donde; S.items = []; S.carpetas = null; B.enAws = null; S.ruta = ''; $('pBarra').hidden = true; $('pLista').innerHTML = '';
|
|
59
59
|
$('pEstado').textContent = '';
|
|
60
60
|
pintarDonde();
|
|
61
61
|
}
|
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
B.pidiendo = B.pidiendo || api('/api/explorar').then(r => {
|
|
91
91
|
const c = {};
|
|
92
92
|
r.nombres.forEach(n => { const seg = n.split('/').filter(Boolean); for (let i = 1; i < seg.length; i++) { const k = '/' + seg.slice(0, i).join('/'); c[k] = (c[k] || 0) + 1; } });
|
|
93
|
+
S.nombres = new Set(r.nombres); S.limitado = r.limitado; // los parámetros también se buscan, no solo las carpetas
|
|
93
94
|
S.carpetas = c; return c;
|
|
94
95
|
}).finally(() => { B.pidiendo = null; });
|
|
95
96
|
return B.pidiendo;
|
|
@@ -111,12 +112,33 @@
|
|
|
111
112
|
try { c = await carpetas(); } catch (e) { caja.innerHTML = `<div class="sug-ayuda error">${esc(e.message)}</div>`; return; }
|
|
112
113
|
if ($('pRuta').value.trim().toLowerCase() !== q) return; // ya se ha escrito otra cosa
|
|
113
114
|
// Primero las que empiezan por lo escrito, luego las más cortas
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
if (S.limitado && q.length >= 2 && !B.enAws?.has(q)) buscarEnAws(q);
|
|
116
|
+
const orden = (a, b) => (b.toLowerCase().startsWith(q) - a.toLowerCase().startsWith(q)) || a.split('/').length - b.split('/').length || a.localeCompare(b);
|
|
117
|
+
const dirs = Object.keys(c).filter(k => k.toLowerCase().includes(q)).sort(orden).slice(0, 200);
|
|
118
|
+
const params = [...(S.nombres || [])].filter(n => !c[n] && n.toLowerCase().includes(q)).sort(orden).slice(0, 100); // los parámetros mismos, después
|
|
119
|
+
B.lista = [...dirs, ...params];
|
|
120
|
+
caja.innerHTML = B.lista.map((k, i) => i < dirs.length
|
|
121
|
+
? `<div class="sug" role="option" data-i="${i}"><code>${resaltar(k, q)}</code><span class="exp-n">${c[k]}</span><button type="button" class="secundario sug-grupo" data-grupo-de="${esc(k)}" title="Guardar esta carpeta como grupo (se detecta dónde va el entorno)" tabindex="-1">+ Grupo</button></div>`
|
|
122
|
+
: `<div class="sug sug-param" role="option" data-i="${i}" title="Parámetro: se carga solo este"><code>${resaltar(k, q)}</code><span class="exp-n">parámetro</span></div>`).join('')
|
|
123
|
+
+ `<div class="sug-ayuda">${B.enAws?.get(q) === 'buscando' ? 'Buscando también en AWS… · ' : ''}${B.lista.length ? '<kbd>↑</kbd> <kbd>↓</kbd> para elegir · «+ Grupo» la guarda en tus grupos · ' : 'Ninguna carpeta ni parámetro con eso. '}<kbd>Intro</kbd> carga ${B.lista.length ? 'la elegida' : 'la ruta tal cual'}${q.startsWith('/') ? '' : ' (las rutas empiezan por /)'}</div>`;
|
|
118
124
|
marcarActiva(B.lista.length && !q.startsWith('/') ? 0 : -1);
|
|
119
125
|
}
|
|
126
|
+
// Con más parámetros de los que caben en la lista, lo escrito se pide a AWS (distingue mayúsculas: tal cual, en minúsculas y en mayúsculas)
|
|
127
|
+
function buscarEnAws(q) {
|
|
128
|
+
B.enAws = B.enAws || new Map(); B.enAws.set(q, 'buscando');
|
|
129
|
+
clearTimeout(B.espera);
|
|
130
|
+
B.espera = setTimeout(async () => {
|
|
131
|
+
try {
|
|
132
|
+
const vistas = await Promise.all([...new Set([q, q.toLowerCase(), q.toUpperCase()])].map(t => api('/api/explorar?contiene=' + encodeURIComponent(t))));
|
|
133
|
+
vistas.forEach(v => v.nombres.forEach(n => {
|
|
134
|
+
if (S.nombres.has(n)) return; S.nombres.add(n);
|
|
135
|
+
const seg = n.split('/').filter(Boolean); for (let i = 1; i < seg.length; i++) { const k = '/' + seg.slice(0, i).join('/'); S.carpetas[k] = (S.carpetas[k] || 0) + 1; }
|
|
136
|
+
}));
|
|
137
|
+
B.enAws.set(q, 'hecho');
|
|
138
|
+
} catch (e) { B.enAws.delete(q); toast(e.message, true); }
|
|
139
|
+
if ($('pRuta').value.trim().toLowerCase() === q) sugerir();
|
|
140
|
+
}, 400);
|
|
141
|
+
}
|
|
120
142
|
// «+ Grupo»: guarda la carpeta en la config detectando dónde va el entorno (lo mismo que Explorar y Ajustes)
|
|
121
143
|
async function anadirGrupo(carpeta) {
|
|
122
144
|
try {
|
|
@@ -155,7 +177,7 @@
|
|
|
155
177
|
// ------------------------------------------------ cargar y pintar
|
|
156
178
|
async function cargar() {
|
|
157
179
|
const ruta = $('pRuta').value.trim();
|
|
158
|
-
if (!ruta
|
|
180
|
+
if (!ruta) return toast('Escribe una ruta o el nombre de un parámetro', true);
|
|
159
181
|
$('pEstado').textContent = `Leyendo ${ruta} de AWS…`; $('pLista').innerHTML = ''; $('pBarra').hidden = true;
|
|
160
182
|
try {
|
|
161
183
|
const r = await post('/api/cargar', { ruta });
|
|
@@ -365,7 +387,7 @@
|
|
|
365
387
|
const hechos = new Set(res.resultados.filter(x => x.ok).map(x => x.name));
|
|
366
388
|
const sinHacer = pendientes().filter(p => !hechos.has(nombreEnvio(p))).map(p => ({ ...p, name: nombreEnvio(p) }));
|
|
367
389
|
const conflictos = new Set([...r.plan.filter(f => f.conflicto).map(f => f.name), ...mal.filter(x => /^Conflicto/.test(x.error)).map(x => x.name)]);
|
|
368
|
-
S.items = []; S.carpetas = null;
|
|
390
|
+
S.items = []; S.carpetas = null; B.enAws = null;
|
|
369
391
|
await cargar();
|
|
370
392
|
if (sinHacer.length && S.ruta) { reponer(S.items, sinHacer, conflictos); pintar(); }
|
|
371
393
|
toast(mal.length ? `${ok} aplicados, ${mal.length} con error` : `${ok} cambios aplicados`, !!mal.length);
|
|
@@ -500,28 +522,46 @@
|
|
|
500
522
|
modal.abrir('<h2>Explorar</h2><p>Leyendo los nombres (sin valores)…</p>');
|
|
501
523
|
let r;
|
|
502
524
|
try { r = await api('/api/explorar'); } catch (e) { return modal.abrir(`<h2>Explorar</h2><p class="error">${esc(e.message)}</p>`); }
|
|
525
|
+
const nombres = new Set(r.nombres); // con más de los que caben, lo que se filtra se pide también a AWS y se añade aquí
|
|
503
526
|
const arbol = q => {
|
|
504
527
|
const raiz = {};
|
|
505
|
-
|
|
528
|
+
[...nombres].filter(n => !q || n.toLowerCase().includes(q)).forEach(n => {
|
|
529
|
+
const seg = n.split('/').filter(Boolean);
|
|
530
|
+
if (seg.length === 1) { raiz[n] = raiz[n] || { _n: 1, _hoja: n }; return; } // sin carpeta: el parámetro mismo
|
|
506
531
|
let nodo = raiz;
|
|
507
|
-
|
|
532
|
+
seg.slice(0, -1).forEach(x => { nodo[x] = nodo[x] || { _n: 0 }; nodo[x]._n++; nodo = nodo[x]; });
|
|
508
533
|
});
|
|
509
534
|
return raiz;
|
|
510
535
|
};
|
|
511
|
-
const fila = (k, ruta, n, nivel) => {
|
|
536
|
+
const fila = (k, ruta, n, nivel, hoja) => {
|
|
512
537
|
const e = entornoDe(k);
|
|
513
538
|
return `<span class="exp-nombre nivel-${Math.min(nivel, 3)}${e ? ' entorno ' + claseEntorno(e) : ''}">${esc(k)}</span><span class="exp-n" title="Parámetros por debajo">${n}</span>
|
|
514
|
-
<span class="exp-acciones"><button type="button" class="secundario" data-ruta="${esc(ruta)}">Cargar</button
|
|
539
|
+
<span class="exp-acciones"><button type="button" class="secundario" data-ruta="${esc(ruta)}">Cargar</button>${hoja ? '' : `<button type="button" class="secundario" data-grupo-de="${esc(ruta)}" title="Guardar esta carpeta como grupo (se detecta dónde va el entorno)">+ Grupo</button>`}</span>`;
|
|
515
540
|
};
|
|
516
|
-
const rama = (nodo, ruta, nivel, abierto) => Object.keys(nodo).filter(k => k !== '_n').sort((a, b) => a.localeCompare(b, 'es', { sensitivity: 'base' })).map(k => {
|
|
517
|
-
const r2 = `${ruta}/${k}`, hijos = rama(nodo[k], r2, nivel + 1, abierto);
|
|
518
|
-
return hijos ? `<details class="rama" ${abierto ? 'open' : ''}><summary class="exp-fila">${fila(k, r2, nodo[k]._n, nivel)}</summary><div class="hijos">${hijos}</div></details>`
|
|
519
|
-
: `<div class="exp-fila exp-hoja">${fila(k, r2, nodo[k]._n, nivel)}</div>`;
|
|
541
|
+
const rama = (nodo, ruta, nivel, abierto) => Object.keys(nodo).filter(k => k !== '_n' && k !== '_hoja').sort((a, b) => a.localeCompare(b, 'es', { sensitivity: 'base' })).map(k => {
|
|
542
|
+
const r2 = nodo[k]._hoja || `${ruta}/${k}`, hijos = rama(nodo[k], r2, nivel + 1, abierto);
|
|
543
|
+
return hijos ? `<details class="rama" ${abierto ? 'open' : ''}><summary class="exp-fila">${fila(k, r2, nodo[k]._n, nivel, nodo[k]._hoja)}</summary><div class="hijos">${hijos}</div></details>`
|
|
544
|
+
: `<div class="exp-fila exp-hoja">${fila(k, r2, nodo[k]._n, nivel, nodo[k]._hoja)}</div>`;
|
|
520
545
|
}).join('');
|
|
521
|
-
modal.abrir(`<h2>Explorar · ${r.nombres.length} parámetros</h2><p>Solo nombres. «Cargar» abre la carpeta; «+ Grupo» la guarda en tu configuración para elegirla luego con su entorno.${r.limitado ? ' <span class="warn">Hay más de los que se muestran:
|
|
546
|
+
modal.abrir(`<h2>Explorar · ${r.nombres.length} parámetros</h2><p>Solo nombres. «Cargar» abre la carpeta; «+ Grupo» la guarda en tu configuración para elegirla luego con su entorno.${r.limitado ? ' <span class="warn">Hay más de los que se muestran: al filtrar se buscan también en AWS.</span>' : ''}</p>
|
|
522
547
|
<div class="exp-cab"><input type="search" id="pExpFiltro" placeholder="Filtrar rutas… (p. ej. rds, api)" ${NOFILL}><span class="leyenda">${cfg().entornos.map(e => `<span class="entorno ${claseEntorno(e)}">${esc(e)}</span>`).join('')}</span></div>
|
|
523
548
|
<div class="arbol" id="pArbol">${rama(arbol(''), '', 0, false) || '<p class="muted">No hay parámetros.</p>'}</div>`);
|
|
524
|
-
|
|
549
|
+
let buscando = false, espera;
|
|
550
|
+
const pintarArbol = () => { const q = $('pExpFiltro').value.trim().toLowerCase(); $('pArbol').innerHTML = rama(arbol(q), '', 0, !!q) || `<p class="muted">${buscando ? 'Buscando en AWS…' : 'Ninguna ruta con eso.'}</p>`; };
|
|
551
|
+
$('pExpFiltro').addEventListener('input', () => {
|
|
552
|
+
pintarArbol();
|
|
553
|
+
const q = $('pExpFiltro').value.trim();
|
|
554
|
+
clearTimeout(espera);
|
|
555
|
+
if (!r.limitado || q.length < 2) return;
|
|
556
|
+
buscando = true; pintarArbol();
|
|
557
|
+
espera = setTimeout(async () => { // AWS distingue mayúsculas: tal cual, en minúsculas y en mayúsculas
|
|
558
|
+
try {
|
|
559
|
+
const vistas = await Promise.all([...new Set([q, q.toLowerCase(), q.toUpperCase()])].map(t => api('/api/explorar?contiene=' + encodeURIComponent(t))));
|
|
560
|
+
vistas.forEach(v => v.nombres.forEach(n => nombres.add(n)));
|
|
561
|
+
} catch (e) { toast(e.message, true); }
|
|
562
|
+
buscando = false; if ($('pExpFiltro')?.value.trim() === q) pintarArbol();
|
|
563
|
+
}, 400);
|
|
564
|
+
});
|
|
525
565
|
$('pExpFiltro').focus();
|
|
526
566
|
$('pArbol').addEventListener('click', async e => {
|
|
527
567
|
const g = e.target.closest('[data-grupo-de]');
|
|
@@ -333,6 +333,71 @@ def test_renombrar_conserva_la_clave_kms_y_nunca_pierde_el_viejo(cliente, ssm, m
|
|
|
333
333
|
assert "/dev/app/url" in ssm.params and not [o for o in ssm.ops("DeleteParameters") if "/dev/app/url" in o["Names"]]
|
|
334
334
|
|
|
335
335
|
|
|
336
|
+
def test_si_el_original_cambia_mientras_se_aplica_no_se_borra(cliente, ssm, monkeypatch):
|
|
337
|
+
ssm.poner("/dev/app/url", "https://x", tipo="String", tags=TAGS)
|
|
338
|
+
ssm.poner("/dev/app/b", "b", tipo="String", tags=TAGS)
|
|
339
|
+
ps = cargar(cliente)
|
|
340
|
+
aplicar = lambda items: {x["name"]: x for x in cliente.post("/api/aplicar", json={ # noqa: E731
|
|
341
|
+
"items": items, "confirmacion": CONFIRMA, "borrados": "1"}).get_json()["resultados"]}
|
|
342
|
+
|
|
343
|
+
# Mientras se crea la copia renombrada, otro cambia el original
|
|
344
|
+
real = ssm.put_parameter
|
|
345
|
+
|
|
346
|
+
def y_otro_cambia_el_original(**kw):
|
|
347
|
+
r = real(**kw)
|
|
348
|
+
if kw["Name"] == "/dev/app/url2":
|
|
349
|
+
real(Name="/dev/app/url", Value="https://nuevo", Overwrite=True)
|
|
350
|
+
return r
|
|
351
|
+
monkeypatch.setattr(ssm, "put_parameter", y_otro_cambia_el_original)
|
|
352
|
+
r = aplicar(renombrar(ps["/dev/app/url"], "/dev/app/url2"))
|
|
353
|
+
assert r["/dev/app/url2"]["ok"] and not r["/dev/app/url"]["ok"]
|
|
354
|
+
assert "Ha cambiado en AWS mientras se aplicaba (versión 1 → 2)" in r["/dev/app/url"]["error"]
|
|
355
|
+
assert "«/dev/app/url2»" in r["/dev/app/url"]["error"] # avisa de la copia con el valor de antes
|
|
356
|
+
assert ssm.params["/dev/app/url"]["value"] == "https://nuevo" # el cambio del otro no se pierde
|
|
357
|
+
assert ssm.ops("DeleteParameters") == []
|
|
358
|
+
monkeypatch.setattr(ssm, "put_parameter", real)
|
|
359
|
+
|
|
360
|
+
# Un borrado normal: lo cambian entre la revisión y el borrado
|
|
361
|
+
revisar_real = ssm_web._revisar
|
|
362
|
+
|
|
363
|
+
def revisa_y_cambia(*a, **k):
|
|
364
|
+
out = revisar_real(*a, **k)
|
|
365
|
+
ssm.poner("/dev/app/b", "b-de-otro", tipo="String")
|
|
366
|
+
return out
|
|
367
|
+
monkeypatch.setattr(ssm_web, "_revisar", revisa_y_cambia)
|
|
368
|
+
r = aplicar([item(ps["/dev/app/b"], accion="borrar")])
|
|
369
|
+
assert not r["/dev/app/b"]["ok"] and "Recarga el grupo" in r["/dev/app/b"]["error"]
|
|
370
|
+
assert "/dev/app/b" in ssm.params and ssm.ops("DeleteParameters") == []
|
|
371
|
+
|
|
372
|
+
# Sin permiso para volver a leerla: se borra como antes (ya se comprobó al revisar)
|
|
373
|
+
def revisa_y_quita_describe(*a, **k):
|
|
374
|
+
out = revisar_real(*a, **k)
|
|
375
|
+
ssm.sin_describe = True
|
|
376
|
+
return out
|
|
377
|
+
monkeypatch.setattr(ssm_web, "_revisar", revisa_y_quita_describe)
|
|
378
|
+
ssm.poner("/dev/app/c", "c", tipo="String")
|
|
379
|
+
r = aplicar([{"name": "/dev/app/c", "accion": "borrar", "version": 1}])
|
|
380
|
+
assert r["/dev/app/c"]["ok"] and "/dev/app/c" not in ssm.params
|
|
381
|
+
ssm.sin_describe = False
|
|
382
|
+
|
|
383
|
+
# Si falla de otra forma al leerla, no se borra (sin comprobar no se toca)
|
|
384
|
+
def revisa_y_rompe(*a, **k):
|
|
385
|
+
out = revisar_real(*a, **k)
|
|
386
|
+
monkeypatch.setattr(ssm, "describe_parameters", lambda **kw: (_ for _ in ()).throw(
|
|
387
|
+
ssm_web.ClientError({"Error": {"Code": "ThrottlingException", "Message": "Rate exceeded"}}, "Op")))
|
|
388
|
+
return out
|
|
389
|
+
monkeypatch.setattr(ssm_web, "_revisar", revisa_y_rompe)
|
|
390
|
+
ssm.poner("/dev/app/d", "d", tipo="String")
|
|
391
|
+
r = aplicar([{"name": "/dev/app/d", "accion": "borrar", "version": 1}])
|
|
392
|
+
assert not r["/dev/app/d"]["ok"] and "No se pudo comprobar" in r["/dev/app/d"]["error"]
|
|
393
|
+
assert "/dev/app/d" in ssm.params
|
|
394
|
+
monkeypatch.undo()
|
|
395
|
+
|
|
396
|
+
# Sin cambios, se borra como siempre (b va ya por la versión 2)
|
|
397
|
+
r = aplicar([{"name": "/dev/app/b", "accion": "borrar", "version": 2}])
|
|
398
|
+
assert r["/dev/app/b"]["ok"] and "/dev/app/b" not in ssm.params
|
|
399
|
+
|
|
400
|
+
|
|
336
401
|
# ---------------------------------------------------------------- nombre sin barra final
|
|
337
402
|
|
|
338
403
|
def test_nuevo_con_barra_final_se_quita(cliente, ssm):
|
|
@@ -407,3 +472,22 @@ def test_js_reponer_lo_no_aplicado(tmp_path):
|
|
|
407
472
|
assert por["/r/d"]["borrar"] is True
|
|
408
473
|
# Lo que ya no está en AWS no se inventa
|
|
409
474
|
assert "/r/ya-no-esta" not in por and len(out) == 6
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
# ---------------------------------------------------------------- buscar un parámetro
|
|
478
|
+
|
|
479
|
+
def test_en_ruta_vale_el_nombre_de_un_parametro_o_uno_sin_carpeta(cliente, ssm):
|
|
480
|
+
ssm.poner("/dev/app/clave", "x1")
|
|
481
|
+
ssm.poner("SinCarpeta", "raiz", tipo="String")
|
|
482
|
+
assert list(cargar(cliente, "/dev/app/clave")) == ["/dev/app/clave"] # el nombre completo, no su carpeta
|
|
483
|
+
assert cargar(cliente, "/dev/app/clave")["/dev/app/clave"]["value"] == "x1"
|
|
484
|
+
assert cargar(cliente, "SinCarpeta")["SinCarpeta"]["value"] == "raiz"
|
|
485
|
+
assert cargar(cliente, "/dev/no/existe") == {}
|
|
486
|
+
assert cargar(cliente, "NoExiste") == {}
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def test_explorar_busca_en_aws_lo_que_no_cabe_en_la_lista(cliente, ssm):
|
|
490
|
+
ssm.poner("/dev/app/clave", "x1")
|
|
491
|
+
ssm.poner("/prod/otra/Cosa", "x2")
|
|
492
|
+
r = cliente.get("/api/explorar?contiene=Cosa").get_json()
|
|
493
|
+
assert r["nombres"] == ["/prod/otra/Cosa"]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.5.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|