raijin-server 0.3.6__py3-none-any.whl → 0.3.7__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.

Potentially problematic release.


This version of raijin-server might be problematic. Click here for more details.

@@ -25,6 +25,7 @@ from raijin_server.utils import (
25
25
  run_cmd,
26
26
  write_file,
27
27
  )
28
+ from raijin_server.minio_utils import get_or_create_minio_user
28
29
 
29
30
  HARBOR_NAMESPACE = "harbor"
30
31
 
@@ -65,34 +66,17 @@ def _uninstall_harbor(ctx: ExecutionContext, namespace: str) -> None:
65
66
 
66
67
 
67
68
  def _get_minio_credentials(ctx: ExecutionContext) -> tuple[str, str]:
68
- """Obtem credenciais do MinIO do Secret do K8s."""
69
- typer.echo("Obtendo credenciais do MinIO...")
69
+ """Obtem ou cria credenciais específicas do MinIO para Harbor.
70
70
 
71
- result = run_cmd(
72
- ["kubectl", "-n", "minio", "get", "secret", "minio-credentials", "-o", "jsonpath={.data.accesskey}"],
73
- ctx,
74
- check=False,
71
+ Esta função cria um usuário MinIO dedicado para o Harbor com acesso
72
+ restrito apenas aos buckets: harbor-registry, harbor-chartmuseum, harbor-jobservice.
73
+ """
74
+ return get_or_create_minio_user(
75
+ ctx=ctx,
76
+ app_name="harbor",
77
+ buckets=["harbor-registry", "harbor-chartmuseum", "harbor-jobservice"],
78
+ namespace=HARBOR_NAMESPACE,
75
79
  )
76
-
77
- if result.returncode == 0 and result.stdout:
78
- access_key = base64.b64decode(result.stdout.strip()).decode("utf-8")
79
-
80
- result = run_cmd(
81
- ["kubectl", "-n", "minio", "get", "secret", "minio-credentials", "-o", "jsonpath={.data.secretkey}"],
82
- ctx,
83
- check=False,
84
- )
85
-
86
- if result.returncode == 0 and result.stdout:
87
- secret_key = base64.b64decode(result.stdout.strip()).decode("utf-8")
88
- return access_key, secret_key
89
-
90
- # Fallback para prompt manual
91
- typer.secho("Não foi possível obter credenciais automaticamente.", fg=typer.colors.YELLOW)
92
- access_key = typer.prompt("MinIO Access Key", default="thor")
93
- secret_key = typer.prompt("MinIO Secret Key", default="rebel1on", hide_input=True)
94
-
95
- return access_key, secret_key
96
80
 
97
81
 
98
82
  def _wait_for_pods_ready(ctx: ExecutionContext, namespace: str, timeout: int = 300) -> bool:
@@ -24,6 +24,7 @@ from raijin_server.utils import (
24
24
  run_cmd,
25
25
  write_file,
26
26
  )
27
+ from raijin_server.minio_utils import get_or_create_minio_user
27
28
 
28
29
  VAULT_NAMESPACE = "vault"
29
30
  ESO_NAMESPACE = "external-secrets"
@@ -124,35 +125,17 @@ def _wait_for_pods_ready(ctx: ExecutionContext, namespace: str, label: str, time
124
125
 
125
126
 
126
127
  def _get_minio_credentials(ctx: ExecutionContext) -> tuple[str, str]:
127
- """Obtem credenciais do MinIO do Secret do K8s."""
128
- typer.echo("Obtendo credenciais do MinIO...")
129
-
130
- # Tenta obter do secret minio-credentials no namespace minio
131
- result = run_cmd(
132
- ["kubectl", "-n", "minio", "get", "secret", "minio-credentials", "-o", "jsonpath={.data.accesskey}"],
133
- ctx,
134
- check=False,
128
+ """Obtem ou cria credenciais específicas do MinIO para Vault.
129
+
130
+ Esta função cria um usuário MinIO dedicado para o Vault com acesso
131
+ restrito apenas ao bucket 'vault-storage'.
132
+ """
133
+ return get_or_create_minio_user(
134
+ ctx=ctx,
135
+ app_name="vault",
136
+ buckets=["vault-storage"],
137
+ namespace=VAULT_NAMESPACE,
135
138
  )
136
-
137
- if result.returncode == 0 and result.stdout:
138
- access_key = base64.b64decode(result.stdout.strip()).decode("utf-8")
139
-
140
- result = run_cmd(
141
- ["kubectl", "-n", "minio", "get", "secret", "minio-credentials", "-o", "jsonpath={.data.secretkey}"],
142
- ctx,
143
- check=False,
144
- )
145
-
146
- if result.returncode == 0 and result.stdout:
147
- secret_key = base64.b64decode(result.stdout.strip()).decode("utf-8")
148
- return access_key, secret_key
149
-
150
- # Fallback para prompt manual
151
- typer.secho("Não foi possível obter credenciais automaticamente.", fg=typer.colors.YELLOW)
152
- access_key = typer.prompt("MinIO Access Key", default="thor")
153
- secret_key = typer.prompt("MinIO Secret Key", default="rebel1on", hide_input=True)
154
-
155
- return access_key, secret_key
156
139
 
157
140
 
158
141
  def _initialize_vault(ctx: ExecutionContext, vault_ns: str, node_ip: str) -> tuple[str, list[str]]:
@@ -410,16 +393,8 @@ def run(ctx: ExecutionContext) -> None:
410
393
  if cleanup:
411
394
  _uninstall_vault(ctx, vault_ns)
412
395
 
413
- # Cria bucket no MinIO para Vault storage
414
- typer.echo("\nCriando bucket 'vault-storage' no MinIO...")
415
- run_cmd(
416
- [
417
- "mc", "mb", "--ignore-existing",
418
- f"minio/vault-storage"
419
- ],
420
- ctx,
421
- check=False,
422
- )
396
+ # Credenciais são obtidas automaticamente via get_or_create_minio_user
397
+ # que já cria o bucket 'vault-storage' e o usuário 'vault-user'
423
398
 
424
399
  vault_values_yaml = f"""server:
425
400
  ha:
@@ -1,10 +1,46 @@
1
1
  """Backup e restore com Velero (production-ready)."""
2
2
 
3
3
  import time
4
+ from pathlib import Path
4
5
 
5
6
  import typer
6
7
 
7
8
  from raijin_server.utils import ExecutionContext, ensure_tool, require_root, run_cmd
9
+ from raijin_server.minio_utils import get_or_create_minio_user
10
+
11
+
12
+ def _create_velero_credentials_file(ctx: ExecutionContext) -> str:
13
+ """Cria arquivo de credenciais para Velero com usuário MinIO dedicado.
14
+
15
+ Returns:
16
+ Caminho para o arquivo de credenciais
17
+ """
18
+ typer.echo("\nConfigurando credenciais MinIO para Velero...")
19
+
20
+ # Cria usuário MinIO específico para Velero
21
+ access_key, secret_key = get_or_create_minio_user(
22
+ ctx=ctx,
23
+ app_name="velero",
24
+ buckets=["velero-backups"],
25
+ namespace="velero",
26
+ )
27
+
28
+ # Cria arquivo de credenciais no formato esperado pelo Velero
29
+ credentials_path = Path("/etc/velero/credentials")
30
+ credentials_path.parent.mkdir(parents=True, exist_ok=True)
31
+
32
+ credentials_content = f"""[default]
33
+ aws_access_key_id = {access_key}
34
+ aws_secret_access_key = {secret_key}
35
+ """
36
+
37
+ if not ctx.dry_run:
38
+ credentials_path.write_text(credentials_content)
39
+ # Protege o arquivo
40
+ credentials_path.chmod(0o600)
41
+ typer.secho(f" ✓ Credenciais salvas em {credentials_path}", fg=typer.colors.GREEN)
42
+
43
+ return str(credentials_path)
8
44
 
9
45
 
10
46
  def _check_existing_velero(ctx: ExecutionContext) -> bool:
@@ -91,8 +127,19 @@ def run(ctx: ExecutionContext) -> None:
91
127
  provider = typer.prompt("Provider (aws, azure, gcp)", default="aws")
92
128
  bucket = typer.prompt("Bucket para backups", default="velero-backups")
93
129
  region = typer.prompt("Region", default="us-east-1")
94
- s3_url = typer.prompt("S3 URL (para MinIO usar http://minio.minio.svc:9000)", default="https://s3.amazonaws.com")
95
- secret_file = typer.prompt("Arquivo de credenciais (secret-file)", default="/etc/velero/credentials")
130
+ s3_url = typer.prompt("S3 URL (para MinIO usar http://minio.minio.svc:9000)", default="http://minio.minio.svc:9000")
131
+
132
+ # Cria credenciais automaticamente usando usuário MinIO dedicado
133
+ use_auto_credentials = typer.confirm(
134
+ "Criar credenciais MinIO automaticamente (usuário dedicado)?",
135
+ default=True,
136
+ )
137
+
138
+ if use_auto_credentials:
139
+ secret_file = _create_velero_credentials_file(ctx)
140
+ else:
141
+ secret_file = typer.prompt("Arquivo de credenciais (secret-file)", default="/etc/velero/credentials")
142
+
96
143
  use_restic = typer.confirm("Habilitar Restic/Kopia para backups de PV?", default=True)
97
144
  schedule = typer.prompt("Schedule cron para backups (ex: '0 2 * * *')", default="0 2 * * *")
98
145
 
@@ -34,7 +34,7 @@ MODULE_DEPENDENCIES = {
34
34
  "kafka": ["kubernetes"],
35
35
  "observability_ingress": ["traefik", "prometheus", "grafana"],
36
36
  "observability_dashboards": ["prometheus", "grafana"],
37
- "apokolips_demo": ["kubernetes", "traefik"],
37
+
38
38
  }
39
39
 
40
40
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: raijin-server
3
- Version: 0.3.6
3
+ Version: 0.3.7
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,12 +1,12 @@
1
- raijin_server/__init__.py,sha256=DizNqR7krrm4UEAK6a2xeOG3wuV7xlSekBC-mCSHadw,94
2
- raijin_server/cli.py,sha256=74ZqEPhg9R9jxNFZRV37CHrwm-dBKmYdT_MxdK4FKKA,37585
1
+ raijin_server/__init__.py,sha256=kD4ksBx35-1QYm5EkRuSYWXJAwwJx_-sM9cp92bOhh4,94
2
+ raijin_server/cli.py,sha256=WvZaPJ5AVjhzzs_jLLe2QGvVEH_VphRwnUkTMEgycbI,37320
3
3
  raijin_server/config.py,sha256=QNiEVvrbW56XgvNn5-h3bkJm46Xc8mjNqPbvixXD8N0,4829
4
- raijin_server/healthchecks.py,sha256=lzXdFw6S0hOYbUKbqksh4phb04lXgXdTspP1Dsz4dx8,15401
4
+ raijin_server/healthchecks.py,sha256=UHSRyeKTsCGeL_4dxDSGZ1t8164Q7wYTi1c3ZiU0cro,13536
5
+ raijin_server/minio_utils.py,sha256=NQxIGoVf4-eM8rNwEHdd4QFnEIh2OxY3DyOiFkznsYs,18299
5
6
  raijin_server/module_manager.py,sha256=Wmhj603CN0XGUVr7_Fo8CHzKd9yIbS9x5BJLqDj78kw,10259
6
7
  raijin_server/utils.py,sha256=9RnGnPoUTYOpMVRLNa4P4lIQrJNQLkSkPUxycZRGv78,20827
7
- raijin_server/validators.py,sha256=EATYPy2pllAb6IX4gUZKnELvospWwyGV3DHrzxb_RMg,11761
8
- raijin_server/modules/__init__.py,sha256=BCTLuNtmvn8IWqGNQZQBRokqAv-KwUZP4_kyxkyHyN4,896
9
- raijin_server/modules/apokolips_demo.py,sha256=8ltsXRbVDwlDwLMIvh02NG-FeAfBWw_v6lh7IGOyNqs,13725
8
+ raijin_server/validators.py,sha256=LvxAIq7F8CSIihdfCCDlDtVd6WPBBntiBHuD8fuM2kg,11713
9
+ raijin_server/modules/__init__.py,sha256=UOv7c7UpxZq-JRZM0i55LGfI3LyucZhTL4lypeHs2Jw,858
10
10
  raijin_server/modules/bootstrap.py,sha256=oVIGNRW_JbgY8zXNHGAIP0vGbbHNHyQexthxo5zhbcw,9762
11
11
  raijin_server/modules/calico.py,sha256=TTPF1bLFdAKb3IVOqFqRxNblULkRmMMRylsIBp4w8I8,6700
12
12
  raijin_server/modules/cert_manager.py,sha256=XkFlXJjiP4_9It_PJaFcVYMS-QKTzzFAt839QQ9qNsg,50223
@@ -14,7 +14,7 @@ raijin_server/modules/essentials.py,sha256=2xUXCyCQtFGd2DnCKV81N1R6bEJqH8zaet8mL
14
14
  raijin_server/modules/firewall.py,sha256=h6AISqiZeTinVT7BjmQIS872qRAFZJLg7meqlth3cfw,757
15
15
  raijin_server/modules/full_install.py,sha256=M4SV4OA-r41xhpMmZQvUieRugiq0faLa4f0N499ksd4,15035
16
16
  raijin_server/modules/grafana.py,sha256=8YbKG-UL19lwTkH1-ZxpUetOZd-CLI4_kPsuZblNaWI,18080
17
- raijin_server/modules/harbor.py,sha256=ix0qnu_FugzbFnnTRZ3Dyy6UmKdm3nD0ubZ4SZYHkWE,21940
17
+ raijin_server/modules/harbor.py,sha256=hFD8x5K4m0unzVBiDOT_yBzDDfbuPW_2SNVrjSCXhGc,21324
18
18
  raijin_server/modules/hardening.py,sha256=4hz3ifkMhPlXa2n7gPxN0gitQgzALZ-073vuU3LM4RI,1616
19
19
  raijin_server/modules/harness.py,sha256=uWTxTVJlY_VB6xi4ftMtTSaIb96HA8WJQS-RbyxU45M,5391
20
20
  raijin_server/modules/internal_dns.py,sha256=Jynngq0TEEUo3jkAR4m8F1ihF10rkQuKHVP-gZYyDFY,15191
@@ -28,10 +28,10 @@ raijin_server/modules/minio.py,sha256=ZoxugJvvuGLzViDfEzrVCRZUevoiFwcEy0PNyn0My4
28
28
  raijin_server/modules/network.py,sha256=QRlYdcryCCPAWG3QQ_W7ld9gJgETI7H8gwntOU7UqFE,4818
29
29
  raijin_server/modules/prometheus.py,sha256=lyhaqLIfMl0GtQ2b2Hre7_A47HrHBB5gspmnWtwXZ4Y,21880
30
30
  raijin_server/modules/sanitize.py,sha256=_RnWn1DUuNrzx3NnKEbMvf5iicgjiN_ubwT59e0rYWY,6040
31
- raijin_server/modules/secrets.py,sha256=C5k10ODo6Ai_e6Ei-JiWTOa-cOh-5d2__PhTQA0UmjI,19452
31
+ raijin_server/modules/secrets.py,sha256=HOFk57LFyzW4XJ3c8uEEPRd5Dj_OYDI1NBVLzJMp0vY,18562
32
32
  raijin_server/modules/ssh_hardening.py,sha256=Zd0dlylUBr01SkrI1CS05-0DB9xIto5rWH1bUVs80ow,5422
33
33
  raijin_server/modules/traefik.py,sha256=omziywss4o-8t64Kj-upLqbXdFYm2JwqOoOukDUmqxY,5008
34
- raijin_server/modules/velero.py,sha256=yDtqd6yUu0L5wzLCjYXqvvxB_RyaAoZtntb6HoHVAOo,5642
34
+ raijin_server/modules/velero.py,sha256=nH7WI145OOK-DZo_ZjNegEnwkppi8h98DeQaB5A_kVg,7161
35
35
  raijin_server/modules/vpn.py,sha256=qWyROiHx2-FzMqhfpmzslrdfRBTewd53ylUq90wR7SQ,9149
36
36
  raijin_server/modules/vpn_client.py,sha256=SjWzSQKLSJCpjz7Y1i1dFaNfPOVIQXFwgD9uM3GYaIY,30035
37
37
  raijin_server/scripts/__init__.py,sha256=deduGfHf8BMVWred4ux5LfBDT2NJ5XYeJAt2sDEU4qs,53
@@ -39,9 +39,9 @@ raijin_server/scripts/checklist.sh,sha256=j6E0Kmk1EfjLvKK1VpCqzXJAXI_7Bm67LK4ndy
39
39
  raijin_server/scripts/install.sh,sha256=Y1ickbQ4siQ0NIPs6UgrqUr8WWy7U0LHmaTQbEgavoI,3949
40
40
  raijin_server/scripts/log_size_metric.sh,sha256=Iv4SsX8AuCYRou-klYn32mX41xB6j0xJGLBO6riw4rU,1208
41
41
  raijin_server/scripts/pre-deploy-check.sh,sha256=XqMo7IMIpwUHF17YEmU0-cVmTDMoCGMBFnmS39FidI4,4912
42
- raijin_server-0.3.6.dist-info/licenses/LICENSE,sha256=kJsMCjOiRZE0AQNtxWqBa32z9kMAaF4EUxyHj3hKaJo,1105
43
- raijin_server-0.3.6.dist-info/METADATA,sha256=egpj4eEFQ3JJAA-bT3Iz5ebOc57e5JpyuQ2a8-ruAD4,8829
44
- raijin_server-0.3.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
45
- raijin_server-0.3.6.dist-info/entry_points.txt,sha256=3ZvxDX4pvcjkIRsXAJ69wIfVmKa78LKo-C3QhqN2KVM,56
46
- raijin_server-0.3.6.dist-info/top_level.txt,sha256=Yz1xneCRtsZOzbPIcTAcrSxd-1p80pohMXYAZ74dpok,14
47
- raijin_server-0.3.6.dist-info/RECORD,,
42
+ raijin_server-0.3.7.dist-info/licenses/LICENSE,sha256=kJsMCjOiRZE0AQNtxWqBa32z9kMAaF4EUxyHj3hKaJo,1105
43
+ raijin_server-0.3.7.dist-info/METADATA,sha256=KGvWKBpPSa4-6oSXFHJ0rykLQ-_VFftLobt0mFm-Co0,8829
44
+ raijin_server-0.3.7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
45
+ raijin_server-0.3.7.dist-info/entry_points.txt,sha256=3ZvxDX4pvcjkIRsXAJ69wIfVmKa78LKo-C3QhqN2KVM,56
46
+ raijin_server-0.3.7.dist-info/top_level.txt,sha256=Yz1xneCRtsZOzbPIcTAcrSxd-1p80pohMXYAZ74dpok,14
47
+ raijin_server-0.3.7.dist-info/RECORD,,
@@ -1,414 +0,0 @@
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 _resolve_ip_access() -> bool:
248
- """Pergunta se deseja acesso via IP direto (para testes)."""
249
- env_ip = os.environ.get("APOKOLIPS_IP_ACCESS")
250
- if env_ip:
251
- return env_ip.strip().lower() in ("1", "true", "yes")
252
- return typer.confirm("Habilitar acesso via IP direto? (apenas para testes)", default=True)
253
-
254
-
255
- def _build_manifest(host: str, tls_secret: str | None, ip_access: bool = False) -> str:
256
- html_block = indent(HTML_TEMPLATE.strip("\n"), " " * 4)
257
- tls_block = ""
258
- if tls_secret:
259
- tls_block = (
260
- " tls:\n"
261
- " - hosts:\n"
262
- f" - {host}\n"
263
- f" secretName: {tls_secret}\n"
264
- )
265
-
266
- # Regra adicional para acesso via IP (sem host)
267
- ip_rule = ""
268
- if ip_access:
269
- ip_rule = """
270
- - http:
271
- paths:
272
- - path: /
273
- pathType: Prefix
274
- backend:
275
- service:
276
- name: apokolips-demo
277
- port:
278
- number: 80"""
279
-
280
- template = """\
281
- apiVersion: v1
282
- kind: Namespace
283
- metadata:
284
- name: {namespace}
285
- ---
286
- apiVersion: v1
287
- kind: ConfigMap
288
- metadata:
289
- name: apokolips-html
290
- namespace: {namespace}
291
- data:
292
- index.html: |
293
- __HTML__
294
- ---
295
- apiVersion: apps/v1
296
- kind: Deployment
297
- metadata:
298
- name: apokolips-demo
299
- namespace: {namespace}
300
- labels:
301
- app: apokolips-demo
302
- spec:
303
- replicas: 1
304
- selector:
305
- matchLabels:
306
- app: apokolips-demo
307
- template:
308
- metadata:
309
- labels:
310
- app: apokolips-demo
311
- spec:
312
- containers:
313
- - name: apokolips-web
314
- image: nginx:1.25
315
- ports:
316
- - containerPort: 80
317
- resources:
318
- limits:
319
- cpu: 100m
320
- memory: 128Mi
321
- requests:
322
- cpu: 50m
323
- memory: 64Mi
324
- volumeMounts:
325
- - name: site
326
- mountPath: /usr/share/nginx/html
327
- readOnly: true
328
- volumes:
329
- - name: site
330
- configMap:
331
- name: apokolips-html
332
- ---
333
- apiVersion: v1
334
- kind: Service
335
- metadata:
336
- name: apokolips-demo
337
- namespace: {namespace}
338
- spec:
339
- type: ClusterIP
340
- selector:
341
- app: apokolips-demo
342
- ports:
343
- - port: 80
344
- targetPort: 80
345
- ---
346
- apiVersion: networking.k8s.io/v1
347
- kind: Ingress
348
- metadata:
349
- name: apokolips-demo
350
- namespace: {namespace}
351
- annotations:
352
- traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
353
- spec:
354
- ingressClassName: traefik
355
- rules:
356
- - host: {host}
357
- http:
358
- paths:
359
- - path: /
360
- pathType: Prefix
361
- backend:
362
- service:
363
- name: apokolips-demo
364
- port:
365
- number: 80__IP_RULE__
366
- __TLS__
367
- """
368
-
369
- manifest = template.format(namespace=NAMESPACE, host=host)
370
- manifest = manifest.replace("__HTML__", html_block)
371
- manifest = manifest.replace("__IP_RULE__", ip_rule)
372
- manifest = manifest.replace("__TLS__", tls_block.rstrip())
373
- return f"{manifest.strip()}\n"
374
-
375
-
376
- def run(ctx: ExecutionContext) -> None:
377
- ensure_tool("kubectl", ctx, install_hint="Instale kubectl para aplicar o manifesto do site.")
378
- host = _resolve_host()
379
- tls_secret = _resolve_tls_secret()
380
- ip_access = _resolve_ip_access()
381
- manifest = _build_manifest(host, tls_secret, ip_access)
382
-
383
- typer.echo("Gerando manifesto Apokolips...")
384
- write_file(TMP_MANIFEST, manifest, ctx)
385
-
386
- try:
387
- run_cmd(["kubectl", "apply", "-f", str(TMP_MANIFEST)], ctx)
388
- finally:
389
- TMP_MANIFEST.unlink(missing_ok=True)
390
-
391
- typer.secho("\nLanding page implantada!", fg=typer.colors.GREEN, bold=True)
392
- typer.echo(f" • Namespace: {NAMESPACE}")
393
- typer.echo(f" • Host: {host}")
394
- if tls_secret:
395
- typer.echo(f" • Secret TLS: {tls_secret}")
396
- if ip_access:
397
- typer.secho(" • Acesso via IP: HABILITADO (apenas para testes)", fg=typer.colors.YELLOW)
398
-
399
- typer.echo("\nTestes sugeridos:")
400
- if ip_access:
401
- typer.echo(" # Acesso direto via IP (teste):")
402
- typer.echo(" curl http://<IP_DO_SERVIDOR>/")
403
- typer.echo("")
404
- typer.echo(f" # Acesso via DNS (produção):")
405
- typer.echo(f" curl -H 'Host: {host}' http://<IP_DO_LOAD_BALANCER>/")
406
- typer.echo(f" kubectl -n {NAMESPACE} get ingress {NAMESPACE}")
407
- typer.echo(f" kubectl -n {NAMESPACE} get pods")
408
-
409
- if ip_access:
410
- typer.secho("\n⚠️ Lembre-se de desabilitar o acesso via IP após configurar o DNS!", fg=typer.colors.YELLOW)
411
- typer.echo(" Rode novamente com APOKOLIPS_IP_ACCESS=false ou responda 'não' na pergunta.")
412
-
413
- typer.echo("\nPara remover:")
414
- typer.echo(f" kubectl delete namespace {NAMESPACE}")