raijin-server 0.1.0__py3-none-any.whl → 0.2.1__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.
Files changed (32) hide show
  1. raijin_server/__init__.py +1 -1
  2. raijin_server/cli.py +76 -5
  3. raijin_server/healthchecks.py +76 -0
  4. raijin_server/modules/__init__.py +11 -2
  5. raijin_server/modules/apokolips_demo.py +378 -0
  6. raijin_server/modules/bootstrap.py +65 -0
  7. raijin_server/modules/calico.py +93 -22
  8. raijin_server/modules/cert_manager.py +127 -0
  9. raijin_server/modules/full_install.py +54 -19
  10. raijin_server/modules/network.py +96 -2
  11. raijin_server/modules/observability_dashboards.py +233 -0
  12. raijin_server/modules/observability_ingress.py +218 -0
  13. raijin_server/modules/sanitize.py +142 -0
  14. raijin_server/modules/secrets.py +109 -0
  15. raijin_server/modules/ssh_hardening.py +128 -0
  16. raijin_server/modules/traefik.py +1 -1
  17. raijin_server/modules/vpn.py +68 -3
  18. raijin_server/scripts/__init__.py +1 -0
  19. raijin_server/scripts/checklist.sh +60 -0
  20. raijin_server/scripts/install.sh +134 -0
  21. raijin_server/scripts/log_size_metric.sh +31 -0
  22. raijin_server/scripts/pre-deploy-check.sh +183 -0
  23. raijin_server/utils.py +45 -12
  24. raijin_server/validators.py +43 -4
  25. raijin_server-0.2.1.dist-info/METADATA +407 -0
  26. raijin_server-0.2.1.dist-info/RECORD +44 -0
  27. raijin_server-0.1.0.dist-info/METADATA +0 -219
  28. raijin_server-0.1.0.dist-info/RECORD +0 -32
  29. {raijin_server-0.1.0.dist-info → raijin_server-0.2.1.dist-info}/WHEEL +0 -0
  30. {raijin_server-0.1.0.dist-info → raijin_server-0.2.1.dist-info}/entry_points.txt +0 -0
  31. {raijin_server-0.1.0.dist-info → raijin_server-0.2.1.dist-info}/licenses/LICENSE +0 -0
  32. {raijin_server-0.1.0.dist-info → raijin_server-0.2.1.dist-info}/top_level.txt +0 -0
raijin_server/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """Pacote principal do CLI Raijin Server."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.1"
4
4
 
5
5
  __all__ = ["__version__"]
raijin_server/cli.py CHANGED
@@ -10,14 +10,18 @@ import typer
10
10
  from rich import box
11
11
  from rich.console import Console
12
12
  from rich.panel import Panel
13
- from rich.prompt import Confirm, Prompt
13
+ from rich.prompt import Prompt
14
14
  from rich.table import Table
15
15
 
16
16
  from raijin_server import __version__
17
17
  from raijin_server.modules import (
18
+ apokolips_demo,
19
+ bootstrap,
18
20
  calico,
21
+ cert_manager,
19
22
  essentials,
20
23
  firewall,
24
+ full_install,
21
25
  grafana,
22
26
  harness,
23
27
  hardening,
@@ -28,11 +32,15 @@ from raijin_server.modules import (
28
32
  loki,
29
33
  minio,
30
34
  network,
35
+ observability_dashboards,
36
+ observability_ingress,
31
37
  prometheus,
38
+ secrets,
39
+ sanitize,
40
+ ssh_hardening,
32
41
  traefik,
33
42
  velero,
34
- bootstrap,
35
- full_install,
43
+ vpn,
36
44
  )
37
45
  from raijin_server.utils import ExecutionContext, logger
38
46
  from raijin_server.validators import validate_system_requirements, check_module_dependencies
@@ -65,19 +73,27 @@ BANNER = r"""
65
73
  """
66
74
 
67
75
  MODULES: Dict[str, Callable[[ExecutionContext], None]] = {
76
+ "sanitize": sanitize.run,
68
77
  "bootstrap": bootstrap.run,
78
+ "ssh_hardening": ssh_hardening.run,
69
79
  "hardening": hardening.run,
70
80
  "network": network.run,
71
81
  "essentials": essentials.run,
72
82
  "firewall": firewall.run,
83
+ "vpn": vpn.run,
73
84
  "kubernetes": kubernetes.run,
74
85
  "calico": calico.run,
86
+ "cert_manager": cert_manager.run,
75
87
  "istio": istio.run,
76
88
  "traefik": traefik.run,
77
89
  "kong": kong.run,
78
90
  "minio": minio.run,
79
91
  "prometheus": prometheus.run,
80
92
  "grafana": grafana.run,
93
+ "observability_ingress": observability_ingress.run,
94
+ "observability_dashboards": observability_dashboards.run,
95
+ "apokolips_demo": apokolips_demo.run,
96
+ "secrets": secrets.run,
81
97
  "loki": loki.run,
82
98
  "harness": harness.run,
83
99
  "velero": velero.run,
@@ -86,19 +102,27 @@ MODULES: Dict[str, Callable[[ExecutionContext], None]] = {
86
102
  }
87
103
 
88
104
  MODULE_DESCRIPTIONS: Dict[str, str] = {
105
+ "sanitize": "Remove instalacoes antigas de Kubernetes e prepara ambiente",
89
106
  "bootstrap": "Instala ferramentas: helm, kubectl, istioctl, velero, containerd",
107
+ "ssh_hardening": "Configura usuario dedicado, chaves e politicas de SSH",
90
108
  "hardening": "Ajustes de kernel, auditd, fail2ban",
91
109
  "network": "Netplan, hostname, DNS",
92
110
  "essentials": "Pacotes basicos, repos, utilitarios",
93
111
  "firewall": "Regras UFW padrao e serviços basicos",
112
+ "vpn": "Provisiona WireGuard com cliente inicial",
94
113
  "kubernetes": "Instala kubeadm/kubelet/kubectl e inicializa cluster",
95
114
  "calico": "CNI Calico e politica default deny",
115
+ "cert_manager": "Instala cert-manager e ClusterIssuer ACME",
96
116
  "istio": "Service mesh Istio via Helm",
97
117
  "traefik": "Ingress controller Traefik com TLS",
98
118
  "kong": "Ingress/Gateway Kong via Helm",
99
119
  "minio": "Objeto storage S3-compat via Helm",
100
120
  "prometheus": "Stack kube-prometheus",
101
121
  "grafana": "Dashboards e datasource Prometheus",
122
+ "observability_ingress": "Ingress seguro com auth/TLS para Grafana/Prometheus/Alertmanager",
123
+ "observability_dashboards": "Dashboards Grafana + alertas default Prometheus/Alertmanager",
124
+ "apokolips_demo": "Landing page Apokolips para testar ingress externo",
125
+ "secrets": "Instala sealed-secrets e external-secrets via Helm",
102
126
  "loki": "Logs centralizados Loki",
103
127
  "harness": "Delegate Harness via Helm",
104
128
  "velero": "Backup/restore de clusters",
@@ -193,7 +217,7 @@ def _select_state_dir() -> Path:
193
217
 
194
218
  fallback = Path("/tmp/raijin-state")
195
219
  fallback.mkdir(parents=True, exist_ok=True)
196
- console.print(f"[yellow]Usando fallback /tmp/raijin-state para marcar conclusao[/yellow]")
220
+ console.print("[yellow]Usando fallback /tmp/raijin-state para marcar conclusao[/yellow]")
197
221
  _STATE_DIR_CACHE = fallback
198
222
  return fallback
199
223
 
@@ -241,6 +265,14 @@ def _render_menu(dry_run: bool) -> int:
241
265
  return exit_idx
242
266
 
243
267
 
268
+ def _version_callback(value: bool) -> None:
269
+ """Imprime a versao e encerra imediatamente."""
270
+
271
+ if value:
272
+ typer.echo(f"raijin-server {__version__}")
273
+ raise typer.Exit()
274
+
275
+
244
276
  def interactive_menu(ctx: typer.Context) -> None:
245
277
  exec_ctx = ctx.obj or ExecutionContext()
246
278
  current_dry_run = exec_ctx.dry_run
@@ -291,6 +323,15 @@ def main(
291
323
  module: Optional[str] = typer.Option(None, "-m", "--module", help="Modulo a executar"),
292
324
  dry_run: bool = typer.Option(False, "-n", "--dry-run", help="Mostra comandos sem executa-los."),
293
325
  skip_validation: bool = typer.Option(False, "--skip-validation", help="Pula validacoes de pre-requisitos"),
326
+ skip_root: bool = typer.Option(False, "--skip-root", help="Permite validar sem exigir root (nao recomendado)"),
327
+ version: Optional[bool] = typer.Option(
328
+ None,
329
+ "--version",
330
+ "-V",
331
+ is_eager=True,
332
+ callback=_version_callback,
333
+ help="Mostra a versao do CLI e sai",
334
+ ),
294
335
  ) -> None:
295
336
  """Mostra um menu simples quando nenhum subcomando e informado."""
296
337
 
@@ -298,7 +339,7 @@ def main(
298
339
 
299
340
  # Executa validacoes de pre-requisitos
300
341
  if not skip_validation and not dry_run:
301
- if not validate_system_requirements(ctx.obj, skip_root=False):
342
+ if not validate_system_requirements(ctx.obj, skip_root=skip_root):
302
343
  typer.secho("\nAbortando devido a pre-requisitos nao atendidos.", fg=typer.colors.RED)
303
344
  typer.echo("Use --skip-validation para pular validacoes (nao recomendado).")
304
345
  raise typer.Exit(code=1)
@@ -324,6 +365,11 @@ def hardening(ctx: typer.Context) -> None:
324
365
  _run_module(ctx, "hardening")
325
366
 
326
367
 
368
+ @app.command(name="ssh-hardening")
369
+ def ssh_hardening_cmd(ctx: typer.Context) -> None:
370
+ _run_module(ctx, "ssh_hardening")
371
+
372
+
327
373
  @app.command()
328
374
  def network(ctx: typer.Context) -> None:
329
375
  _run_module(ctx, "network")
@@ -379,6 +425,21 @@ def grafana(ctx: typer.Context) -> None:
379
425
  _run_module(ctx, "grafana")
380
426
 
381
427
 
428
+ @app.command(name="apokolips-demo")
429
+ def apokolips_demo_cmd(ctx: typer.Context) -> None:
430
+ _run_module(ctx, "apokolips_demo")
431
+
432
+
433
+ @app.command(name="observability-ingress")
434
+ def observability_ingress_cmd(ctx: typer.Context) -> None:
435
+ _run_module(ctx, "observability_ingress")
436
+
437
+
438
+ @app.command(name="observability-dashboards")
439
+ def observability_dashboards_cmd(ctx: typer.Context) -> None:
440
+ _run_module(ctx, "observability_dashboards")
441
+
442
+
382
443
  @app.command()
383
444
  def loki(ctx: typer.Context) -> None:
384
445
  _run_module(ctx, "loki")
@@ -399,6 +460,16 @@ def kafka(ctx: typer.Context) -> None:
399
460
  _run_module(ctx, "kafka")
400
461
 
401
462
 
463
+ @app.command()
464
+ def vpn(ctx: typer.Context) -> None:
465
+ _run_module(ctx, "vpn")
466
+
467
+
468
+ @app.command()
469
+ def sanitize(ctx: typer.Context) -> None:
470
+ _run_module(ctx, "sanitize")
471
+
472
+
402
473
  @app.command(name="bootstrap")
403
474
  def bootstrap_cmd(ctx: typer.Context) -> None:
404
475
  """Instala todas as ferramentas necessarias: helm, kubectl, istioctl, velero, containerd."""
@@ -10,6 +10,10 @@ import typer
10
10
 
11
11
  from raijin_server.utils import ExecutionContext, logger
12
12
 
13
+ SEALED_NS = "kube-system"
14
+ ESO_NS = "external-secrets"
15
+ CERT_NS = "cert-manager"
16
+
13
17
 
14
18
  def wait_for_condition(
15
19
  check_fn: Callable[[], bool],
@@ -259,6 +263,75 @@ def verify_helm_chart(release: str, namespace: str, ctx: ExecutionContext) -> bo
259
263
  return check_k8s_pods_in_namespace(namespace, ctx, timeout=180)
260
264
 
261
265
 
266
+ def verify_cert_manager(ctx: ExecutionContext) -> bool:
267
+ """Health check para cert-manager."""
268
+ return verify_helm_chart("cert-manager", CERT_NS, ctx)
269
+
270
+
271
+ def verify_secrets(ctx: ExecutionContext) -> bool:
272
+ """Health check para sealed-secrets e external-secrets."""
273
+ typer.secho("\n=== Health Check: Secrets ===", fg=typer.colors.CYAN)
274
+
275
+ sealed_ok = verify_helm_chart("sealed-secrets", SEALED_NS, ctx)
276
+ eso_ok = verify_helm_chart("external-secrets", ESO_NS, ctx)
277
+
278
+ return sealed_ok and eso_ok
279
+
280
+
281
+ def verify_apokolips_demo(ctx: ExecutionContext) -> bool:
282
+ """Health check especifico para a landing page Apokolips."""
283
+ namespace = "apokolips-demo"
284
+ logger.info("Verificando health check: apokolips-demo")
285
+ typer.secho("\n=== Health Check: Apokolips Demo ===", fg=typer.colors.CYAN)
286
+
287
+ pods_ok = check_k8s_pods_in_namespace(namespace, ctx, timeout=120)
288
+ if not pods_ok:
289
+ return False
290
+ if ctx.dry_run:
291
+ return True
292
+
293
+ try:
294
+ import json
295
+
296
+ result = subprocess.run(
297
+ [
298
+ "kubectl",
299
+ "get",
300
+ "ingress",
301
+ "apokolips-demo",
302
+ "-n",
303
+ namespace,
304
+ "-o",
305
+ "json",
306
+ ],
307
+ capture_output=True,
308
+ text=True,
309
+ timeout=10,
310
+ )
311
+ if result.returncode != 0:
312
+ typer.secho(" ✗ Nao foi possivel consultar o ingress", fg=typer.colors.YELLOW)
313
+ logger.warning("kubectl get ingress retornou codigo != 0 para apokolips-demo")
314
+ return False
315
+
316
+ data = json.loads(result.stdout)
317
+ ingress_data = data.get("status", {}).get("loadBalancer", {}).get("ingress", [])
318
+ address = ""
319
+ if ingress_data:
320
+ entry = ingress_data[0]
321
+ address = entry.get("ip") or entry.get("hostname", "")
322
+
323
+ if address:
324
+ typer.secho(f" ✓ LoadBalancer publicado ({address})", fg=typer.colors.GREEN)
325
+ return True
326
+
327
+ typer.secho(" ✗ LoadBalancer ainda sem IP/hostname", fg=typer.colors.YELLOW)
328
+ return False
329
+ except Exception as exc:
330
+ typer.secho(f" ✗ Erro ao verificar ingress: {exc}", fg=typer.colors.YELLOW)
331
+ logger.error(f"Erro verificando ingress apokolips-demo: {exc}")
332
+ return False
333
+
334
+
262
335
  # Mapeamento de modulos para funcoes de health check
263
336
  HEALTH_CHECKS = {
264
337
  "essentials": verify_essentials,
@@ -273,6 +346,9 @@ HEALTH_CHECKS = {
273
346
  "minio": lambda ctx: verify_helm_chart("minio", "minio", ctx),
274
347
  "velero": lambda ctx: verify_helm_chart("velero", "velero", ctx),
275
348
  "kafka": lambda ctx: verify_helm_chart("kafka", "kafka", ctx),
349
+ "cert_manager": verify_cert_manager,
350
+ "secrets": verify_secrets,
351
+ "apokolips_demo": verify_apokolips_demo,
276
352
  }
277
353
 
278
354
 
@@ -1,6 +1,7 @@
1
1
  """Colecao de modulos suportados pelo CLI."""
2
2
 
3
3
  __all__ = [
4
+ "sanitize",
4
5
  "hardening",
5
6
  "network",
6
7
  "essentials",
@@ -18,9 +19,17 @@ __all__ = [
18
19
  "velero",
19
20
  "kafka",
20
21
  "bootstrap",
22
+ "ssh_hardening",
23
+ "vpn",
24
+ "observability_ingress",
25
+ "observability_dashboards",
26
+ "apokolips_demo",
27
+ "cert_manager",
28
+ "secrets",
21
29
  "full_install",
22
30
  ]
23
31
 
24
32
  from raijin_server.modules import calico, essentials, firewall, grafana, harness, hardening, istio
25
- from raijin_server.modules import kafka, kong, kubernetes, loki, minio, network, prometheus, traefik
26
- from raijin_server.modules import velero, bootstrap, full_install
33
+ from raijin_server.modules import kafka, kong, kubernetes, loki, minio, network, observability_dashboards
34
+ from raijin_server.modules import observability_ingress, prometheus, traefik, velero, apokolips_demo, secrets, cert_manager
35
+ from raijin_server.modules import bootstrap, full_install, sanitize, ssh_hardening, vpn
@@ -0,0 +1,378 @@
1
+ """Provisiona uma landing page tema Apokolips para validar ingress."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from textwrap import dedent, indent
8
+
9
+ import typer
10
+
11
+ from raijin_server.utils import ExecutionContext, ensure_tool, run_cmd, write_file
12
+
13
+ NAMESPACE = "apokolips-demo"
14
+ TMP_MANIFEST = Path("/tmp/raijin-apokolips.yaml")
15
+ DEFAULT_HOST = "apokolips.raijin.local"
16
+ HTML_TEMPLATE = dedent(
17
+ """
18
+ <!DOCTYPE html>
19
+ <html lang="pt-BR">
20
+ <head>
21
+ <meta charset="UTF-8" />
22
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
23
+ <title>Apokolips Signal Check</title>
24
+ <link rel="preconnect" href="https://fonts.googleapis.com">
25
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
26
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600&family=Unica+One&display=swap" rel="stylesheet">
27
+ <style>
28
+ :root {
29
+ --lava: #ff4d00;
30
+ --ember: #ff9500;
31
+ --ash: #2c2c34;
32
+ --void: #050007;
33
+ --smoke: #a0a3b1;
34
+ }
35
+ * {
36
+ box-sizing: border-box;
37
+ }
38
+ body {
39
+ margin: 0;
40
+ padding: 0;
41
+ min-height: 100vh;
42
+ font-family: 'Space Grotesk', sans-serif;
43
+ color: #f7f7ff;
44
+ background: radial-gradient(circle at top, rgba(255,77,0,0.45), rgba(5,0,7,0.9)), #050007;
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ overflow: hidden;
49
+ }
50
+ .atmosphere {
51
+ position: absolute;
52
+ inset: 0;
53
+ background: url('https://www.transparenttextures.com/patterns/asfalt-dark.png');
54
+ opacity: 0.35;
55
+ mix-blend-mode: screen;
56
+ pointer-events: none;
57
+ }
58
+ .container {
59
+ width: min(960px, 90vw);
60
+ background: linear-gradient(135deg, rgba(20,22,35,0.9), rgba(10,12,22,0.95));
61
+ border: 1px solid rgba(255,255,255,0.08);
62
+ border-radius: 28px;
63
+ padding: 48px;
64
+ position: relative;
65
+ overflow: hidden;
66
+ box-shadow: 0 40px 120px rgba(0,0,0,0.55);
67
+ animation: rise 1.2s ease forwards;
68
+ }
69
+ .container::before, .container::after {
70
+ content: '';
71
+ position: absolute;
72
+ width: 320px;
73
+ height: 320px;
74
+ border-radius: 50%;
75
+ background: radial-gradient(circle, rgba(255,77,0,0.45), transparent 60%);
76
+ filter: blur(20px);
77
+ z-index: 0;
78
+ }
79
+ .container::before {
80
+ top: -120px;
81
+ right: -60px;
82
+ }
83
+ .container::after {
84
+ bottom: -140px;
85
+ left: -80px;
86
+ background: radial-gradient(circle, rgba(255,149,0,0.4), transparent 60%);
87
+ }
88
+ @keyframes rise {
89
+ from { transform: translateY(40px); opacity: 0; }
90
+ to { transform: translateY(0); opacity: 1; }
91
+ }
92
+ h1 {
93
+ font-family: 'Unica One', sans-serif;
94
+ font-size: clamp(3rem, 5vw, 4.5rem);
95
+ letter-spacing: 0.08em;
96
+ text-transform: uppercase;
97
+ color: var(--lava);
98
+ margin: 0 0 12px;
99
+ z-index: 1;
100
+ }
101
+ .subhead {
102
+ font-size: 1.15rem;
103
+ color: var(--smoke);
104
+ letter-spacing: 0.05em;
105
+ margin-bottom: 32px;
106
+ text-transform: uppercase;
107
+ }
108
+ .panels {
109
+ display: grid;
110
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
111
+ gap: 24px;
112
+ z-index: 1;
113
+ }
114
+ .panel {
115
+ background: rgba(12,14,24,0.85);
116
+ border-radius: 18px;
117
+ padding: 20px;
118
+ border: 1px solid rgba(255,255,255,0.05);
119
+ }
120
+ .panel h2 {
121
+ font-size: 0.95rem;
122
+ text-transform: uppercase;
123
+ letter-spacing: 0.08em;
124
+ color: var(--ember);
125
+ margin: 0 0 12px;
126
+ }
127
+ .status {
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: 6px;
131
+ font-size: 1rem;
132
+ color: var(--smoke);
133
+ }
134
+ .status span::before {
135
+ content: '●';
136
+ margin-right: 8px;
137
+ color: var(--lava);
138
+ }
139
+ .cta {
140
+ margin-top: 36px;
141
+ display: flex;
142
+ flex-wrap: wrap;
143
+ gap: 18px;
144
+ z-index: 1;
145
+ }
146
+ a.button {
147
+ background: linear-gradient(135deg, var(--lava), var(--ember));
148
+ color: #050007;
149
+ text-decoration: none;
150
+ padding: 14px 26px;
151
+ border-radius: 999px;
152
+ font-weight: 600;
153
+ letter-spacing: 0.08em;
154
+ text-transform: uppercase;
155
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
156
+ }
157
+ a.button:hover {
158
+ transform: translateY(-2px);
159
+ box-shadow: 0 20px 35px rgba(255,77,0,0.25);
160
+ }
161
+ pre {
162
+ background: rgba(5,0,7,0.75);
163
+ border-radius: 16px;
164
+ padding: 16px;
165
+ color: var(--smoke);
166
+ font-size: 0.95rem;
167
+ line-height: 1.5;
168
+ overflow-x: auto;
169
+ }
170
+ footer {
171
+ margin-top: 28px;
172
+ font-size: 0.85rem;
173
+ letter-spacing: 0.04em;
174
+ color: rgba(247,247,255,0.7);
175
+ }
176
+ @media (max-width: 640px) {
177
+ .container {
178
+ padding: 32px 24px;
179
+ }
180
+ .cta {
181
+ flex-direction: column;
182
+ }
183
+ }
184
+ </style>
185
+ </head>
186
+ <body>
187
+ <div class="atmosphere"></div>
188
+ <main class="container">
189
+ <h1>Apokolips Online</h1>
190
+ <p class="subhead">Canal de prova para ingress / load balancer</p>
191
+ <section class="panels">
192
+ <article class="panel">
193
+ <h2>Estado</h2>
194
+ <div class="status">
195
+ <span>Pods sincronizados</span>
196
+ <span>ConfigMap montado</span>
197
+ <span>Ingress publicado</span>
198
+ </div>
199
+ </article>
200
+ <article class="panel">
201
+ <h2>Checklist</h2>
202
+ <div class="status">
203
+ <span>DNS aponta para balanceador</span>
204
+ <span>TLS (opcional) emitido</span>
205
+ <span>Firewall libera HTTP/S</span>
206
+ </div>
207
+ </article>
208
+ <article class="panel">
209
+ <h2>Debug Rápido</h2>
210
+ <pre>kubectl -n apokolips-demo get all
211
+ kubectl -n apokolips-demo describe ingress apokolips-demo
212
+ curl -H "Host: SEU_HOST" https://LB_IP/</pre>
213
+ </article>
214
+ </section>
215
+ <div class="cta">
216
+ <a class="button" href="https://github.com/darkseid/raijin-server" target="_blank" rel="noreferrer noopener">Docs Raijin</a>
217
+ <a class="button" href="https://status.cloudflare.com/" target="_blank" rel="noreferrer noopener">Status Externo</a>
218
+ </div>
219
+ <footer>
220
+ Se esta página carregou via ingress, seu cluster respondeu à chamada de Apokolips.
221
+ </footer>
222
+ </main>
223
+ </body>
224
+ </html>
225
+ """
226
+ )
227
+
228
+
229
+ def _resolve_host() -> str:
230
+ env_host = os.environ.get("APOKOLIPS_HOST")
231
+ if env_host:
232
+ return env_host.strip()
233
+ return typer.prompt("Host (FQDN) para o ingress", default=DEFAULT_HOST).strip()
234
+
235
+
236
+ def _resolve_tls_secret() -> str | None:
237
+ env_secret = os.environ.get("APOKOLIPS_TLS_SECRET")
238
+ if env_secret:
239
+ return env_secret.strip()
240
+ use_tls = typer.confirm("Deseja referenciar um Secret TLS existente?", default=False)
241
+ if not use_tls:
242
+ return None
243
+ secret = typer.prompt("Nome do Secret TLS", default="apokolips-demo-tls")
244
+ return secret.strip() or None
245
+
246
+
247
+ def _build_manifest(host: str, tls_secret: str | None) -> str:
248
+ html_block = indent(HTML_TEMPLATE.strip("\n"), " " * 4)
249
+ tls_block = ""
250
+ if tls_secret:
251
+ tls_block = (
252
+ " tls:\n"
253
+ " - hosts:\n"
254
+ f" - {host}\n"
255
+ f" secretName: {tls_secret}\n"
256
+ )
257
+
258
+ template = """\
259
+ apiVersion: v1
260
+ kind: Namespace
261
+ metadata:
262
+ name: {namespace}
263
+ ---
264
+ apiVersion: v1
265
+ kind: ConfigMap
266
+ metadata:
267
+ name: apokolips-html
268
+ namespace: {namespace}
269
+ data:
270
+ index.html: |
271
+ __HTML__
272
+ ---
273
+ apiVersion: apps/v1
274
+ kind: Deployment
275
+ metadata:
276
+ name: apokolips-demo
277
+ namespace: {namespace}
278
+ labels:
279
+ app: apokolips-demo
280
+ spec:
281
+ replicas: 1
282
+ selector:
283
+ matchLabels:
284
+ app: apokolips-demo
285
+ template:
286
+ metadata:
287
+ labels:
288
+ app: apokolips-demo
289
+ spec:
290
+ containers:
291
+ - name: apokolips-web
292
+ image: nginx:1.25
293
+ ports:
294
+ - containerPort: 80
295
+ resources:
296
+ limits:
297
+ cpu: 100m
298
+ memory: 128Mi
299
+ requests:
300
+ cpu: 50m
301
+ memory: 64Mi
302
+ volumeMounts:
303
+ - name: site
304
+ mountPath: /usr/share/nginx/html
305
+ readOnly: true
306
+ volumes:
307
+ - name: site
308
+ configMap:
309
+ name: apokolips-html
310
+ ---
311
+ apiVersion: v1
312
+ kind: Service
313
+ metadata:
314
+ name: apokolips-demo
315
+ namespace: {namespace}
316
+ spec:
317
+ type: ClusterIP
318
+ selector:
319
+ app: apokolips-demo
320
+ ports:
321
+ - port: 80
322
+ targetPort: 80
323
+ ---
324
+ apiVersion: networking.k8s.io/v1
325
+ kind: Ingress
326
+ metadata:
327
+ name: apokolips-demo
328
+ namespace: {namespace}
329
+ annotations:
330
+ traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
331
+ spec:
332
+ ingressClassName: traefik
333
+ rules:
334
+ - host: {host}
335
+ http:
336
+ paths:
337
+ - path: /
338
+ pathType: Prefix
339
+ backend:
340
+ service:
341
+ name: apokolips-demo
342
+ port:
343
+ number: 80
344
+ __TLS__
345
+ """
346
+
347
+ manifest = template.format(namespace=NAMESPACE, host=host)
348
+ manifest = manifest.replace("__HTML__", html_block)
349
+ manifest = manifest.replace("__TLS__", tls_block.rstrip())
350
+ return f"{manifest.strip()}\n"
351
+
352
+
353
+ def run(ctx: ExecutionContext) -> None:
354
+ ensure_tool("kubectl", ctx, install_hint="Instale kubectl para aplicar o manifesto do site.")
355
+ host = _resolve_host()
356
+ tls_secret = _resolve_tls_secret()
357
+ manifest = _build_manifest(host, tls_secret)
358
+
359
+ typer.echo("Gerando manifesto Apokolips...")
360
+ write_file(TMP_MANIFEST, manifest, ctx)
361
+
362
+ try:
363
+ run_cmd(["kubectl", "apply", "-f", str(TMP_MANIFEST)], ctx)
364
+ finally:
365
+ TMP_MANIFEST.unlink(missing_ok=True)
366
+
367
+ typer.secho("\nLanding page implantada!", fg=typer.colors.GREEN, bold=True)
368
+ typer.echo(f" • Namespace: {NAMESPACE}")
369
+ typer.echo(f" • Host: {host}")
370
+ if tls_secret:
371
+ typer.echo(f" • Secret TLS: {tls_secret}")
372
+ typer.echo("\nTestes sugeridos:")
373
+ typer.echo(f" curl -H 'Host: {host}' https://<IP_DO_LOAD_BALANCER>/ --insecure")
374
+ typer.echo(f" kubectl -n {NAMESPACE} get ingress {NAMESPACE}")
375
+ typer.echo(f" kubectl -n {NAMESPACE} get pods")
376
+
377
+ typer.echo("\nPara remover:")
378
+ typer.echo(f" kubectl delete namespace {NAMESPACE}")