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.
- raijin_server/__init__.py +1 -1
- raijin_server/cli.py +76 -5
- raijin_server/healthchecks.py +76 -0
- raijin_server/modules/__init__.py +11 -2
- raijin_server/modules/apokolips_demo.py +378 -0
- raijin_server/modules/bootstrap.py +65 -0
- raijin_server/modules/calico.py +93 -22
- raijin_server/modules/cert_manager.py +127 -0
- raijin_server/modules/full_install.py +54 -19
- raijin_server/modules/network.py +96 -2
- raijin_server/modules/observability_dashboards.py +233 -0
- raijin_server/modules/observability_ingress.py +218 -0
- raijin_server/modules/sanitize.py +142 -0
- raijin_server/modules/secrets.py +109 -0
- raijin_server/modules/ssh_hardening.py +128 -0
- raijin_server/modules/traefik.py +1 -1
- raijin_server/modules/vpn.py +68 -3
- raijin_server/scripts/__init__.py +1 -0
- raijin_server/scripts/checklist.sh +60 -0
- raijin_server/scripts/install.sh +134 -0
- raijin_server/scripts/log_size_metric.sh +31 -0
- raijin_server/scripts/pre-deploy-check.sh +183 -0
- raijin_server/utils.py +45 -12
- raijin_server/validators.py +43 -4
- raijin_server-0.2.1.dist-info/METADATA +407 -0
- raijin_server-0.2.1.dist-info/RECORD +44 -0
- raijin_server-0.1.0.dist-info/METADATA +0 -219
- raijin_server-0.1.0.dist-info/RECORD +0 -32
- {raijin_server-0.1.0.dist-info → raijin_server-0.2.1.dist-info}/WHEEL +0 -0
- {raijin_server-0.1.0.dist-info → raijin_server-0.2.1.dist-info}/entry_points.txt +0 -0
- {raijin_server-0.1.0.dist-info → raijin_server-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {raijin_server-0.1.0.dist-info → raijin_server-0.2.1.dist-info}/top_level.txt +0 -0
raijin_server/__init__.py
CHANGED
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
|
|
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
|
-
|
|
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(
|
|
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=
|
|
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."""
|
raijin_server/healthchecks.py
CHANGED
|
@@ -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,
|
|
26
|
-
from raijin_server.modules import velero,
|
|
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}")
|