raijin-server 0.2.26__py3-none-any.whl → 0.2.28__py3-none-any.whl

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.
raijin_server/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """Pacote principal do CLI Raijin Server."""
2
2
 
3
- __version__ = "0.2.26"
3
+ __version__ = "0.2.28"
4
4
 
5
5
  __all__ = ["__version__"]
raijin_server/cli.py CHANGED
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  from pathlib import Path
7
- from typing import Callable, Dict, Optional
7
+ from typing import Callable, Dict, List, Optional
8
8
 
9
9
  import subprocess
10
10
 
@@ -49,6 +49,7 @@ from raijin_server.utils import ExecutionContext, logger, active_log_file, avail
49
49
  from raijin_server.validators import validate_system_requirements, check_module_dependencies, MODULE_DEPENDENCIES
50
50
  from raijin_server.healthchecks import run_health_check
51
51
  from raijin_server.config import ConfigManager
52
+ from raijin_server import module_manager
52
53
 
53
54
  app = typer.Typer(add_completion=False, help="Automacao de setup e hardening para Ubuntu Server")
54
55
  console = Console()
@@ -838,6 +839,257 @@ def validate(skip_root: bool = typer.Option(False, "--skip-root", help="Pula val
838
839
  raise typer.Exit(code=1)
839
840
 
840
841
 
842
+ # ============================================================================
843
+ # Comandos Install / Uninstall / List
844
+ # ============================================================================
845
+
846
+ def _get_available_modules() -> List[str]:
847
+ """Retorna lista de modulos disponiveis (excluindo full_install)."""
848
+ return [m for m in MODULES.keys() if m != "full_install"]
849
+
850
+
851
+ def _register_uninstall_handlers() -> None:
852
+ """Registra handlers de uninstall dos modulos que os possuem."""
853
+ # Mapeamento de modulos para suas funcoes de uninstall
854
+ handlers = {
855
+ "kong": kong._uninstall_kong,
856
+ "grafana": grafana._uninstall_grafana,
857
+ "velero": velero._uninstall_velero,
858
+ "metallb": metallb._uninstall_metallb,
859
+ "minio": minio._uninstall_minio,
860
+ "loki": loki._uninstall_loki,
861
+ "prometheus": lambda ctx: prometheus._uninstall_prometheus(ctx, "monitoring"),
862
+ "traefik": traefik._uninstall_traefik,
863
+ "istio": istio._uninstall_istio,
864
+ "kafka": kafka._uninstall_kafka,
865
+ "cert_manager": cert_manager._uninstall_cert_manager,
866
+ "calico": lambda ctx: _generic_uninstall(ctx, "calico", "calico-system", ["calico"]),
867
+ "secrets": lambda ctx: _uninstall_secrets(ctx),
868
+ "harness": lambda ctx: harness._uninstall_delegate(ctx, "harness", "harness-delegate"),
869
+ }
870
+
871
+ module_manager.UNINSTALL_HANDLERS.update(handlers)
872
+
873
+
874
+ def _uninstall_secrets(ctx: ExecutionContext) -> None:
875
+ """Handler de uninstall para secrets (sealed-secrets + external-secrets)."""
876
+ secrets._uninstall_sealed_secrets(ctx, "sealed-secrets")
877
+ secrets._uninstall_external_secrets(ctx, "external-secrets")
878
+
879
+
880
+ def _generic_uninstall(ctx: ExecutionContext, module: str, namespace: str, releases: List[str]) -> None:
881
+ """Handler generico de uninstall para modulos Helm."""
882
+ for release in releases:
883
+ module_manager.generic_helm_uninstall(release, namespace, ctx)
884
+ module_manager.cleanup_namespace(namespace, ctx)
885
+
886
+
887
+ @app.command()
888
+ def install(
889
+ ctx: typer.Context,
890
+ module: str = typer.Argument(..., help="Nome do modulo a instalar"),
891
+ force: bool = typer.Option(False, "--force", "-f", help="Ignora dependencias faltantes"),
892
+ ) -> None:
893
+ """Instala um modulo especifico.
894
+
895
+ Exemplo:
896
+ raijin-server install kong
897
+ raijin-server install prometheus --force
898
+ """
899
+ available = _get_available_modules()
900
+
901
+ # Normaliza nome do modulo (aceita hifens)
902
+ module_normalized = module.replace("-", "_")
903
+
904
+ if module_normalized not in available:
905
+ typer.secho(f"Modulo '{module}' nao encontrado.", fg=typer.colors.RED)
906
+ typer.echo(f"\nModulos disponiveis:")
907
+ for m in sorted(available):
908
+ typer.echo(f" - {m}")
909
+ raise typer.Exit(code=1)
910
+
911
+ exec_ctx = ctx.obj or ExecutionContext()
912
+
913
+ # Verifica dependencias
914
+ if not force:
915
+ if not check_module_dependencies(module_normalized, exec_ctx):
916
+ typer.echo("\nUse --force para ignorar dependencias faltantes.")
917
+ raise typer.Exit(code=1)
918
+
919
+ # Mostra dependencias
920
+ module_manager.show_dependency_tree(module_normalized)
921
+
922
+ # Confirma instalacao
923
+ if not typer.confirm(f"\nInstalar modulo '{module_normalized}'?", default=True):
924
+ typer.secho("Instalacao cancelada.", fg=typer.colors.YELLOW)
925
+ raise typer.Exit(code=0)
926
+
927
+ # Executa instalacao
928
+ _run_module(ctx, module_normalized)
929
+
930
+
931
+ @app.command()
932
+ def uninstall(
933
+ ctx: typer.Context,
934
+ module: str = typer.Argument(..., help="Nome do modulo a desinstalar"),
935
+ force: bool = typer.Option(False, "--force", "-f", help="Ignora avisos de seguranca"),
936
+ cascade: bool = typer.Option(False, "--cascade", help="Remove tambem modulos dependentes"),
937
+ yes: bool = typer.Option(False, "--yes", "-y", help="Confirma automaticamente"),
938
+ ) -> None:
939
+ """Desinstala um modulo e seus recursos.
940
+
941
+ Analisa dependencias e mostra impacto antes de remover.
942
+
943
+ Exemplo:
944
+ raijin-server uninstall kong
945
+ raijin-server uninstall kubernetes --cascade
946
+ raijin-server uninstall prometheus --force -y
947
+ """
948
+ available = _get_available_modules()
949
+
950
+ # Normaliza nome do modulo
951
+ module_normalized = module.replace("-", "_")
952
+
953
+ if module_normalized not in available:
954
+ typer.secho(f"Modulo '{module}' nao encontrado.", fg=typer.colors.RED)
955
+ raise typer.Exit(code=1)
956
+
957
+ # Registra handlers de uninstall
958
+ _register_uninstall_handlers()
959
+
960
+ exec_ctx = ctx.obj or ExecutionContext()
961
+
962
+ # Verifica se esta instalado
963
+ if not module_manager.is_module_installed(module_normalized):
964
+ typer.secho(f"Modulo '{module_normalized}' nao esta instalado.", fg=typer.colors.YELLOW)
965
+
966
+ # Mesmo assim, oferece limpar recursos
967
+ if typer.confirm("Deseja tentar limpar recursos orfaos mesmo assim?", default=False):
968
+ if module_normalized in module_manager.UNINSTALL_HANDLERS:
969
+ module_manager.UNINSTALL_HANDLERS[module_normalized](exec_ctx)
970
+ typer.secho("Limpeza concluida.", fg=typer.colors.GREEN)
971
+ raise typer.Exit(code=0)
972
+
973
+ # Mostra arvore de dependencias
974
+ module_manager.show_dependency_tree(module_normalized)
975
+
976
+ # Analisa impacto
977
+ is_safe, affected = module_manager.show_uninstall_impact(module_normalized)
978
+
979
+ if not is_safe and not force:
980
+ typer.secho(
981
+ f"\n⚠️ ATENCAO: Remover '{module_normalized}' pode quebrar {len(affected)} modulo(s)!",
982
+ fg=typer.colors.YELLOW,
983
+ bold=True,
984
+ )
985
+
986
+ if cascade:
987
+ typer.echo(f"\nModo --cascade: os seguintes modulos serao removidos primeiro:")
988
+ for dep in affected:
989
+ typer.echo(f" - {dep}")
990
+
991
+ # Confirmacao
992
+ if not yes:
993
+ if cascade and affected:
994
+ confirm_msg = f"Remover '{module_normalized}' e {len(affected)} dependente(s)?"
995
+ else:
996
+ confirm_msg = f"Remover '{module_normalized}'?"
997
+
998
+ if not typer.confirm(f"\n{confirm_msg}", default=False):
999
+ typer.secho("Operacao cancelada.", fg=typer.colors.YELLOW)
1000
+ raise typer.Exit(code=0)
1001
+
1002
+ # Executa uninstall
1003
+ success = module_manager.uninstall_module(
1004
+ module_normalized,
1005
+ exec_ctx,
1006
+ force=force,
1007
+ cascade=cascade,
1008
+ )
1009
+
1010
+ if success:
1011
+ typer.secho(f"\n✓ Modulo '{module_normalized}' removido com sucesso!", fg=typer.colors.GREEN, bold=True)
1012
+ else:
1013
+ typer.secho(f"\n✗ Falha ao remover modulo '{module_normalized}'.", fg=typer.colors.RED)
1014
+ raise typer.Exit(code=1)
1015
+
1016
+
1017
+ @app.command(name="list")
1018
+ def list_modules(
1019
+ ctx: typer.Context,
1020
+ installed_only: bool = typer.Option(False, "--installed", "-i", help="Mostra apenas modulos instalados"),
1021
+ show_deps: bool = typer.Option(False, "--deps", "-d", help="Mostra dependencias detalhadas"),
1022
+ ) -> None:
1023
+ """Lista todos os modulos e seus status de instalacao.
1024
+
1025
+ Exemplo:
1026
+ raijin-server list
1027
+ raijin-server list --installed
1028
+ raijin-server list --deps
1029
+ """
1030
+ from rich.table import Table
1031
+
1032
+ table = Table(title="Modulos Raijin Server")
1033
+ table.add_column("Modulo", style="cyan")
1034
+ table.add_column("Status", justify="center")
1035
+
1036
+ if show_deps:
1037
+ table.add_column("Dependencias", style="yellow")
1038
+ table.add_column("Dependentes", style="magenta")
1039
+
1040
+ table.add_column("Descricao", style="dim", max_width=40)
1041
+
1042
+ status = module_manager.get_module_status()
1043
+
1044
+ for module_name in sorted(status.keys()):
1045
+ installed = status[module_name]
1046
+
1047
+ if installed_only and not installed:
1048
+ continue
1049
+
1050
+ status_icon = "[green]✓ Instalado[/green]" if installed else "[dim]○ Pendente[/dim]"
1051
+ desc = MODULE_DESCRIPTIONS.get(module_name, "")
1052
+
1053
+ if show_deps:
1054
+ deps = MODULE_DEPENDENCIES.get(module_name, [])
1055
+ deps_str = ", ".join(deps) if deps else "-"
1056
+
1057
+ from raijin_server.validators import get_reverse_dependencies
1058
+ rev_deps = get_reverse_dependencies(module_name)
1059
+ rev_deps_str = ", ".join(rev_deps) if rev_deps else "-"
1060
+
1061
+ table.add_row(module_name, status_icon, deps_str, rev_deps_str, desc)
1062
+ else:
1063
+ table.add_row(module_name, status_icon, desc)
1064
+
1065
+ console.print(table)
1066
+
1067
+ # Resumo
1068
+ total = len(status)
1069
+ installed_count = sum(1 for v in status.values() if v)
1070
+ console.print(f"\n[dim]Total: {total} modulos | Instalados: {installed_count} | Pendentes: {total - installed_count}[/dim]")
1071
+
1072
+
1073
+ @app.command()
1074
+ def deps(
1075
+ module: str = typer.Argument(..., help="Nome do modulo"),
1076
+ ) -> None:
1077
+ """Mostra arvore de dependencias de um modulo.
1078
+
1079
+ Exemplo:
1080
+ raijin-server deps grafana
1081
+ raijin-server deps kong
1082
+ """
1083
+ available = _get_available_modules()
1084
+ module_normalized = module.replace("-", "_")
1085
+
1086
+ if module_normalized not in available:
1087
+ typer.secho(f"Modulo '{module}' nao encontrado.", fg=typer.colors.RED)
1088
+ raise typer.Exit(code=1)
1089
+
1090
+ module_manager.show_dependency_tree(module_normalized)
1091
+
1092
+
841
1093
  def main_entrypoint() -> None:
842
1094
  """Ponto de entrada para console_scripts."""
843
1095
 
@@ -0,0 +1,311 @@
1
+ """Gerenciador de instalacao e desinstalacao de modulos."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Callable, Dict, List, Optional, Tuple
8
+
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+
14
+ from raijin_server.utils import ExecutionContext, run_cmd, logger
15
+ from raijin_server.validators import (
16
+ MODULE_DEPENDENCIES,
17
+ get_reverse_dependencies,
18
+ get_installed_dependents,
19
+ check_uninstall_safety,
20
+ check_module_dependencies,
21
+ )
22
+
23
+ console = Console()
24
+
25
+
26
+ # Mapeamento de modulos para suas funcoes de uninstall
27
+ # Sera populado dinamicamente pelo CLI
28
+ UNINSTALL_HANDLERS: Dict[str, Callable[[ExecutionContext], None]] = {}
29
+
30
+
31
+ def get_state_dir() -> Path:
32
+ """Retorna diretorio de estado."""
33
+ state_dir = Path(os.environ.get("RAIJIN_STATE_DIR", "/var/lib/raijin-server/state"))
34
+ if state_dir.exists():
35
+ return state_dir
36
+ alt_dir = Path.home() / ".local/share/raijin-server/state"
37
+ return alt_dir
38
+
39
+
40
+ def is_module_installed(module: str) -> bool:
41
+ """Verifica se um modulo esta instalado (tem arquivo .done)."""
42
+ state_dir = get_state_dir()
43
+ state_file = state_dir / f"{module}.done"
44
+ return state_file.exists()
45
+
46
+
47
+ def mark_module_uninstalled(module: str) -> None:
48
+ """Remove marcador de instalacao do modulo."""
49
+ state_dir = get_state_dir()
50
+ state_file = state_dir / f"{module}.done"
51
+ if state_file.exists():
52
+ state_file.unlink()
53
+ logger.info(f"Marcador de estado removido: {state_file}")
54
+
55
+
56
+ def get_module_status() -> Dict[str, bool]:
57
+ """Retorna status de instalacao de todos os modulos."""
58
+ from raijin_server.cli import MODULES
59
+
60
+ status = {}
61
+ for module in MODULES.keys():
62
+ if module == "full_install":
63
+ continue
64
+ status[module] = is_module_installed(module)
65
+ return status
66
+
67
+
68
+ def show_dependency_tree(module: str) -> None:
69
+ """Exibe arvore de dependencias de um modulo."""
70
+ console.print(f"\n[bold cyan]Arvore de dependencias para '{module}':[/bold cyan]")
71
+
72
+ # Dependencias (o que este modulo precisa)
73
+ deps = MODULE_DEPENDENCIES.get(module, [])
74
+ if deps:
75
+ console.print(f"\n[yellow]Requer (dependencias):[/yellow]")
76
+ for dep in deps:
77
+ installed = "✓" if is_module_installed(dep) else "✗"
78
+ color = "green" if is_module_installed(dep) else "red"
79
+ console.print(f" [{color}]{installed}[/{color}] {dep}")
80
+ else:
81
+ console.print(f"\n[dim]Nenhuma dependencia[/dim]")
82
+
83
+ # Dependentes (quem depende deste modulo)
84
+ dependents = get_reverse_dependencies(module)
85
+ if dependents:
86
+ console.print(f"\n[yellow]Dependentes (quem usa este modulo):[/yellow]")
87
+ for dep in dependents:
88
+ installed = "✓" if is_module_installed(dep) else "○"
89
+ color = "green" if is_module_installed(dep) else "dim"
90
+ console.print(f" [{color}]{installed}[/{color}] {dep}")
91
+ else:
92
+ console.print(f"\n[dim]Nenhum modulo depende deste[/dim]")
93
+
94
+
95
+ def show_uninstall_impact(module: str) -> Tuple[bool, List[str]]:
96
+ """Mostra impacto da remocao e retorna se e seguro."""
97
+ is_safe, affected, warning = check_uninstall_safety(module)
98
+
99
+ if not is_safe:
100
+ console.print(Panel(
101
+ warning,
102
+ title="⚠️ AVISO DE IMPACTO",
103
+ border_style="yellow",
104
+ ))
105
+
106
+ # Mostra arvore de impacto detalhada
107
+ table = Table(title="Modulos que serao afetados")
108
+ table.add_column("Modulo", style="red")
109
+ table.add_column("Status", style="yellow")
110
+ table.add_column("Depende de", style="cyan")
111
+
112
+ for mod in affected:
113
+ deps = MODULE_DEPENDENCIES.get(mod, [])
114
+ relevant_deps = [d for d in deps if d == module or d in affected]
115
+ table.add_row(
116
+ mod,
117
+ "Instalado" if is_module_installed(mod) else "Nao instalado",
118
+ ", ".join(relevant_deps)
119
+ )
120
+
121
+ console.print(table)
122
+ console.print()
123
+
124
+ return is_safe, affected
125
+
126
+
127
+ def uninstall_module(
128
+ module: str,
129
+ ctx: ExecutionContext,
130
+ force: bool = False,
131
+ cascade: bool = False,
132
+ ) -> bool:
133
+ """Desinstala um modulo com verificacao de seguranca.
134
+
135
+ Args:
136
+ module: Nome do modulo a desinstalar
137
+ ctx: Contexto de execucao
138
+ force: Ignora avisos de seguranca
139
+ cascade: Remove tambem os modulos dependentes
140
+
141
+ Returns:
142
+ True se desinstalou com sucesso
143
+ """
144
+ if not is_module_installed(module):
145
+ typer.secho(f"Modulo '{module}' nao esta instalado.", fg=typer.colors.YELLOW)
146
+ return False
147
+
148
+ # Verifica seguranca
149
+ is_safe, affected = show_uninstall_impact(module)
150
+
151
+ if not is_safe and not force:
152
+ if cascade:
153
+ # Desinstala dependentes primeiro (ordem reversa)
154
+ console.print(f"\n[yellow]Modo cascade: removendo dependentes primeiro...[/yellow]")
155
+ for dep in reversed(affected):
156
+ if is_module_installed(dep):
157
+ console.print(f"\n[cyan]Removendo {dep}...[/cyan]")
158
+ uninstall_module(dep, ctx, force=True, cascade=False)
159
+ else:
160
+ confirm = typer.confirm(
161
+ "\nDeseja continuar mesmo assim? (os modulos afetados podem parar de funcionar)",
162
+ default=False,
163
+ )
164
+ if not confirm:
165
+ typer.secho("Operacao cancelada.", fg=typer.colors.YELLOW)
166
+ return False
167
+
168
+ # Executa uninstall especifico se disponivel
169
+ if module in UNINSTALL_HANDLERS:
170
+ typer.echo(f"\nExecutando uninstall de '{module}'...")
171
+ try:
172
+ UNINSTALL_HANDLERS[module](ctx)
173
+ except Exception as e:
174
+ typer.secho(f"Erro durante uninstall: {e}", fg=typer.colors.RED)
175
+ if not force:
176
+ return False
177
+ else:
178
+ # Uninstall generico - apenas remove marcador
179
+ typer.secho(
180
+ f"Modulo '{module}' nao tem handler de uninstall especifico.",
181
+ fg=typer.colors.YELLOW,
182
+ )
183
+ typer.echo("Apenas o marcador de estado sera removido.")
184
+ typer.echo("Recursos no cluster podem precisar de remocao manual.")
185
+
186
+ # Remove marcador de estado
187
+ mark_module_uninstalled(module)
188
+ typer.secho(f"✓ Modulo '{module}' marcado como desinstalado.", fg=typer.colors.GREEN)
189
+
190
+ return True
191
+
192
+
193
+ def list_modules_status() -> None:
194
+ """Lista todos os modulos e seus status."""
195
+ from raijin_server.cli import MODULE_DESCRIPTIONS
196
+
197
+ table = Table(title="Status dos Modulos Raijin")
198
+ table.add_column("Modulo", style="cyan")
199
+ table.add_column("Status", justify="center")
200
+ table.add_column("Dependencias")
201
+ table.add_column("Descricao", style="dim")
202
+
203
+ status = get_module_status()
204
+
205
+ for module, installed in sorted(status.items()):
206
+ status_icon = "[green]✓ Instalado[/green]" if installed else "[dim]○ Nao instalado[/dim]"
207
+ deps = MODULE_DEPENDENCIES.get(module, [])
208
+ deps_str = ", ".join(deps) if deps else "-"
209
+ desc = MODULE_DESCRIPTIONS.get(module, "")[:40]
210
+ if len(MODULE_DESCRIPTIONS.get(module, "")) > 40:
211
+ desc += "..."
212
+
213
+ table.add_row(module, status_icon, deps_str, desc)
214
+
215
+ console.print(table)
216
+
217
+
218
+ def check_kubernetes_resource(resource_type: str, name: str, namespace: str = "") -> bool:
219
+ """Verifica se um recurso Kubernetes existe."""
220
+ cmd = ["kubectl", "get", resource_type, name]
221
+ if namespace:
222
+ cmd.extend(["-n", namespace])
223
+
224
+ import subprocess
225
+ result = subprocess.run(cmd, capture_output=True, text=True)
226
+ return result.returncode == 0
227
+
228
+
229
+ def generic_helm_uninstall(release_name: str, namespace: str, ctx: ExecutionContext) -> bool:
230
+ """Desinstala um release Helm de forma generica."""
231
+ typer.echo(f"Removendo release Helm '{release_name}' do namespace '{namespace}'...")
232
+
233
+ # Verifica se existe
234
+ result = run_cmd(
235
+ ["helm", "status", release_name, "-n", namespace],
236
+ ctx,
237
+ check=False,
238
+ )
239
+
240
+ if result.returncode != 0:
241
+ typer.echo(f"Release '{release_name}' nao encontrado.")
242
+ return False
243
+
244
+ # Remove release
245
+ result = run_cmd(
246
+ ["helm", "uninstall", release_name, "-n", namespace],
247
+ ctx,
248
+ check=False,
249
+ )
250
+
251
+ if result.returncode == 0:
252
+ typer.secho(f"✓ Release '{release_name}' removido.", fg=typer.colors.GREEN)
253
+ return True
254
+ else:
255
+ typer.secho(f"✗ Falha ao remover release '{release_name}'.", fg=typer.colors.RED)
256
+ return False
257
+
258
+
259
+ def cleanup_namespace(namespace: str, ctx: ExecutionContext, wait: bool = True) -> bool:
260
+ """Remove um namespace do Kubernetes."""
261
+ typer.echo(f"Removendo namespace '{namespace}'...")
262
+
263
+ result = run_cmd(
264
+ ["kubectl", "delete", "namespace", namespace, "--ignore-not-found"],
265
+ ctx,
266
+ check=False,
267
+ )
268
+
269
+ if wait and result.returncode == 0:
270
+ import time
271
+ # Aguarda namespace ser removido
272
+ deadline = time.time() + 60
273
+ while time.time() < deadline:
274
+ check = run_cmd(
275
+ ["kubectl", "get", "namespace", namespace],
276
+ ctx,
277
+ check=False,
278
+ )
279
+ if check.returncode != 0:
280
+ break
281
+ time.sleep(5)
282
+
283
+ return result.returncode == 0
284
+
285
+
286
+ def cleanup_crds(pattern: str, ctx: ExecutionContext) -> int:
287
+ """Remove CRDs que correspondem ao padrao."""
288
+ import subprocess
289
+
290
+ # Lista CRDs
291
+ result = subprocess.run(
292
+ ["kubectl", "get", "crd", "-o", "name"],
293
+ capture_output=True,
294
+ text=True,
295
+ )
296
+
297
+ if result.returncode != 0:
298
+ return 0
299
+
300
+ removed = 0
301
+ for line in result.stdout.strip().split("\n"):
302
+ if pattern in line:
303
+ crd_name = line.replace("customresourcedefinition.apiextensions.k8s.io/", "")
304
+ run_cmd(
305
+ ["kubectl", "delete", "crd", crd_name, "--ignore-not-found"],
306
+ ctx,
307
+ check=False,
308
+ )
309
+ removed += 1
310
+
311
+ return removed
@@ -52,7 +52,7 @@ def _check_existing_kong(ctx: ExecutionContext) -> bool:
52
52
 
53
53
 
54
54
  def _check_orphan_crds(ctx: ExecutionContext) -> list[str]:
55
- """Detecta CRDs orfaos do Kong (sem ownership do Helm)."""
55
+ """Detecta CRDs do Kong que existem sem ownership do Helm."""
56
56
  result = run_cmd(
57
57
  ["kubectl", "get", "crd", "-o", "name"],
58
58
  ctx,
@@ -72,19 +72,71 @@ def _check_orphan_crds(ctx: ExecutionContext) -> list[str]:
72
72
  return kong_crds
73
73
 
74
74
 
75
- def _cleanup_orphan_crds(ctx: ExecutionContext, crds: list[str]) -> None:
76
- """Remove CRDs orfaos do Kong."""
77
- typer.echo(f"Removendo {len(crds)} CRDs orfaos do Kong...")
75
+ def _adopt_crds_for_helm(ctx: ExecutionContext, crds: list[str]) -> bool:
76
+ """Adiciona labels do Helm aos CRDs existentes para permitir adocao."""
77
+ typer.echo(f"Adicionando labels Helm a {len(crds)} CRDs existentes...")
78
78
 
79
79
  for crd in crds:
80
+ # Adiciona label managed-by
80
81
  run_cmd(
81
- ["kubectl", "delete", "crd", crd, "--ignore-not-found"],
82
+ ["kubectl", "label", "crd", crd, "app.kubernetes.io/managed-by=Helm", "--overwrite"],
83
+ ctx,
84
+ check=False,
85
+ )
86
+ # Adiciona annotations de release
87
+ run_cmd(
88
+ ["kubectl", "annotate", "crd", crd, "meta.helm.sh/release-name=kong", "--overwrite"],
89
+ ctx,
90
+ check=False,
91
+ )
92
+ run_cmd(
93
+ ["kubectl", "annotate", "crd", crd, "meta.helm.sh/release-namespace=kong", "--overwrite"],
82
94
  ctx,
83
95
  check=False,
84
96
  )
85
97
 
86
- time.sleep(3)
87
- typer.secho(" CRDs orfaos removidos.", fg=typer.colors.GREEN)
98
+ typer.secho(" ✓ CRDs preparados para adocao pelo Helm.", fg=typer.colors.GREEN)
99
+ return True
100
+
101
+
102
+ def _cleanup_orphan_crds(ctx: ExecutionContext, crds: list[str]) -> bool:
103
+ """Remove CRDs do Kong completamente."""
104
+ typer.echo(f"Removendo {len(crds)} CRDs do Kong...")
105
+
106
+ for crd in crds:
107
+ run_cmd(
108
+ ["kubectl", "delete", "crd", crd, "--ignore-not-found", "--wait=true"],
109
+ ctx,
110
+ check=False,
111
+ )
112
+
113
+ # Aguarda e verifica se foram realmente removidos
114
+ typer.echo(" Aguardando remocao completa dos CRDs...")
115
+ max_attempts = 10
116
+ for attempt in range(max_attempts):
117
+ time.sleep(2)
118
+ remaining = _check_orphan_crds(ctx)
119
+ if not remaining:
120
+ typer.secho(" ✓ CRDs removidos com sucesso.", fg=typer.colors.GREEN)
121
+ return True
122
+
123
+ if attempt < max_attempts - 1:
124
+ typer.echo(f" Ainda restam {len(remaining)} CRDs. Tentando remover novamente...")
125
+ for crd in remaining:
126
+ run_cmd(
127
+ ["kubectl", "delete", "crd", crd, "--ignore-not-found", "--wait=true", "--timeout=30s"],
128
+ ctx,
129
+ check=False,
130
+ )
131
+
132
+ remaining = _check_orphan_crds(ctx)
133
+ if remaining:
134
+ typer.secho(f" ⚠️ {len(remaining)} CRDs ainda existem:", fg=typer.colors.YELLOW)
135
+ for crd in remaining[:5]:
136
+ typer.echo(f" - {crd}")
137
+ return False
138
+
139
+ return True
88
140
 
89
141
 
90
142
  def _uninstall_kong(ctx: ExecutionContext) -> None:
@@ -158,29 +210,42 @@ def run(ctx: ExecutionContext) -> None:
158
210
  if cleanup:
159
211
  _uninstall_kong(ctx)
160
212
 
161
- # Verificar CRDs orfaos (sem ownership do Helm)
162
- orphan_crds = _check_orphan_crds(ctx)
163
- if orphan_crds:
213
+ # Verificar CRDs existentes do Kong (sem ownership do Helm)
214
+ existing_crds = _check_orphan_crds(ctx)
215
+ skip_crds = False
216
+
217
+ if existing_crds:
164
218
  typer.secho(
165
- f"\n⚠️ Detectados {len(orphan_crds)} CRDs orfaos do Kong (sem ownership do Helm):",
219
+ f"\n⚠️ Detectados {len(existing_crds)} CRDs do Kong sem labels do Helm:",
166
220
  fg=typer.colors.YELLOW,
167
221
  )
168
- for crd in orphan_crds[:5]:
222
+ for crd in existing_crds[:5]:
169
223
  typer.echo(f" - {crd}")
170
- if len(orphan_crds) > 5:
171
- typer.echo(f" ... e mais {len(orphan_crds) - 5}")
224
+ if len(existing_crds) > 5:
225
+ typer.echo(f" ... e mais {len(existing_crds) - 5}")
172
226
 
173
- cleanup_crds = typer.confirm(
174
- "\nRemover CRDs orfaos para permitir instalacao limpa?",
175
- default=True,
176
- )
177
- if cleanup_crds:
178
- _cleanup_orphan_crds(ctx, orphan_crds)
227
+ typer.echo("\nOpcoes:")
228
+ typer.echo(" 1. Adotar CRDs existentes (adicionar labels Helm) - RECOMENDADO")
229
+ typer.echo(" 2. Deletar CRDs e deixar Helm recriar")
230
+ typer.echo(" 3. Ignorar CRDs (usar --skip-crds)")
231
+ typer.echo(" 4. Cancelar instalacao")
232
+
233
+ choice = typer.prompt("Escolha", default="1")
234
+
235
+ if choice == "1":
236
+ _adopt_crds_for_helm(ctx, existing_crds)
237
+ elif choice == "2":
238
+ cleanup_success = _cleanup_orphan_crds(ctx, existing_crds)
239
+ if not cleanup_success:
240
+ if not typer.confirm("CRDs ainda existem. Continuar mesmo assim?", default=False):
241
+ typer.secho("Instalacao cancelada.", fg=typer.colors.RED)
242
+ return
243
+ elif choice == "3":
244
+ skip_crds = True
245
+ typer.secho("Helm não gerenciará os CRDs existentes.", fg=typer.colors.YELLOW)
179
246
  else:
180
- typer.secho(
181
- "AVISO: A instalacao pode falhar devido aos CRDs orfaos.",
182
- fg=typer.colors.YELLOW,
183
- )
247
+ typer.secho("Instalacao cancelada.", fg=typer.colors.RED)
248
+ return
184
249
 
185
250
  # Detectar dependencias
186
251
  has_metallb = _check_metallb_installed(ctx)
@@ -204,7 +269,7 @@ def run(ctx: ExecutionContext) -> None:
204
269
  enable_metrics = typer.confirm("Habilitar métricas Prometheus?", default=True)
205
270
  db_mode = typer.prompt(
206
271
  "Modo de banco de dados (dbless/postgres)",
207
- default="dbless",
272
+ default="postgres",
208
273
  )
209
274
 
210
275
  node_name = _detect_node_name(ctx)
@@ -273,6 +338,11 @@ podAnnotations:
273
338
 
274
339
  run_cmd(["kubectl", "create", "namespace", "kong"], ctx, check=False)
275
340
 
341
+ # Monta args extras para o helm
342
+ extra_args = ["-f", str(values_path)]
343
+ if skip_crds:
344
+ extra_args.append("--skip-crds")
345
+
276
346
  helm_upgrade_install(
277
347
  release="kong",
278
348
  chart="kong",
@@ -281,7 +351,7 @@ podAnnotations:
281
351
  repo_url="https://charts.konghq.com",
282
352
  ctx=ctx,
283
353
  values=[],
284
- extra_args=["-f", str(values_path)],
354
+ extra_args=extra_args,
285
355
  )
286
356
 
287
357
  # Aguarda pods ficarem prontos
@@ -269,3 +269,80 @@ def check_module_dependencies(module: str, ctx: ExecutionContext) -> bool:
269
269
  return False
270
270
 
271
271
  return True
272
+
273
+
274
+ def get_reverse_dependencies(module: str) -> List[str]:
275
+ """Retorna lista de modulos que dependem do modulo especificado.
276
+
277
+ Args:
278
+ module: Nome do modulo para verificar dependencias reversas
279
+
280
+ Returns:
281
+ Lista de modulos que dependem deste modulo
282
+ """
283
+ dependents = []
284
+ for mod, deps in MODULE_DEPENDENCIES.items():
285
+ if module in deps:
286
+ dependents.append(mod)
287
+ return dependents
288
+
289
+
290
+ def get_installed_dependents(module: str) -> List[str]:
291
+ """Retorna lista de modulos instalados que dependem do modulo especificado.
292
+
293
+ Args:
294
+ module: Nome do modulo para verificar
295
+
296
+ Returns:
297
+ Lista de modulos instalados que dependem deste modulo
298
+ """
299
+ state_dir = Path(os.environ.get("RAIJIN_STATE_DIR", "/var/lib/raijin-server/state"))
300
+ if not state_dir.exists():
301
+ state_dir = Path.home() / ".local/share/raijin-server/state"
302
+
303
+ dependents = get_reverse_dependencies(module)
304
+ installed = []
305
+
306
+ for dep in dependents:
307
+ state_file = state_dir / f"{dep}.done"
308
+ if state_file.exists():
309
+ installed.append(dep)
310
+
311
+ return installed
312
+
313
+
314
+ def check_uninstall_safety(module: str) -> Tuple[bool, List[str], str]:
315
+ """Verifica se e seguro remover um modulo.
316
+
317
+ Args:
318
+ module: Nome do modulo a ser removido
319
+
320
+ Returns:
321
+ Tupla (is_safe, affected_modules, warning_message)
322
+ """
323
+ installed_dependents = get_installed_dependents(module)
324
+
325
+ if not installed_dependents:
326
+ return True, [], ""
327
+
328
+ # Constroi arvore de impacto (recursivo)
329
+ all_affected = set(installed_dependents)
330
+ to_check = list(installed_dependents)
331
+
332
+ while to_check:
333
+ current = to_check.pop(0)
334
+ sub_dependents = get_installed_dependents(current)
335
+ for sub in sub_dependents:
336
+ if sub not in all_affected:
337
+ all_affected.add(sub)
338
+ to_check.append(sub)
339
+
340
+ affected_list = sorted(list(all_affected))
341
+
342
+ warning = f"AVISO: Remover '{module}' afetara os seguintes modulos instalados:\n"
343
+ for dep in affected_list:
344
+ warning += f" - {dep}\n"
345
+ warning += "\nEstes modulos podem parar de funcionar corretamente!"
346
+
347
+ return False, affected_list, warning
348
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: raijin-server
3
- Version: 0.2.26
3
+ Version: 0.2.28
4
4
  Summary: CLI para automacao de setup e hardening de servidores Ubuntu Server.
5
5
  Home-page: https://example.com/raijin-server
6
6
  Author: Equipe Raijin
@@ -1,9 +1,10 @@
1
- raijin_server/__init__.py,sha256=eyoihT94C2L9-FRAopoagny015Laj3hhZ94X-WZ_hvg,95
2
- raijin_server/cli.py,sha256=71nn7QN0f3MJkXcHr0STXmxljr-CaPibzOoiItbOT88,28571
1
+ raijin_server/__init__.py,sha256=WPj_zYPlDDdC8qb99tlMy-WAu5vWOQOyCldFgDmmT9Q,95
2
+ raijin_server/cli.py,sha256=2m7q1znMLbBdnUwN6oOUrCZXEqC2e7SfbjYkymbP4lQ,37884
3
3
  raijin_server/config.py,sha256=QNiEVvrbW56XgvNn5-h3bkJm46Xc8mjNqPbvixXD8N0,4829
4
4
  raijin_server/healthchecks.py,sha256=lzXdFw6S0hOYbUKbqksh4phb04lXgXdTspP1Dsz4dx8,15401
5
+ raijin_server/module_manager.py,sha256=Wmhj603CN0XGUVr7_Fo8CHzKd9yIbS9x5BJLqDj78kw,10259
5
6
  raijin_server/utils.py,sha256=9RnGnPoUTYOpMVRLNa4P4lIQrJNQLkSkPUxycZRGv78,20827
6
- raijin_server/validators.py,sha256=5SpGz0aDmt60v19KJQSU-VpQOVc3WyNJQn45acUhq0Q,9463
7
+ raijin_server/validators.py,sha256=EATYPy2pllAb6IX4gUZKnELvospWwyGV3DHrzxb_RMg,11761
7
8
  raijin_server/modules/__init__.py,sha256=e_IbkhLGPcF8to9QUmIESP6fpcTOYcIhaXLKIvqRJMY,920
8
9
  raijin_server/modules/apokolips_demo.py,sha256=8ltsXRbVDwlDwLMIvh02NG-FeAfBWw_v6lh7IGOyNqs,13725
9
10
  raijin_server/modules/bootstrap.py,sha256=oVIGNRW_JbgY8zXNHGAIP0vGbbHNHyQexthxo5zhbcw,9762
@@ -17,7 +18,7 @@ raijin_server/modules/hardening.py,sha256=4hz3ifkMhPlXa2n7gPxN0gitQgzALZ-073vuU3
17
18
  raijin_server/modules/harness.py,sha256=uWTxTVJlY_VB6xi4ftMtTSaIb96HA8WJQS-RbyxU45M,5391
18
19
  raijin_server/modules/istio.py,sha256=o0K5-Fw4LRs-kbAVgwzYxHzEt_aPFJG8suqOqvg2748,7297
19
20
  raijin_server/modules/kafka.py,sha256=n7ZpLPWv6sKBJhdBiPe7VgeDB24YiCIOWvOQkWwt03Y,5664
20
- raijin_server/modules/kong.py,sha256=ehr-Bj_zfvFrYV14YhvKpb-k8KFli6sbaKn6WSIaSvA,9823
21
+ raijin_server/modules/kong.py,sha256=5KCGcIhnEkHHwYEDEDdUTaoDCUwn7HKaZetoqOBlP3E,12553
21
22
  raijin_server/modules/kubernetes.py,sha256=9E6zV0zGQWZW92NVpxwYctpi-4JDmi6YzF3tKRI4HlU,13343
22
23
  raijin_server/modules/loki.py,sha256=aNiUpnOFppZMXoQwYhn7IoPMzwUz4aHi6pbiqj1PRjc,5022
23
24
  raijin_server/modules/metallb.py,sha256=uUuklc_RsQ-W2qDVRMQAxQm9HKGEqso444b1IwBpM6w,8554
@@ -37,9 +38,9 @@ raijin_server/scripts/checklist.sh,sha256=j6E0Kmk1EfjLvKK1VpCqzXJAXI_7Bm67LK4ndy
37
38
  raijin_server/scripts/install.sh,sha256=Y1ickbQ4siQ0NIPs6UgrqUr8WWy7U0LHmaTQbEgavoI,3949
38
39
  raijin_server/scripts/log_size_metric.sh,sha256=Iv4SsX8AuCYRou-klYn32mX41xB6j0xJGLBO6riw4rU,1208
39
40
  raijin_server/scripts/pre-deploy-check.sh,sha256=XqMo7IMIpwUHF17YEmU0-cVmTDMoCGMBFnmS39FidI4,4912
40
- raijin_server-0.2.26.dist-info/licenses/LICENSE,sha256=kJsMCjOiRZE0AQNtxWqBa32z9kMAaF4EUxyHj3hKaJo,1105
41
- raijin_server-0.2.26.dist-info/METADATA,sha256=viulP29E95tjwFrGpTBDrk3kQw3MrtFBB5plAWcL1Xc,22476
42
- raijin_server-0.2.26.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
43
- raijin_server-0.2.26.dist-info/entry_points.txt,sha256=3ZvxDX4pvcjkIRsXAJ69wIfVmKa78LKo-C3QhqN2KVM,56
44
- raijin_server-0.2.26.dist-info/top_level.txt,sha256=Yz1xneCRtsZOzbPIcTAcrSxd-1p80pohMXYAZ74dpok,14
45
- raijin_server-0.2.26.dist-info/RECORD,,
41
+ raijin_server-0.2.28.dist-info/licenses/LICENSE,sha256=kJsMCjOiRZE0AQNtxWqBa32z9kMAaF4EUxyHj3hKaJo,1105
42
+ raijin_server-0.2.28.dist-info/METADATA,sha256=hVqz4e9G6A5HwIr9nHeGF1zGwDSKoiuPDPGttOAY1vM,22476
43
+ raijin_server-0.2.28.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
44
+ raijin_server-0.2.28.dist-info/entry_points.txt,sha256=3ZvxDX4pvcjkIRsXAJ69wIfVmKa78LKo-C3QhqN2KVM,56
45
+ raijin_server-0.2.28.dist-info/top_level.txt,sha256=Yz1xneCRtsZOzbPIcTAcrSxd-1p80pohMXYAZ74dpok,14
46
+ raijin_server-0.2.28.dist-info/RECORD,,