paramsx 2.2.2__tar.gz → 2.3.0__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.
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.3.0
4
+
5
+ ### Añadido
6
+
7
+ - **La descripción de los parámetros, editable como el valor.** El fichero exportado trae un campo `parameter_description` **debajo del nombre y antes del valor**: se lee de AWS, lo editas y al cargar se actualiza. Los cambios de descripción se detectan igual que los de valor y aparecen en la pantalla de confirmación como `Modificado (descripción)`. La opción 4 también la pide al crear un parámetro, entre la ruta y el valor.
8
+ - Los backups incluyen la descripción, así que un backup restaura también las descripciones.
9
+ - **Flag `forzar_securestring`** (por defecto `True`, el comportamiento de siempre): todo lo que sube ParamsX va cifrado como `SecureString`. Con `False` se respeta el tipo que ya tuviera el parámetro en AWS: al actualizar no se manda el tipo y AWS conserva el suyo (`Type` solo es obligatorio al crear). El fichero exportado trae además un campo `parameter_type` editable, por si quieres cambiar el tipo a propósito, y sus cambios se detectan como los del valor o la descripción. El tipo se lee de la misma llamada que el valor, así que no hace falta ningún permiso nuevo.
10
+
11
+ ### Nota sobre el valor y la descripción
12
+
13
+ `put_parameter` con `Overwrite=True` reescribe la definición del parámetro, así que ParamsX **manda siempre la descripción que trae el fichero**, incluso cuando lo único que cambió fue el valor. Antes no se mandaba, y una descripción que ya existiera en AWS podía quedarse vacía al editar el valor.
14
+
15
+ ### Nota sobre permisos
16
+
17
+ Leer las descripciones necesita **`ssm:DescribeParameters`**, porque no vienen en `GetParametersByPath` (son metadatos, no valores). Si tu rol no lo tiene, ParamsX te avisa al leer, el campo no aparece en el fichero y al cargar **no se toca** la descripción que haya en AWS. El resto sigue funcionando igual.
18
+
3
19
  ## 2.2.2
4
20
 
5
21
  Un renombrado de la configuración, sin cambios de comportamiento. **Los nombres antiguos
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: paramsx
3
- Version: 2.2.2
3
+ Version: 2.3.0
4
4
  Summary: Librería para gestionar y respaldar parámetros de AWS SSM de manera sencilla y eficiente.
5
5
  Home-page: https://github.com/pistatxos/paramsx
6
6
  Author: Mariox
@@ -70,7 +70,7 @@ Al ejecutar ```paramsx``` desde la terminal, accedes a un menú interactivo con
70
70
  - Navega por la lista de parámetros configurada en tu archivo de configuración y selecciona cuál descargar.
71
71
  - Elige el entorno deseado (por ejemplo, dev o prod).
72
72
  - Archivos generados:
73
- - parameters_dev.py o parameters_prod.py → Edita estos archivos para modificar, añadir o eliminar parámetros (valores y tags).
73
+ - parameters_dev.py o parameters_prod.py → Edita estos archivos para modificar, añadir o eliminar parámetros (descripciones, valores y tags).
74
74
  - parameters_dev_backup.py → Respaldo automático del archivo descargado, incluidas las tags.
75
75
  Los archivos se crean en la misma ruta desde donde ejecutas paramsx, evitando movernos innecesariamente entre carpetas.
76
76
  - Si tu rol IAM no tiene permisos sobre la ruta configurada, verás un mensaje claro en lugar de una traza de boto3:
@@ -98,6 +98,7 @@ Esto te permitirá mantener respaldos seguros o realizar migraciones/reorganizac
98
98
  4. Crear nuevo parámetro
99
99
  - Crea un parámetro desde cero sin salir de la herramienta. Te pedirá, en este orden:
100
100
  - La ruta **sin el entorno**, igual que la declaras en `parameter_list`: el perfil se encarga de colocarlo. Verás en pantalla la ruta resultante en AWS antes de seguir.
101
+ - La descripción, opcional: para qué sirve el parámetro. Se guarda en AWS y luego se puede editar en el fichero exportado.
101
102
  - El valor: texto plano o JSON en una línea (por ejemplo `{"host": "x", "user": "y", "pass": "z"}`), que se valida antes de continuar.
102
103
  - Las tags obligatorias, si tienes `tags_activas = True`.
103
104
  - El perfil que se usa es el que declares en `perfil_nuevos`. Si tu perfil es de tipo `mixto`, escribe el `*` en la ruta para indicar dónde va el entorno.
@@ -212,6 +213,16 @@ perfil_nuevos = "min"
212
213
  # segunda lectura te machaque el fichero de la primera.
213
214
  fichero_por_ruta = False
214
215
 
216
+ # ¿Todo lo que sube ParamsX va cifrado como SecureString?
217
+ # True (por defecto) -> sí, siempre. Es lo recomendable: los parámetros de una cuenta
218
+ # suelen tener secretos, y así no depende de acordarse.
219
+ # False -> se respeta el tipo que ya tuviera el parámetro en AWS (String, StringList o
220
+ # SecureString): al actualizar no se manda el tipo, así que AWS conserva el suyo.
221
+ # En este modo el fichero exportado trae además un campo 'parameter_type' que
222
+ # puedes editar si quieres cambiarlo a propósito. Los parámetros que crees con
223
+ # la opción 4 se siguen creando como SecureString.
224
+ forzar_securestring = True
225
+
215
226
 
216
227
  ## Configuraciones Tags
217
228
 
@@ -269,6 +280,38 @@ En el nombre van el entorno, la ruta y el perfil, porque dos entradas pueden com
269
280
  | `{"path": "/API/STA", "perfil": "max"}` | `parameters_dev__API_STA__max.py` |
270
281
  | `{"path": "/API/*/STA", "perfil": "mixto_max"}` | `parameters_dev__API_env_STA__mixto_max.py` |
271
282
 
283
+ ### La descripción de los parámetros
284
+
285
+ El fichero exportado trae un campo `parameter_description` **debajo del nombre y antes del valor**. Se lee de AWS, se edita como cualquier otro campo y se aplica al cargar:
286
+
287
+ ```python
288
+ parametros = [
289
+ {'parameter_name': '/dev/api/sta/token',
290
+ 'parameter_description': "Token JWT de la API, rota cada 90 dias",
291
+ 'parameter_value': """abc123"""},
292
+ ]
293
+ ```
294
+
295
+ Dos cosas que conviene saber:
296
+
297
+ - **Hace falta el permiso `ssm:DescribeParameters`.** La descripción no viaja en `GetParametersByPath` como el valor: es un metadato y se pide con `DescribeParameters`. Si tu rol no lo tiene, ParamsX te avisa, el campo **no aparece** en el fichero y al cargar no se toca la descripción que haya en AWS.
298
+ - **Al actualizar un parámetro, AWS reescribe su definición entera.** Por eso ParamsX manda siempre la descripción del fichero cuando cambia el valor, aunque no la hayas tocado: si no la mandara, se perdería. Y como no existe una API para cambiar solo la descripción, editarla crea una versión nueva del parámetro igual que cambiar el valor.
299
+
300
+ Un cambio de descripción se ve en la pantalla de confirmación como `Modificado (descripción)`, y si cambias las dos cosas, `Modificado (valor | descripción)`.
301
+
302
+ ### Cifrado: `forzar_securestring`
303
+
304
+ ParamsX sube todo como **SecureString** por defecto, y así se recomienda dejarlo: los parámetros de una cuenta acaban teniendo secretos, y con esto no depende de que nadie se acuerde.
305
+
306
+ | `forzar_securestring` | Qué hace |
307
+ |---|---|
308
+ | `True` (por defecto) | Todo lo que se sube va cifrado como `SecureString`, sea del tipo que sea en AWS. |
309
+ | `False` | Se respeta el tipo que ya tuviera el parámetro (`String`, `StringList` o `SecureString`): al actualizar no se manda el tipo, y AWS conserva el suyo. El fichero exportado trae además un campo `parameter_type` que puedes editar si quieres cambiarlo a propósito, y ese cambio cuenta como uno más: verás `Modificado (tipo: SecureString)`. |
310
+
311
+ Los parámetros que creas con la opción 4 se crean siempre como `SecureString`, en los dos modos.
312
+
313
+ El tipo se lee de la misma llamada que el valor, así que **no hace falta ningún permiso extra** para esto (a diferencia de la descripción).
314
+
272
315
  ### Cómo acotar tu parameter_list (recomendación)
273
316
 
274
317
  `parameter_list` es solo tu vista de trabajo: **la seguridad real la impone IAM, no este fichero**. La recomendación de uso es:
@@ -300,6 +343,7 @@ Formato del fichero exportado con `tags_activas = True`:
300
343
  ```python
301
344
  parametros = [
302
345
  {'parameter_name': '/dev/email/users',
346
+ 'parameter_description': "Buzon que recibe los avisos de altas",
303
347
  'parameter_value': """agpetrovici@stanalytics.es""",
304
348
  'tagApplication': "",
305
349
  'tagEnvironment': "",
@@ -50,7 +50,7 @@ Al ejecutar ```paramsx``` desde la terminal, accedes a un menú interactivo con
50
50
  - Navega por la lista de parámetros configurada en tu archivo de configuración y selecciona cuál descargar.
51
51
  - Elige el entorno deseado (por ejemplo, dev o prod).
52
52
  - Archivos generados:
53
- - parameters_dev.py o parameters_prod.py → Edita estos archivos para modificar, añadir o eliminar parámetros (valores y tags).
53
+ - parameters_dev.py o parameters_prod.py → Edita estos archivos para modificar, añadir o eliminar parámetros (descripciones, valores y tags).
54
54
  - parameters_dev_backup.py → Respaldo automático del archivo descargado, incluidas las tags.
55
55
  Los archivos se crean en la misma ruta desde donde ejecutas paramsx, evitando movernos innecesariamente entre carpetas.
56
56
  - Si tu rol IAM no tiene permisos sobre la ruta configurada, verás un mensaje claro en lugar de una traza de boto3:
@@ -78,6 +78,7 @@ Esto te permitirá mantener respaldos seguros o realizar migraciones/reorganizac
78
78
  4. Crear nuevo parámetro
79
79
  - Crea un parámetro desde cero sin salir de la herramienta. Te pedirá, en este orden:
80
80
  - La ruta **sin el entorno**, igual que la declaras en `parameter_list`: el perfil se encarga de colocarlo. Verás en pantalla la ruta resultante en AWS antes de seguir.
81
+ - La descripción, opcional: para qué sirve el parámetro. Se guarda en AWS y luego se puede editar en el fichero exportado.
81
82
  - El valor: texto plano o JSON en una línea (por ejemplo `{"host": "x", "user": "y", "pass": "z"}`), que se valida antes de continuar.
82
83
  - Las tags obligatorias, si tienes `tags_activas = True`.
83
84
  - El perfil que se usa es el que declares en `perfil_nuevos`. Si tu perfil es de tipo `mixto`, escribe el `*` en la ruta para indicar dónde va el entorno.
@@ -192,6 +193,16 @@ perfil_nuevos = "min"
192
193
  # segunda lectura te machaque el fichero de la primera.
193
194
  fichero_por_ruta = False
194
195
 
196
+ # ¿Todo lo que sube ParamsX va cifrado como SecureString?
197
+ # True (por defecto) -> sí, siempre. Es lo recomendable: los parámetros de una cuenta
198
+ # suelen tener secretos, y así no depende de acordarse.
199
+ # False -> se respeta el tipo que ya tuviera el parámetro en AWS (String, StringList o
200
+ # SecureString): al actualizar no se manda el tipo, así que AWS conserva el suyo.
201
+ # En este modo el fichero exportado trae además un campo 'parameter_type' que
202
+ # puedes editar si quieres cambiarlo a propósito. Los parámetros que crees con
203
+ # la opción 4 se siguen creando como SecureString.
204
+ forzar_securestring = True
205
+
195
206
 
196
207
  ## Configuraciones Tags
197
208
 
@@ -249,6 +260,38 @@ En el nombre van el entorno, la ruta y el perfil, porque dos entradas pueden com
249
260
  | `{"path": "/API/STA", "perfil": "max"}` | `parameters_dev__API_STA__max.py` |
250
261
  | `{"path": "/API/*/STA", "perfil": "mixto_max"}` | `parameters_dev__API_env_STA__mixto_max.py` |
251
262
 
263
+ ### La descripción de los parámetros
264
+
265
+ El fichero exportado trae un campo `parameter_description` **debajo del nombre y antes del valor**. Se lee de AWS, se edita como cualquier otro campo y se aplica al cargar:
266
+
267
+ ```python
268
+ parametros = [
269
+ {'parameter_name': '/dev/api/sta/token',
270
+ 'parameter_description': "Token JWT de la API, rota cada 90 dias",
271
+ 'parameter_value': """abc123"""},
272
+ ]
273
+ ```
274
+
275
+ Dos cosas que conviene saber:
276
+
277
+ - **Hace falta el permiso `ssm:DescribeParameters`.** La descripción no viaja en `GetParametersByPath` como el valor: es un metadato y se pide con `DescribeParameters`. Si tu rol no lo tiene, ParamsX te avisa, el campo **no aparece** en el fichero y al cargar no se toca la descripción que haya en AWS.
278
+ - **Al actualizar un parámetro, AWS reescribe su definición entera.** Por eso ParamsX manda siempre la descripción del fichero cuando cambia el valor, aunque no la hayas tocado: si no la mandara, se perdería. Y como no existe una API para cambiar solo la descripción, editarla crea una versión nueva del parámetro igual que cambiar el valor.
279
+
280
+ Un cambio de descripción se ve en la pantalla de confirmación como `Modificado (descripción)`, y si cambias las dos cosas, `Modificado (valor | descripción)`.
281
+
282
+ ### Cifrado: `forzar_securestring`
283
+
284
+ ParamsX sube todo como **SecureString** por defecto, y así se recomienda dejarlo: los parámetros de una cuenta acaban teniendo secretos, y con esto no depende de que nadie se acuerde.
285
+
286
+ | `forzar_securestring` | Qué hace |
287
+ |---|---|
288
+ | `True` (por defecto) | Todo lo que se sube va cifrado como `SecureString`, sea del tipo que sea en AWS. |
289
+ | `False` | Se respeta el tipo que ya tuviera el parámetro (`String`, `StringList` o `SecureString`): al actualizar no se manda el tipo, y AWS conserva el suyo. El fichero exportado trae además un campo `parameter_type` que puedes editar si quieres cambiarlo a propósito, y ese cambio cuenta como uno más: verás `Modificado (tipo: SecureString)`. |
290
+
291
+ Los parámetros que creas con la opción 4 se crean siempre como `SecureString`, en los dos modos.
292
+
293
+ El tipo se lee de la misma llamada que el valor, así que **no hace falta ningún permiso extra** para esto (a diferencia de la descripción).
294
+
252
295
  ### Cómo acotar tu parameter_list (recomendación)
253
296
 
254
297
  `parameter_list` es solo tu vista de trabajo: **la seguridad real la impone IAM, no este fichero**. La recomendación de uso es:
@@ -280,6 +323,7 @@ Formato del fichero exportado con `tags_activas = True`:
280
323
  ```python
281
324
  parametros = [
282
325
  {'parameter_name': '/dev/email/users',
326
+ 'parameter_description': "Buzon que recibe los avisos de altas",
283
327
  'parameter_value': """agpetrovici@stanalytics.es""",
284
328
  'tagApplication': "",
285
329
  'tagEnvironment': "",
@@ -0,0 +1 @@
1
+ __version__ = "2.3.0"
@@ -161,7 +161,10 @@ def get_parameters_by_prefix(ssm, prefix):
161
161
  for param in response.get('Parameters', []):
162
162
  parameters.append({
163
163
  "parameter_name": param['Name'],
164
- "parameter_value": param['Value']
164
+ "parameter_value": param['Value'],
165
+ # El tipo viene en la misma respuesta que el valor, sin permisos extra.
166
+ # Solo se usa si forzar_securestring = False.
167
+ "parameter_type": param.get('Type', "SecureString"),
165
168
  })
166
169
 
167
170
  next_token = response.get('NextToken')
@@ -174,6 +177,61 @@ def get_parameters_by_prefix(ssm, prefix):
174
177
  return parameters
175
178
 
176
179
 
180
+ # Descripciones de los parámetros de una ruta, indexadas por nombre.
181
+ # La descripción NO viene en get_parameters_by_path: hay que pedirla aparte con
182
+ # describe_parameters, que es la que devuelve los metadatos. Eso exige el permiso
183
+ # ssm:DescribeParameters, que no todo el mundo tiene.
184
+ def get_descriptions_by_prefix(ssm, prefix):
185
+ descripciones = {}
186
+ next_token = None
187
+ # Con prefijo "/" (backup de la cuenta entera) no se filtra: se piden todos
188
+ ruta = prefix.rstrip("/")
189
+
190
+ while True:
191
+ request_args = {"MaxResults": 50}
192
+ if ruta:
193
+ request_args["ParameterFilters"] = [
194
+ {"Key": "Path", "Option": "Recursive", "Values": [ruta]}
195
+ ]
196
+ if next_token:
197
+ request_args["NextToken"] = next_token
198
+
199
+ try:
200
+ response = ssm.describe_parameters(**request_args)
201
+ except ClientError as e:
202
+ if _codigo_error(e) == "AccessDeniedException":
203
+ raise AccessDeniedError(prefix, sugerir_acotar=False) from None
204
+ raise
205
+
206
+ for meta in response.get("Parameters", []):
207
+ descripciones[meta["Name"]] = meta.get("Description", "")
208
+
209
+ next_token = response.get("NextToken")
210
+ if not next_token:
211
+ break
212
+
213
+ return descripciones
214
+
215
+
216
+ # Rellenar cada parámetro con su descripción. Devuelve la lista de avisos.
217
+ # Si no hay permisos, el campo NO se añade al fichero exportado: así al cargar se sabe
218
+ # que las descripciones no se gestionan y no se toca la que haya en AWS.
219
+ def agregar_descripciones_a_parametros(ssm, parameters, prefix):
220
+ try:
221
+ descripciones = get_descriptions_by_prefix(ssm, prefix)
222
+ except AccessDeniedError:
223
+ for param in parameters:
224
+ param.pop("parameter_description", None)
225
+ return [
226
+ "⚠ Sin permisos para leer las descripciones (hace falta ssm:DescribeParameters): "
227
+ "no aparecen en el fichero y al cargar no se tocarán las que ya existan en AWS."
228
+ ]
229
+
230
+ for param in parameters:
231
+ param["parameter_description"] = descripciones.get(param["parameter_name"], "")
232
+ return []
233
+
234
+
177
235
  # Comprobar si existe algún parámetro bajo un prefijo (None = no se pudo comprobar por permisos)
178
236
  def existe_prefijo(ssm, prefix):
179
237
  try:
@@ -318,7 +376,8 @@ def _orden_tags(param, tags_obligatorias):
318
376
 
319
377
 
320
378
  # Función para exportar parámetros a un archivo
321
- def export_parameters_to_file(parameters, file_path, tags_activas=False, tags_obligatorias=None):
379
+ def export_parameters_to_file(parameters, file_path, tags_activas=False, tags_obligatorias=None,
380
+ incluir_tipo=False):
322
381
  tags_obligatorias = tags_obligatorias or []
323
382
 
324
383
  with open(file_path, 'w') as f:
@@ -329,6 +388,17 @@ def export_parameters_to_file(parameters, file_path, tags_activas=False, tags_ob
329
388
  parameter_value = param['parameter_value']
330
389
  f.write(f" {{'parameter_name': '{parameter_name}',\n")
331
390
 
391
+ # La descripción va debajo del nombre y antes del valor. Solo se escribe si
392
+ # se ha podido leer de AWS: si falta el campo, la carga no la toca.
393
+ if "parameter_description" in param:
394
+ descripcion = _escapar_valor_tag(param.get("parameter_description") or "")
395
+ f.write(f" 'parameter_description': \"{descripcion}\",\n")
396
+
397
+ # El tipo solo se escribe si se respeta (forzar_securestring = False): con
398
+ # el valor por defecto todo acaba en SecureString y el campo solo confundiría.
399
+ if incluir_tipo and "parameter_type" in param:
400
+ f.write(f" 'parameter_type': \"{param['parameter_type']}\",\n")
401
+
332
402
  if not tags_activas:
333
403
  # Usar triple comillas para valores largos
334
404
  f.write(f" 'parameter_value': \"\"\"{parameter_value}\"\"\"}},\n\n")
@@ -346,6 +416,10 @@ def indexar_parametros(parametros):
346
416
  return {
347
417
  p['parameter_name']: {
348
418
  "value": p.get('parameter_value', ""),
419
+ # None (campo ausente) significa "este fichero no gestiona descripciones",
420
+ # que no es lo mismo que una descripción vacía
421
+ "description": p.get('parameter_description'),
422
+ "type": p.get('parameter_type'),
349
423
  "tags": extraer_tags(p),
350
424
  }
351
425
  for p in parametros
@@ -357,6 +431,10 @@ def indice_a_parametros(indice):
357
431
  parametros = []
358
432
  for nombre, datos in indice.items():
359
433
  param = {"parameter_name": nombre, "parameter_value": datos["value"]}
434
+ if datos.get("description") is not None:
435
+ param["parameter_description"] = datos["description"]
436
+ if datos.get("type") is not None:
437
+ param["parameter_type"] = datos["type"]
360
438
  for clave, valor in datos["tags"].items():
361
439
  param[f"{TAG_FIELD_PREFIX}{clave}"] = valor
362
440
  parametros.append(param)
@@ -416,24 +494,44 @@ def compare_parameters(file_path, backup_file_path, stdscr=None, tags_activas=Fa
416
494
  "tipo": "Nuevo",
417
495
  "detalle": "nuevo parámetro",
418
496
  "value": actual["value"],
497
+ "description": actual["description"],
498
+ "type": actual["type"],
419
499
  "tags": actual["tags"],
420
500
  "value_changed": True,
501
+ "description_changed": actual["description"] is not None,
421
502
  "tags_set": {k: v for k, v in actual["tags"].items() if str(v).strip()},
422
503
  "tags_remove": [],
423
504
  })
424
505
  continue
425
506
 
426
507
  value_changed = actual["value"] != anterior["value"]
508
+ # Solo se compara la descripción si los dos ficheros la traen: si falta en alguno,
509
+ # es que no se pudo leer de AWS y no se debe tocar
510
+ description_changed = (
511
+ actual["description"] is not None
512
+ and anterior["description"] is not None
513
+ and actual["description"] != anterior["description"]
514
+ )
515
+ type_changed = (
516
+ actual["type"] is not None
517
+ and anterior["type"] is not None
518
+ and actual["type"] != anterior["type"]
519
+ )
427
520
  tags_set, tags_remove = ({}, [])
428
521
  if tags_activas:
429
522
  tags_set, tags_remove = _diff_tags(actual["tags"], anterior["tags"])
430
523
 
431
- if not value_changed and not tags_set and not tags_remove:
524
+ if (not value_changed and not description_changed and not type_changed
525
+ and not tags_set and not tags_remove):
432
526
  continue
433
527
 
434
528
  detalles = []
435
529
  if value_changed:
436
530
  detalles.append("valor")
531
+ if description_changed:
532
+ detalles.append("descripción")
533
+ if type_changed:
534
+ detalles.append(f"tipo: {actual['type']}")
437
535
  if tags_set:
438
536
  detalles.append(f"tags: {', '.join(sorted(tags_set))}")
439
537
  if tags_remove:
@@ -444,8 +542,12 @@ def compare_parameters(file_path, backup_file_path, stdscr=None, tags_activas=Fa
444
542
  "tipo": "Modificado",
445
543
  "detalle": " | ".join(detalles),
446
544
  "value": actual["value"],
545
+ "description": actual["description"],
546
+ "type": actual["type"],
447
547
  "tags": actual["tags"],
448
548
  "value_changed": value_changed,
549
+ "description_changed": description_changed,
550
+ "type_changed": type_changed,
449
551
  "tags_set": tags_set,
450
552
  "tags_remove": tags_remove,
451
553
  })
@@ -457,8 +559,12 @@ def compare_parameters(file_path, backup_file_path, stdscr=None, tags_activas=Fa
457
559
  "tipo": "Eliminado",
458
560
  "detalle": "se borrará de AWS",
459
561
  "value": backup_dict[nombre]["value"],
562
+ "description": backup_dict[nombre]["description"],
563
+ "type": backup_dict[nombre]["type"],
460
564
  "tags": backup_dict[nombre]["tags"],
461
565
  "value_changed": False,
566
+ "description_changed": False,
567
+ "type_changed": False,
462
568
  "tags_set": {},
463
569
  "tags_remove": [],
464
570
  })
@@ -15,6 +15,7 @@ from .funcions import (
15
15
  compare_parameters, load_parameters, show_main_menu_selection,
16
16
  AccessDeniedError, build_full_path, etiqueta_entrada, agregar_tags_a_parametros,
17
17
  validar_tags_obligatorias, aplicar_cambios_tags, check_rds_correlacion,
18
+ agregar_descripciones_a_parametros,
18
19
  show_report, prompt_input, indexar_parametros, indice_a_parametros,
19
20
  ficheros_cargables,
20
21
  POSICIONES_ENTORNO, CASES_ENTORNO, CASES_RUTA, MARCADOR_ENTORNO, slug_entrada,
@@ -51,7 +52,7 @@ que va siempre en minúscula, aplicándole el 'case_entorno' del perfil."""
51
52
  # Opciones que viven fuera de 'configuraciones' y son opcionales: si el usuario no las
52
53
  # tiene, se usa el valor de la plantilla. Se listan para poder decirle qué le falta.
53
54
  CLAVES_OPCIONALES = (
54
- "perfiles", "perfil_nuevos", "fichero_por_ruta",
55
+ "perfiles", "perfil_nuevos", "fichero_por_ruta", "forzar_securestring",
55
56
  "tags_activas", "obligatorias_vacias", "tags_obligatorias",
56
57
  )
57
58
 
@@ -98,6 +99,9 @@ def load_config():
98
99
  config["fichero_por_ruta"] = bool(
99
100
  getattr(modulo, "fichero_por_ruta", plantilla_config.fichero_por_ruta)
100
101
  )
102
+ config["forzar_securestring"] = bool(
103
+ getattr(modulo, "forzar_securestring", plantilla_config.forzar_securestring)
104
+ )
101
105
  # Si no se declara, los parámetros nuevos usan el primer perfil definido
102
106
  # ('convencion_nuevos' es el nombre que tuvo en la 2.2.0)
103
107
  primer_perfil = next(iter(config["perfiles"]), None)
@@ -301,9 +305,9 @@ def leer_ruta(stdscr, ssm, full_path, tags_activas, tags_obligatorias):
301
305
  show_message(stdscr, f"{e}", 2)
302
306
  return None, []
303
307
 
304
- avisos = []
308
+ avisos = agregar_descripciones_a_parametros(ssm, parameters, full_path)
305
309
  if tags_activas:
306
- avisos = agregar_tags_a_parametros(ssm, parameters, tags_obligatorias)
310
+ avisos.extend(agregar_tags_a_parametros(ssm, parameters, tags_obligatorias))
307
311
 
308
312
  return parameters, avisos
309
313
 
@@ -334,7 +338,7 @@ def crear_parametro(stdscr, ssm, entornos, perfil, nombre_perfil, tags_activas,
334
338
  while True:
335
339
  path = prompt_input(
336
340
  stdscr,
337
- "Crear nuevo parámetro (1/4): ruta",
341
+ "Crear nuevo parámetro (1/5): ruta",
338
342
  f"Ruta del parámetro (perfil '{nombre_perfil}', sin el entorno):",
339
343
  valor=path,
340
344
  ayuda=ayudas.get(posicion, ""),
@@ -360,7 +364,7 @@ def crear_parametro(stdscr, ssm, entornos, perfil, nombre_perfil, tags_activas,
360
364
  path = build_full_path(path_declarado, perfil, entorno)
361
365
 
362
366
  if not show_report(
363
- stdscr, "Crear nuevo parámetro (2/4): confirmar la ruta",
367
+ stdscr, "Crear nuevo parámetro (2/5): confirmar la ruta",
364
368
  [f"Perfil: {nombre_perfil}",
365
369
  f"Escrita: {path_declarado}",
366
370
  f"En AWS: {path}",
@@ -370,12 +374,26 @@ def crear_parametro(stdscr, ssm, entornos, perfil, nombre_perfil, tags_activas,
370
374
  ):
371
375
  return
372
376
 
373
- # 2. Valor (string plano o JSON)
377
+ # 3. Descripción: va debajo del nombre y antes del valor, igual que en el fichero
378
+ descripcion = prompt_input(
379
+ stdscr,
380
+ "Crear nuevo parámetro (3/5): descripción",
381
+ "Descripción del parámetro (opcional):",
382
+ valor="",
383
+ permitir_vacio=True,
384
+ ayuda="Para qué sirve este parámetro. Se guarda en AWS y luego se puede editar "
385
+ "en el fichero exportado. Máximo 1024 caracteres; Enter para dejarla vacía.",
386
+ )
387
+ if descripcion is None:
388
+ return
389
+ descripcion = descripcion.strip()[:1024]
390
+
391
+ # 4. Valor (string plano o JSON)
374
392
  valor = ""
375
393
  while True:
376
394
  valor = prompt_input(
377
395
  stdscr,
378
- "Crear nuevo parámetro (3/4): valor",
396
+ "Crear nuevo parámetro (4/5): valor",
379
397
  f"Valor para {path}:",
380
398
  valor=valor,
381
399
  ayuda='Texto plano o JSON en una línea, ej: {"host": "x", "user": "y", "pass": "z"}',
@@ -392,7 +410,7 @@ def crear_parametro(stdscr, ssm, entornos, perfil, nombre_perfil, tags_activas,
392
410
  continue
393
411
  break
394
412
 
395
- # 3. Tags obligatorias
413
+ # 5. Tags obligatorias
396
414
  tags = {}
397
415
  faltantes = []
398
416
  if tags_activas:
@@ -406,7 +424,7 @@ def crear_parametro(stdscr, ssm, entornos, perfil, nombre_perfil, tags_activas,
406
424
  predeterminado = entorno if clave == "Environment" else ""
407
425
  respuesta = prompt_input(
408
426
  stdscr,
409
- f"Crear nuevo parámetro (4/4): tags [{indice}/{len(tags_obligatorias)}]",
427
+ f"Crear nuevo parámetro (5/5): tags [{indice}/{len(tags_obligatorias)}]",
410
428
  f"Valor de la tag obligatoria '{clave}':",
411
429
  valor=predeterminado,
412
430
  permitir_vacio=obligatorias_vacias,
@@ -433,7 +451,13 @@ def crear_parametro(stdscr, ssm, entornos, perfil, nombre_perfil, tags_activas,
433
451
  aviso_rds = check_rds_correlacion(ssm, path)
434
452
 
435
453
  # Resumen y confirmación
436
- lineas = [f"Ruta: {path}", f"Valor: {valor[:120]}", "Tipo: SecureString", ""]
454
+ lineas = [
455
+ f"Ruta: {path}",
456
+ f"Descripción: {descripcion or '(vacía)'}",
457
+ f"Valor: {valor[:120]}",
458
+ "Tipo: SecureString",
459
+ "",
460
+ ]
437
461
  if tags_activas:
438
462
  lineas.append("Tags:")
439
463
  lineas.extend([f" {clave} = {valor_tag}" for clave, valor_tag in tags.items()])
@@ -456,6 +480,8 @@ def crear_parametro(stdscr, ssm, entornos, perfil, nombre_perfil, tags_activas,
456
480
  "Type": "SecureString",
457
481
  "Overwrite": False,
458
482
  }
483
+ if descripcion:
484
+ kwargs["Description"] = descripcion
459
485
  if tags_activas and tags:
460
486
  kwargs["Tags"] = [{"Key": k, "Value": v} for k, v in tags.items()]
461
487
 
@@ -512,6 +538,7 @@ def main(stdscr, config=None):
512
538
  TAGS_OBLIGATORIAS = config['tags_obligatorias']
513
539
  OBLIGATORIAS_VACIAS = config.get('obligatorias_vacias', False)
514
540
  FICHERO_POR_RUTA = config.get('fichero_por_ruta', False)
541
+ FORZAR_SECURESTRING = config.get('forzar_securestring', True)
515
542
  PERFIL_NUEVOS = config.get('perfil_nuevos')
516
543
 
517
544
  # Nombre del fichero exportado: con fichero_por_ruta cada ruta tiene el suyo, así que
@@ -565,10 +592,12 @@ def main(stdscr, config=None):
565
592
  continue
566
593
 
567
594
  # Exportar parámetros al archivo principal
568
- export_parameters_to_file(parameters, file_name, TAGS_ACTIVAS, TAGS_OBLIGATORIAS)
595
+ export_parameters_to_file(parameters, file_name, TAGS_ACTIVAS, TAGS_OBLIGATORIAS,
596
+ incluir_tipo=not FORZAR_SECURESTRING)
569
597
 
570
598
  # Crear un respaldo exacto del archivo principal (valores y tags)
571
- export_parameters_to_file(parameters, backup_file_name, TAGS_ACTIVAS, TAGS_OBLIGATORIAS)
599
+ export_parameters_to_file(parameters, backup_file_name, TAGS_ACTIVAS, TAGS_OBLIGATORIAS,
600
+ incluir_tipo=not FORZAR_SECURESTRING)
572
601
 
573
602
  # Confirmación de archivos creados
574
603
  lineas = [
@@ -661,14 +690,30 @@ def main(stdscr, config=None):
661
690
  )
662
691
 
663
692
  try:
664
- if change["value_changed"]:
665
- # Subir o actualizar parámetros
666
- ssm.put_parameter(
667
- Name=param_name,
668
- Value=change["value"],
669
- Type='SecureString',
670
- Overwrite=True
671
- )
693
+ if (change["value_changed"] or change.get("description_changed")
694
+ or change.get("type_changed")):
695
+ kwargs_put = {
696
+ "Name": param_name,
697
+ "Value": change["value"],
698
+ "Overwrite": True,
699
+ }
700
+ # El 'Type' solo es obligatorio al CREAR: al actualizar, si no
701
+ # se manda, AWS conserva el que tuviera el parámetro. Por eso
702
+ # con forzar_securestring = False se omite en las
703
+ # modificaciones en vez de adivinarlo.
704
+ if FORZAR_SECURESTRING:
705
+ kwargs_put["Type"] = "SecureString"
706
+ elif change.get("type"):
707
+ kwargs_put["Type"] = change["type"]
708
+ elif tipo == "Nuevo":
709
+ kwargs_put["Type"] = "SecureString"
710
+ # put_parameter con Overwrite reescribe la definición del
711
+ # parámetro: si no se manda la descripción, la que hubiera en
712
+ # AWS se pierde. Así que se manda siempre que el fichero la
713
+ # traiga, incluso cuando lo que cambió fue solo el valor.
714
+ if change.get("description") is not None:
715
+ kwargs_put["Description"] = change["description"]
716
+ ssm.put_parameter(**kwargs_put)
672
717
  # Los cambios de tags se aplican en la misma pasada que el valor
673
718
  aplicar_cambios_tags(
674
719
  ssm, param_name, change["tags_set"], change["tags_remove"]
@@ -715,7 +760,8 @@ def main(stdscr, config=None):
715
760
  "tags": change["tags"],
716
761
  }
717
762
  export_parameters_to_file(
718
- indice_a_parametros(indice), backup_file_name, TAGS_ACTIVAS, TAGS_OBLIGATORIAS
763
+ indice_a_parametros(indice), backup_file_name, TAGS_ACTIVAS,
764
+ TAGS_OBLIGATORIAS, incluir_tipo=not FORZAR_SECURESTRING
719
765
  )
720
766
 
721
767
  lineas.extend(["", f"Errores ({len(errores)}):"])
@@ -765,6 +811,9 @@ def main(stdscr, config=None):
765
811
  # Ruta sin parámetros para ese entorno: se ignora en el backup total
766
812
  continue
767
813
 
814
+ avisos_totales.extend(
815
+ agregar_descripciones_a_parametros(ssm, parameters, full_path)
816
+ )
768
817
  if TAGS_ACTIVAS:
769
818
  avisos_totales.extend(
770
819
  agregar_tags_a_parametros(ssm, parameters, TAGS_OBLIGATORIAS)
@@ -774,7 +823,8 @@ def main(stdscr, config=None):
774
823
  # Crear archivo de backup total listado
775
824
  backup_file_name = "total_listed_parameters_backup.py"
776
825
  export_parameters_to_file(
777
- all_parameters, backup_file_name, TAGS_ACTIVAS, TAGS_OBLIGATORIAS
826
+ all_parameters, backup_file_name, TAGS_ACTIVAS, TAGS_OBLIGATORIAS,
827
+ incluir_tipo=not FORZAR_SECURESTRING
778
828
  )
779
829
 
780
830
  # Confirmación de backup creado
@@ -796,7 +846,8 @@ def main(stdscr, config=None):
796
846
  # Crear archivo de backup de todos los parámetros
797
847
  backup_file_name = "all_parameters_backup.py"
798
848
  export_parameters_to_file(
799
- parameters, backup_file_name, TAGS_ACTIVAS, TAGS_OBLIGATORIAS
849
+ parameters, backup_file_name, TAGS_ACTIVAS, TAGS_OBLIGATORIAS,
850
+ incluir_tipo=not FORZAR_SECURESTRING
800
851
  )
801
852
  lineas = [
802
853
  f"Backup de todos los parámetros creado: {backup_file_name}",
@@ -829,7 +880,8 @@ def main(stdscr, config=None):
829
880
  # Crear archivo de backup con nombre claro
830
881
  backup_file_name = f"{slug_entrada(selected_param)}_{selected_env}_backup.py"
831
882
  export_parameters_to_file(
832
- parameters, backup_file_name, TAGS_ACTIVAS, TAGS_OBLIGATORIAS
883
+ parameters, backup_file_name, TAGS_ACTIVAS, TAGS_OBLIGATORIAS,
884
+ incluir_tipo=not FORZAR_SECURESTRING
833
885
  )
834
886
 
835
887
  # Confirmación de backup creado
@@ -77,6 +77,16 @@ perfil_nuevos = "min"
77
77
  # segunda lectura te machaque el fichero de la primera.
78
78
  fichero_por_ruta = False
79
79
 
80
+ # ¿Todo lo que sube ParamsX va cifrado como SecureString?
81
+ # True (por defecto) -> sí, siempre. Es lo recomendable: los parámetros de una cuenta
82
+ # suelen tener secretos, y así no depende de acordarse.
83
+ # False -> se respeta el tipo que ya tuviera el parámetro en AWS (String, StringList o
84
+ # SecureString): al actualizar no se manda el tipo, así que AWS conserva el suyo.
85
+ # En este modo el fichero exportado trae además un campo 'parameter_type' que
86
+ # puedes editar si quieres cambiarlo a propósito. Los parámetros que crees con
87
+ # la opción 4 se siguen creando como SecureString.
88
+ forzar_securestring = True
89
+
80
90
 
81
91
  ## Configuraciones Tags
82
92
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: paramsx
3
- Version: 2.2.2
3
+ Version: 2.3.0
4
4
  Summary: Librería para gestionar y respaldar parámetros de AWS SSM de manera sencilla y eficiente.
5
5
  Home-page: https://github.com/pistatxos/paramsx
6
6
  Author: Mariox
@@ -70,7 +70,7 @@ Al ejecutar ```paramsx``` desde la terminal, accedes a un menú interactivo con
70
70
  - Navega por la lista de parámetros configurada en tu archivo de configuración y selecciona cuál descargar.
71
71
  - Elige el entorno deseado (por ejemplo, dev o prod).
72
72
  - Archivos generados:
73
- - parameters_dev.py o parameters_prod.py → Edita estos archivos para modificar, añadir o eliminar parámetros (valores y tags).
73
+ - parameters_dev.py o parameters_prod.py → Edita estos archivos para modificar, añadir o eliminar parámetros (descripciones, valores y tags).
74
74
  - parameters_dev_backup.py → Respaldo automático del archivo descargado, incluidas las tags.
75
75
  Los archivos se crean en la misma ruta desde donde ejecutas paramsx, evitando movernos innecesariamente entre carpetas.
76
76
  - Si tu rol IAM no tiene permisos sobre la ruta configurada, verás un mensaje claro en lugar de una traza de boto3:
@@ -98,6 +98,7 @@ Esto te permitirá mantener respaldos seguros o realizar migraciones/reorganizac
98
98
  4. Crear nuevo parámetro
99
99
  - Crea un parámetro desde cero sin salir de la herramienta. Te pedirá, en este orden:
100
100
  - La ruta **sin el entorno**, igual que la declaras en `parameter_list`: el perfil se encarga de colocarlo. Verás en pantalla la ruta resultante en AWS antes de seguir.
101
+ - La descripción, opcional: para qué sirve el parámetro. Se guarda en AWS y luego se puede editar en el fichero exportado.
101
102
  - El valor: texto plano o JSON en una línea (por ejemplo `{"host": "x", "user": "y", "pass": "z"}`), que se valida antes de continuar.
102
103
  - Las tags obligatorias, si tienes `tags_activas = True`.
103
104
  - El perfil que se usa es el que declares en `perfil_nuevos`. Si tu perfil es de tipo `mixto`, escribe el `*` en la ruta para indicar dónde va el entorno.
@@ -212,6 +213,16 @@ perfil_nuevos = "min"
212
213
  # segunda lectura te machaque el fichero de la primera.
213
214
  fichero_por_ruta = False
214
215
 
216
+ # ¿Todo lo que sube ParamsX va cifrado como SecureString?
217
+ # True (por defecto) -> sí, siempre. Es lo recomendable: los parámetros de una cuenta
218
+ # suelen tener secretos, y así no depende de acordarse.
219
+ # False -> se respeta el tipo que ya tuviera el parámetro en AWS (String, StringList o
220
+ # SecureString): al actualizar no se manda el tipo, así que AWS conserva el suyo.
221
+ # En este modo el fichero exportado trae además un campo 'parameter_type' que
222
+ # puedes editar si quieres cambiarlo a propósito. Los parámetros que crees con
223
+ # la opción 4 se siguen creando como SecureString.
224
+ forzar_securestring = True
225
+
215
226
 
216
227
  ## Configuraciones Tags
217
228
 
@@ -269,6 +280,38 @@ En el nombre van el entorno, la ruta y el perfil, porque dos entradas pueden com
269
280
  | `{"path": "/API/STA", "perfil": "max"}` | `parameters_dev__API_STA__max.py` |
270
281
  | `{"path": "/API/*/STA", "perfil": "mixto_max"}` | `parameters_dev__API_env_STA__mixto_max.py` |
271
282
 
283
+ ### La descripción de los parámetros
284
+
285
+ El fichero exportado trae un campo `parameter_description` **debajo del nombre y antes del valor**. Se lee de AWS, se edita como cualquier otro campo y se aplica al cargar:
286
+
287
+ ```python
288
+ parametros = [
289
+ {'parameter_name': '/dev/api/sta/token',
290
+ 'parameter_description': "Token JWT de la API, rota cada 90 dias",
291
+ 'parameter_value': """abc123"""},
292
+ ]
293
+ ```
294
+
295
+ Dos cosas que conviene saber:
296
+
297
+ - **Hace falta el permiso `ssm:DescribeParameters`.** La descripción no viaja en `GetParametersByPath` como el valor: es un metadato y se pide con `DescribeParameters`. Si tu rol no lo tiene, ParamsX te avisa, el campo **no aparece** en el fichero y al cargar no se toca la descripción que haya en AWS.
298
+ - **Al actualizar un parámetro, AWS reescribe su definición entera.** Por eso ParamsX manda siempre la descripción del fichero cuando cambia el valor, aunque no la hayas tocado: si no la mandara, se perdería. Y como no existe una API para cambiar solo la descripción, editarla crea una versión nueva del parámetro igual que cambiar el valor.
299
+
300
+ Un cambio de descripción se ve en la pantalla de confirmación como `Modificado (descripción)`, y si cambias las dos cosas, `Modificado (valor | descripción)`.
301
+
302
+ ### Cifrado: `forzar_securestring`
303
+
304
+ ParamsX sube todo como **SecureString** por defecto, y así se recomienda dejarlo: los parámetros de una cuenta acaban teniendo secretos, y con esto no depende de que nadie se acuerde.
305
+
306
+ | `forzar_securestring` | Qué hace |
307
+ |---|---|
308
+ | `True` (por defecto) | Todo lo que se sube va cifrado como `SecureString`, sea del tipo que sea en AWS. |
309
+ | `False` | Se respeta el tipo que ya tuviera el parámetro (`String`, `StringList` o `SecureString`): al actualizar no se manda el tipo, y AWS conserva el suyo. El fichero exportado trae además un campo `parameter_type` que puedes editar si quieres cambiarlo a propósito, y ese cambio cuenta como uno más: verás `Modificado (tipo: SecureString)`. |
310
+
311
+ Los parámetros que creas con la opción 4 se crean siempre como `SecureString`, en los dos modos.
312
+
313
+ El tipo se lee de la misma llamada que el valor, así que **no hace falta ningún permiso extra** para esto (a diferencia de la descripción).
314
+
272
315
  ### Cómo acotar tu parameter_list (recomendación)
273
316
 
274
317
  `parameter_list` es solo tu vista de trabajo: **la seguridad real la impone IAM, no este fichero**. La recomendación de uso es:
@@ -300,6 +343,7 @@ Formato del fichero exportado con `tags_activas = True`:
300
343
  ```python
301
344
  parametros = [
302
345
  {'parameter_name': '/dev/email/users',
346
+ 'parameter_description': "Buzon que recibe los avisos de altas",
303
347
  'parameter_value': """agpetrovici@stanalytics.es""",
304
348
  'tagApplication': "",
305
349
  'tagEnvironment': "",
@@ -1 +0,0 @@
1
- __version__ = "2.2.2"
File without changes
File without changes
File without changes
File without changes
File without changes