raijin-server 0.3.6__tar.gz → 0.3.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {raijin_server-0.3.6/src/raijin_server.egg-info → raijin_server-0.3.8}/PKG-INFO +1 -1
- {raijin_server-0.3.6 → raijin_server-0.3.8}/setup.cfg +1 -1
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/__init__.py +1 -1
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/cli.py +2 -8
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/healthchecks.py +1 -55
- raijin_server-0.3.8/src/raijin_server/minio_utils.py +562 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/__init__.py +1 -2
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/harbor.py +10 -26
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/secrets.py +86 -72
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/velero.py +49 -2
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/validators.py +1 -1
- {raijin_server-0.3.6 → raijin_server-0.3.8/src/raijin_server.egg-info}/PKG-INFO +1 -1
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server.egg-info/SOURCES.txt +1 -1
- raijin_server-0.3.6/src/raijin_server/modules/apokolips_demo.py +0 -414
- {raijin_server-0.3.6 → raijin_server-0.3.8}/LICENSE +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/README.md +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/pyproject.toml +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/config.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/module_manager.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/bootstrap.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/calico.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/cert_manager.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/essentials.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/firewall.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/full_install.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/grafana.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/hardening.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/harness.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/internal_dns.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/istio.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/kafka.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/kong.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/kubernetes.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/loki.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/metallb.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/minio.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/network.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/prometheus.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/sanitize.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/ssh_hardening.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/traefik.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/vpn.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/modules/vpn_client.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/scripts/__init__.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/scripts/checklist.sh +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/scripts/install.sh +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/scripts/log_size_metric.sh +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/scripts/pre-deploy-check.sh +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server/utils.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server.egg-info/dependency_links.txt +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server.egg-info/entry_points.txt +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server.egg-info/requires.txt +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/src/raijin_server.egg-info/top_level.txt +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/tests/test_full_install_sequence.py +0 -0
- {raijin_server-0.3.6 → raijin_server-0.3.8}/tests/test_registry.py +0 -0
|
@@ -17,7 +17,6 @@ from rich.table import Table
|
|
|
17
17
|
|
|
18
18
|
from raijin_server import __version__
|
|
19
19
|
from raijin_server.modules import (
|
|
20
|
-
apokolips_demo,
|
|
21
20
|
bootstrap,
|
|
22
21
|
calico,
|
|
23
22
|
cert_manager,
|
|
@@ -98,7 +97,7 @@ MODULES: Dict[str, Callable[[ExecutionContext], None]] = {
|
|
|
98
97
|
"minio": minio.run,
|
|
99
98
|
"prometheus": prometheus.run,
|
|
100
99
|
"grafana": grafana.run,
|
|
101
|
-
|
|
100
|
+
|
|
102
101
|
"secrets": secrets.run,
|
|
103
102
|
"loki": loki.run,
|
|
104
103
|
"harbor": harbor.run,
|
|
@@ -134,7 +133,7 @@ MODULE_DESCRIPTIONS: Dict[str, str] = {
|
|
|
134
133
|
"minio": "Objeto storage S3-compat via Helm",
|
|
135
134
|
"prometheus": "Stack kube-prometheus",
|
|
136
135
|
"grafana": "Dashboards e datasource Prometheus",
|
|
137
|
-
|
|
136
|
+
|
|
138
137
|
"secrets": "Secrets management (Vault + External Secrets Operator)",
|
|
139
138
|
"loki": "Logs centralizados Loki",
|
|
140
139
|
"harbor": "Container registry privado com vulnerability scanning",
|
|
@@ -552,11 +551,6 @@ def grafana(ctx: typer.Context) -> None:
|
|
|
552
551
|
_run_module(ctx, "grafana")
|
|
553
552
|
|
|
554
553
|
|
|
555
|
-
@app.command(name="apokolips-demo")
|
|
556
|
-
def apokolips_demo_cmd(ctx: typer.Context) -> None:
|
|
557
|
-
_run_module(ctx, "apokolips_demo")
|
|
558
|
-
|
|
559
|
-
|
|
560
554
|
@app.command()
|
|
561
555
|
def loki(ctx: typer.Context) -> None:
|
|
562
556
|
_run_module(ctx, "loki")
|
|
@@ -359,60 +359,6 @@ def verify_secrets(ctx: ExecutionContext) -> bool:
|
|
|
359
359
|
return sealed_ok and eso_ok
|
|
360
360
|
|
|
361
361
|
|
|
362
|
-
def verify_apokolips_demo(ctx: ExecutionContext) -> bool:
|
|
363
|
-
"""Health check especifico para a landing page Apokolips."""
|
|
364
|
-
namespace = "apokolips-demo"
|
|
365
|
-
logger.info("Verificando health check: apokolips-demo")
|
|
366
|
-
typer.secho("\n=== Health Check: Apokolips Demo ===", fg=typer.colors.CYAN)
|
|
367
|
-
|
|
368
|
-
pods_ok = check_k8s_pods_in_namespace(namespace, ctx, timeout=120)
|
|
369
|
-
if not pods_ok:
|
|
370
|
-
return False
|
|
371
|
-
if ctx.dry_run:
|
|
372
|
-
return True
|
|
373
|
-
|
|
374
|
-
try:
|
|
375
|
-
import json
|
|
376
|
-
|
|
377
|
-
result = subprocess.run(
|
|
378
|
-
[
|
|
379
|
-
"kubectl",
|
|
380
|
-
"get",
|
|
381
|
-
"ingress",
|
|
382
|
-
"apokolips-demo",
|
|
383
|
-
"-n",
|
|
384
|
-
namespace,
|
|
385
|
-
"-o",
|
|
386
|
-
"json",
|
|
387
|
-
],
|
|
388
|
-
capture_output=True,
|
|
389
|
-
text=True,
|
|
390
|
-
timeout=10,
|
|
391
|
-
)
|
|
392
|
-
if result.returncode != 0:
|
|
393
|
-
typer.secho(" ✗ Nao foi possivel consultar o ingress", fg=typer.colors.YELLOW)
|
|
394
|
-
logger.warning("kubectl get ingress retornou codigo != 0 para apokolips-demo")
|
|
395
|
-
return False
|
|
396
|
-
|
|
397
|
-
data = json.loads(result.stdout)
|
|
398
|
-
ingress_data = data.get("status", {}).get("loadBalancer", {}).get("ingress", [])
|
|
399
|
-
address = ""
|
|
400
|
-
if ingress_data:
|
|
401
|
-
entry = ingress_data[0]
|
|
402
|
-
address = entry.get("ip") or entry.get("hostname", "")
|
|
403
|
-
|
|
404
|
-
if address:
|
|
405
|
-
typer.secho(f" ✓ LoadBalancer publicado ({address})", fg=typer.colors.GREEN)
|
|
406
|
-
return True
|
|
407
|
-
|
|
408
|
-
typer.secho(" ✗ LoadBalancer ainda sem IP/hostname", fg=typer.colors.YELLOW)
|
|
409
|
-
return False
|
|
410
|
-
except Exception as exc:
|
|
411
|
-
typer.secho(f" ✗ Erro ao verificar ingress: {exc}", fg=typer.colors.YELLOW)
|
|
412
|
-
logger.error(f"Erro verificando ingress apokolips-demo: {exc}")
|
|
413
|
-
return False
|
|
414
|
-
|
|
415
|
-
|
|
416
362
|
# Mapeamento de modulos para funcoes de health check
|
|
417
363
|
HEALTH_CHECKS = {
|
|
418
364
|
"essentials": verify_essentials,
|
|
@@ -429,7 +375,7 @@ HEALTH_CHECKS = {
|
|
|
429
375
|
"kafka": lambda ctx: verify_helm_chart("kafka", "kafka", ctx),
|
|
430
376
|
"cert_manager": verify_cert_manager,
|
|
431
377
|
"secrets": verify_secrets,
|
|
432
|
-
|
|
378
|
+
|
|
433
379
|
}
|
|
434
380
|
|
|
435
381
|
|
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
"""Utilitários centralizados para gerenciamento de usuários e políticas MinIO.
|
|
2
|
+
|
|
3
|
+
Este módulo implementa o princípio do menor privilégio para aplicações que
|
|
4
|
+
usam MinIO como backend de storage, criando usuários específicos com acesso
|
|
5
|
+
limitado aos buckets necessários.
|
|
6
|
+
|
|
7
|
+
Aplicações suportadas:
|
|
8
|
+
- vault-user → vault-storage bucket
|
|
9
|
+
- velero-user → velero-backups bucket
|
|
10
|
+
- harbor-user → harbor-registry, harbor-chartmuseum, harbor-jobservice buckets
|
|
11
|
+
- loki-user → loki-chunks, loki-ruler, loki-admin buckets (futuro)
|
|
12
|
+
|
|
13
|
+
Uso:
|
|
14
|
+
from raijin_server.minio_utils import get_or_create_minio_user
|
|
15
|
+
|
|
16
|
+
# Cria usuário específico para a aplicação
|
|
17
|
+
access_key, secret_key = get_or_create_minio_user(
|
|
18
|
+
ctx=ctx,
|
|
19
|
+
app_name="harbor",
|
|
20
|
+
buckets=["harbor-registry", "harbor-chartmuseum", "harbor-jobservice"],
|
|
21
|
+
)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import base64
|
|
25
|
+
import json
|
|
26
|
+
import secrets
|
|
27
|
+
import time
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Optional
|
|
30
|
+
|
|
31
|
+
import typer
|
|
32
|
+
|
|
33
|
+
from raijin_server.utils import ExecutionContext, run_cmd
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Configuração de usuários por aplicação
|
|
37
|
+
MINIO_APP_USERS = {
|
|
38
|
+
"vault": {
|
|
39
|
+
"username": "vault-user",
|
|
40
|
+
"buckets": ["vault-storage"],
|
|
41
|
+
"description": "Usuário para HashiCorp Vault backend storage",
|
|
42
|
+
},
|
|
43
|
+
"velero": {
|
|
44
|
+
"username": "velero-user",
|
|
45
|
+
"buckets": ["velero-backups"],
|
|
46
|
+
"description": "Usuário para Velero backup storage",
|
|
47
|
+
},
|
|
48
|
+
"harbor": {
|
|
49
|
+
"username": "harbor-user",
|
|
50
|
+
"buckets": ["harbor-registry", "harbor-chartmuseum", "harbor-jobservice"],
|
|
51
|
+
"description": "Usuário para Harbor container registry",
|
|
52
|
+
},
|
|
53
|
+
"loki": {
|
|
54
|
+
"username": "loki-user",
|
|
55
|
+
"buckets": ["loki-chunks", "loki-ruler", "loki-admin"],
|
|
56
|
+
"description": "Usuário para Loki logs storage",
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _generate_password(length: int = 32) -> str:
|
|
62
|
+
"""Gera senha aleatória segura."""
|
|
63
|
+
return secrets.token_urlsafe(length)[:length]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _get_minio_root_credentials(ctx: ExecutionContext) -> tuple[str, str]:
|
|
67
|
+
"""Obtém credenciais root do MinIO do Secret do K8s."""
|
|
68
|
+
# Tenta secret 'minio' primeiro (padrão do Helm chart)
|
|
69
|
+
result = run_cmd(
|
|
70
|
+
["kubectl", "-n", "minio", "get", "secret", "minio", "-o", "jsonpath={.data.rootUser}"],
|
|
71
|
+
ctx,
|
|
72
|
+
check=False,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if result.returncode == 0 and result.stdout:
|
|
76
|
+
root_user = base64.b64decode(result.stdout.strip()).decode("utf-8")
|
|
77
|
+
|
|
78
|
+
result = run_cmd(
|
|
79
|
+
["kubectl", "-n", "minio", "get", "secret", "minio", "-o", "jsonpath={.data.rootPassword}"],
|
|
80
|
+
ctx,
|
|
81
|
+
check=False,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if result.returncode == 0 and result.stdout:
|
|
85
|
+
root_password = base64.b64decode(result.stdout.strip()).decode("utf-8")
|
|
86
|
+
return root_user, root_password
|
|
87
|
+
|
|
88
|
+
# Fallback para secret minio-credentials
|
|
89
|
+
result = run_cmd(
|
|
90
|
+
["kubectl", "-n", "minio", "get", "secret", "minio-credentials", "-o", "jsonpath={.data.accesskey}"],
|
|
91
|
+
ctx,
|
|
92
|
+
check=False,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if result.returncode == 0 and result.stdout:
|
|
96
|
+
access_key = base64.b64decode(result.stdout.strip()).decode("utf-8")
|
|
97
|
+
|
|
98
|
+
result = run_cmd(
|
|
99
|
+
["kubectl", "-n", "minio", "get", "secret", "minio-credentials", "-o", "jsonpath={.data.secretkey}"],
|
|
100
|
+
ctx,
|
|
101
|
+
check=False,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if result.returncode == 0 and result.stdout:
|
|
105
|
+
secret_key = base64.b64decode(result.stdout.strip()).decode("utf-8")
|
|
106
|
+
return access_key, secret_key
|
|
107
|
+
|
|
108
|
+
raise RuntimeError("Não foi possível obter credenciais root do MinIO. Verifique se o MinIO está instalado.")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _setup_mc_alias(ctx: ExecutionContext, root_user: str, root_password: str) -> bool:
|
|
112
|
+
"""Configura alias 'local' no mc dentro do pod MinIO."""
|
|
113
|
+
result = run_cmd(
|
|
114
|
+
[
|
|
115
|
+
"kubectl", "-n", "minio", "exec", "minio-0", "--",
|
|
116
|
+
"mc", "alias", "set", "local", "http://localhost:9000", root_user, root_password,
|
|
117
|
+
],
|
|
118
|
+
ctx,
|
|
119
|
+
check=False,
|
|
120
|
+
)
|
|
121
|
+
return result.returncode == 0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _check_user_exists(ctx: ExecutionContext, username: str) -> bool:
|
|
125
|
+
"""Verifica se usuário já existe no MinIO."""
|
|
126
|
+
result = run_cmd(
|
|
127
|
+
["kubectl", "-n", "minio", "exec", "minio-0", "--", "mc", "admin", "user", "info", "local", username],
|
|
128
|
+
ctx,
|
|
129
|
+
check=False,
|
|
130
|
+
)
|
|
131
|
+
return result.returncode == 0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _create_minio_user(ctx: ExecutionContext, username: str, password: str) -> bool:
|
|
135
|
+
"""Cria usuário no MinIO."""
|
|
136
|
+
result = run_cmd(
|
|
137
|
+
[
|
|
138
|
+
"kubectl", "-n", "minio", "exec", "minio-0", "--",
|
|
139
|
+
"mc", "admin", "user", "add", "local", username, password,
|
|
140
|
+
],
|
|
141
|
+
ctx,
|
|
142
|
+
check=False,
|
|
143
|
+
)
|
|
144
|
+
return result.returncode == 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _create_bucket_policy(ctx: ExecutionContext, policy_name: str, buckets: list[str]) -> bool:
|
|
148
|
+
"""Cria política de acesso restrita aos buckets especificados."""
|
|
149
|
+
# Cria documento de policy S3
|
|
150
|
+
policy_doc = {
|
|
151
|
+
"Version": "2012-10-17",
|
|
152
|
+
"Statement": [
|
|
153
|
+
{
|
|
154
|
+
"Effect": "Allow",
|
|
155
|
+
"Action": [
|
|
156
|
+
"s3:GetBucketLocation",
|
|
157
|
+
"s3:ListBucket",
|
|
158
|
+
"s3:ListBucketMultipartUploads",
|
|
159
|
+
],
|
|
160
|
+
"Resource": [f"arn:aws:s3:::{bucket}" for bucket in buckets],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"Effect": "Allow",
|
|
164
|
+
"Action": [
|
|
165
|
+
"s3:GetObject",
|
|
166
|
+
"s3:PutObject",
|
|
167
|
+
"s3:DeleteObject",
|
|
168
|
+
"s3:ListMultipartUploadParts",
|
|
169
|
+
"s3:AbortMultipartUpload",
|
|
170
|
+
],
|
|
171
|
+
"Resource": [f"arn:aws:s3:::{bucket}/*" for bucket in buckets],
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
policy_json = json.dumps(policy_doc)
|
|
177
|
+
|
|
178
|
+
# Salva policy em arquivo temporário dentro do pod
|
|
179
|
+
result = run_cmd(
|
|
180
|
+
[
|
|
181
|
+
"kubectl", "-n", "minio", "exec", "minio-0", "--",
|
|
182
|
+
"sh", "-c", f"echo '{policy_json}' > /tmp/{policy_name}.json",
|
|
183
|
+
],
|
|
184
|
+
ctx,
|
|
185
|
+
check=False,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if result.returncode != 0:
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
# Cria policy no MinIO
|
|
192
|
+
result = run_cmd(
|
|
193
|
+
[
|
|
194
|
+
"kubectl", "-n", "minio", "exec", "minio-0", "--",
|
|
195
|
+
"mc", "admin", "policy", "create", "local", policy_name, f"/tmp/{policy_name}.json",
|
|
196
|
+
],
|
|
197
|
+
ctx,
|
|
198
|
+
check=False,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Se policy já existe, atualiza
|
|
202
|
+
if result.returncode != 0:
|
|
203
|
+
result = run_cmd(
|
|
204
|
+
[
|
|
205
|
+
"kubectl", "-n", "minio", "exec", "minio-0", "--",
|
|
206
|
+
"mc", "admin", "policy", "remove", "local", policy_name,
|
|
207
|
+
],
|
|
208
|
+
ctx,
|
|
209
|
+
check=False,
|
|
210
|
+
)
|
|
211
|
+
result = run_cmd(
|
|
212
|
+
[
|
|
213
|
+
"kubectl", "-n", "minio", "exec", "minio-0", "--",
|
|
214
|
+
"mc", "admin", "policy", "create", "local", policy_name, f"/tmp/{policy_name}.json",
|
|
215
|
+
],
|
|
216
|
+
ctx,
|
|
217
|
+
check=False,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return result.returncode == 0
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _attach_policy_to_user(ctx: ExecutionContext, username: str, policy_name: str) -> bool:
|
|
224
|
+
"""Associa política ao usuário."""
|
|
225
|
+
result = run_cmd(
|
|
226
|
+
[
|
|
227
|
+
"kubectl", "-n", "minio", "exec", "minio-0", "--",
|
|
228
|
+
"mc", "admin", "policy", "attach", "local", policy_name, "--user", username,
|
|
229
|
+
],
|
|
230
|
+
ctx,
|
|
231
|
+
check=False,
|
|
232
|
+
)
|
|
233
|
+
return result.returncode == 0
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _create_bucket(ctx: ExecutionContext, bucket_name: str) -> bool:
|
|
237
|
+
"""Cria bucket se não existir."""
|
|
238
|
+
# Verifica se bucket existe
|
|
239
|
+
result = run_cmd(
|
|
240
|
+
["kubectl", "-n", "minio", "exec", "minio-0", "--", "mc", "ls", f"local/{bucket_name}"],
|
|
241
|
+
ctx,
|
|
242
|
+
check=False,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if result.returncode == 0:
|
|
246
|
+
typer.echo(f" Bucket '{bucket_name}' já existe.")
|
|
247
|
+
return True
|
|
248
|
+
|
|
249
|
+
# Cria bucket
|
|
250
|
+
result = run_cmd(
|
|
251
|
+
["kubectl", "-n", "minio", "exec", "minio-0", "--", "mc", "mb", f"local/{bucket_name}"],
|
|
252
|
+
ctx,
|
|
253
|
+
check=False,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if result.returncode == 0:
|
|
257
|
+
typer.secho(f" ✓ Bucket '{bucket_name}' criado.", fg=typer.colors.GREEN)
|
|
258
|
+
return True
|
|
259
|
+
|
|
260
|
+
typer.secho(f" ✗ Falha ao criar bucket '{bucket_name}'.", fg=typer.colors.RED)
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _save_credentials_to_k8s_secret(
|
|
265
|
+
ctx: ExecutionContext,
|
|
266
|
+
app_name: str,
|
|
267
|
+
username: str,
|
|
268
|
+
password: str,
|
|
269
|
+
namespace: str,
|
|
270
|
+
) -> bool:
|
|
271
|
+
"""Salva credenciais do usuário em um Secret K8s."""
|
|
272
|
+
import subprocess
|
|
273
|
+
import tempfile
|
|
274
|
+
from pathlib import Path
|
|
275
|
+
|
|
276
|
+
secret_name = f"minio-{app_name}-credentials"
|
|
277
|
+
|
|
278
|
+
# Cria ou atualiza secret
|
|
279
|
+
secret_manifest = f"""apiVersion: v1
|
|
280
|
+
kind: Secret
|
|
281
|
+
metadata:
|
|
282
|
+
name: {secret_name}
|
|
283
|
+
namespace: {namespace}
|
|
284
|
+
labels:
|
|
285
|
+
app.kubernetes.io/managed-by: raijin-server
|
|
286
|
+
app.kubernetes.io/component: minio-credentials
|
|
287
|
+
app.kubernetes.io/part-of: {app_name}
|
|
288
|
+
type: Opaque
|
|
289
|
+
stringData:
|
|
290
|
+
accesskey: "{username}"
|
|
291
|
+
secretkey: "{password}"
|
|
292
|
+
AWS_ACCESS_KEY_ID: "{username}"
|
|
293
|
+
AWS_SECRET_ACCESS_KEY: "{password}"
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
# Usa arquivo temporário para aplicar o manifest
|
|
297
|
+
tmp_path = None
|
|
298
|
+
try:
|
|
299
|
+
with tempfile.NamedTemporaryFile("w", delete=False, suffix=".yaml") as tmp:
|
|
300
|
+
tmp.write(secret_manifest)
|
|
301
|
+
tmp.flush()
|
|
302
|
+
tmp_path = Path(tmp.name)
|
|
303
|
+
|
|
304
|
+
result = run_cmd(
|
|
305
|
+
["kubectl", "apply", "-f", str(tmp_path)],
|
|
306
|
+
ctx,
|
|
307
|
+
check=False,
|
|
308
|
+
)
|
|
309
|
+
return result.returncode == 0
|
|
310
|
+
finally:
|
|
311
|
+
if tmp_path and tmp_path.exists():
|
|
312
|
+
tmp_path.unlink(missing_ok=True)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _get_credentials_from_k8s_secret(
|
|
316
|
+
ctx: ExecutionContext,
|
|
317
|
+
app_name: str,
|
|
318
|
+
namespace: str,
|
|
319
|
+
) -> Optional[tuple[str, str]]:
|
|
320
|
+
"""Recupera credenciais do Secret K8s se existir."""
|
|
321
|
+
secret_name = f"minio-{app_name}-credentials"
|
|
322
|
+
|
|
323
|
+
result = run_cmd(
|
|
324
|
+
["kubectl", "-n", namespace, "get", "secret", secret_name, "-o", "jsonpath={.data.accesskey}"],
|
|
325
|
+
ctx,
|
|
326
|
+
check=False,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
if result.returncode == 0 and result.stdout:
|
|
330
|
+
access_key = base64.b64decode(result.stdout.strip()).decode("utf-8")
|
|
331
|
+
|
|
332
|
+
result = run_cmd(
|
|
333
|
+
["kubectl", "-n", namespace, "get", "secret", secret_name, "-o", "jsonpath={.data.secretkey}"],
|
|
334
|
+
ctx,
|
|
335
|
+
check=False,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if result.returncode == 0 and result.stdout:
|
|
339
|
+
secret_key = base64.b64decode(result.stdout.strip()).decode("utf-8")
|
|
340
|
+
return access_key, secret_key
|
|
341
|
+
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def get_or_create_minio_user(
|
|
346
|
+
ctx: ExecutionContext,
|
|
347
|
+
app_name: str,
|
|
348
|
+
buckets: Optional[list[str]] = None,
|
|
349
|
+
namespace: Optional[str] = None,
|
|
350
|
+
force_recreate: bool = False,
|
|
351
|
+
) -> tuple[str, str]:
|
|
352
|
+
"""Obtém ou cria usuário MinIO específico para uma aplicação.
|
|
353
|
+
|
|
354
|
+
Esta função implementa o princípio do menor privilégio, criando um usuário
|
|
355
|
+
MinIO com acesso restrito apenas aos buckets necessários para a aplicação.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
ctx: Contexto de execução
|
|
359
|
+
app_name: Nome da aplicação (vault, velero, harbor, loki)
|
|
360
|
+
buckets: Lista de buckets para criar/permitir acesso (opcional, usa padrão se não fornecido)
|
|
361
|
+
namespace: Namespace K8s para salvar o secret (opcional, usa app_name se não fornecido)
|
|
362
|
+
force_recreate: Se True, recria usuário mesmo se já existir
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
Tupla (access_key, secret_key) com as credenciais do usuário
|
|
366
|
+
|
|
367
|
+
Raises:
|
|
368
|
+
RuntimeError: Se não conseguir criar usuário ou MinIO não estiver disponível
|
|
369
|
+
"""
|
|
370
|
+
typer.echo(f"\n🔐 Configurando usuário MinIO para '{app_name}'...")
|
|
371
|
+
|
|
372
|
+
# Obtém configuração padrão se não fornecida
|
|
373
|
+
if app_name in MINIO_APP_USERS:
|
|
374
|
+
config = MINIO_APP_USERS[app_name]
|
|
375
|
+
username = config["username"]
|
|
376
|
+
if buckets is None:
|
|
377
|
+
buckets = config["buckets"]
|
|
378
|
+
if namespace is None:
|
|
379
|
+
namespace = app_name
|
|
380
|
+
else:
|
|
381
|
+
username = f"{app_name}-user"
|
|
382
|
+
if buckets is None:
|
|
383
|
+
buckets = [f"{app_name}-data"]
|
|
384
|
+
if namespace is None:
|
|
385
|
+
namespace = app_name
|
|
386
|
+
|
|
387
|
+
policy_name = f"{app_name}-policy"
|
|
388
|
+
|
|
389
|
+
# Verifica se já existe secret com credenciais
|
|
390
|
+
if not force_recreate:
|
|
391
|
+
existing_creds = _get_credentials_from_k8s_secret(ctx, app_name, namespace)
|
|
392
|
+
if existing_creds:
|
|
393
|
+
typer.secho(f" ✓ Credenciais existentes encontradas para '{app_name}'.", fg=typer.colors.GREEN)
|
|
394
|
+
return existing_creds
|
|
395
|
+
|
|
396
|
+
if ctx.dry_run:
|
|
397
|
+
typer.echo(f" [DRY-RUN] Criaria usuário '{username}' com acesso a: {', '.join(buckets)}")
|
|
398
|
+
return (username, "dry-run-password")
|
|
399
|
+
|
|
400
|
+
# Obtém credenciais root
|
|
401
|
+
try:
|
|
402
|
+
root_user, root_password = _get_minio_root_credentials(ctx)
|
|
403
|
+
except RuntimeError as e:
|
|
404
|
+
typer.secho(f" ✗ {e}", fg=typer.colors.RED)
|
|
405
|
+
raise
|
|
406
|
+
|
|
407
|
+
# Configura mc alias
|
|
408
|
+
if not _setup_mc_alias(ctx, root_user, root_password):
|
|
409
|
+
raise RuntimeError("Falha ao configurar mc alias no MinIO.")
|
|
410
|
+
|
|
411
|
+
# Cria buckets necessários
|
|
412
|
+
typer.echo(f" Criando buckets para '{app_name}'...")
|
|
413
|
+
for bucket in buckets:
|
|
414
|
+
_create_bucket(ctx, bucket)
|
|
415
|
+
|
|
416
|
+
# Verifica se usuário já existe
|
|
417
|
+
user_exists = _check_user_exists(ctx, username)
|
|
418
|
+
|
|
419
|
+
if user_exists and not force_recreate:
|
|
420
|
+
# Recupera senha existente do secret se disponível
|
|
421
|
+
existing = _get_credentials_from_k8s_secret(ctx, app_name, namespace)
|
|
422
|
+
if existing:
|
|
423
|
+
typer.secho(f" ✓ Usuário '{username}' já existe.", fg=typer.colors.GREEN)
|
|
424
|
+
return existing
|
|
425
|
+
|
|
426
|
+
# Gera nova senha
|
|
427
|
+
password = _generate_password(32)
|
|
428
|
+
|
|
429
|
+
# Cria ou recria usuário
|
|
430
|
+
if user_exists:
|
|
431
|
+
typer.echo(f" Recriando usuário '{username}'...")
|
|
432
|
+
# Remove usuário antigo
|
|
433
|
+
run_cmd(
|
|
434
|
+
["kubectl", "-n", "minio", "exec", "minio-0", "--", "mc", "admin", "user", "remove", "local", username],
|
|
435
|
+
ctx,
|
|
436
|
+
check=False,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
typer.echo(f" Criando usuário '{username}'...")
|
|
440
|
+
if not _create_minio_user(ctx, username, password):
|
|
441
|
+
raise RuntimeError(f"Falha ao criar usuário MinIO '{username}'.")
|
|
442
|
+
|
|
443
|
+
# Cria política de acesso
|
|
444
|
+
typer.echo(f" Criando política '{policy_name}'...")
|
|
445
|
+
if not _create_bucket_policy(ctx, policy_name, buckets):
|
|
446
|
+
raise RuntimeError(f"Falha ao criar política '{policy_name}'.")
|
|
447
|
+
|
|
448
|
+
# Associa política ao usuário
|
|
449
|
+
typer.echo(f" Associando política ao usuário...")
|
|
450
|
+
if not _attach_policy_to_user(ctx, username, policy_name):
|
|
451
|
+
raise RuntimeError(f"Falha ao associar política ao usuário '{username}'.")
|
|
452
|
+
|
|
453
|
+
# Garante que namespace existe
|
|
454
|
+
run_cmd(
|
|
455
|
+
["kubectl", "create", "namespace", namespace, "--dry-run=client", "-o", "yaml"],
|
|
456
|
+
ctx,
|
|
457
|
+
check=False,
|
|
458
|
+
)
|
|
459
|
+
run_cmd(
|
|
460
|
+
["kubectl", "create", "namespace", namespace],
|
|
461
|
+
ctx,
|
|
462
|
+
check=False,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# Salva credenciais em Secret K8s
|
|
466
|
+
typer.echo(f" Salvando credenciais em Secret K8s...")
|
|
467
|
+
if not _save_credentials_to_k8s_secret(ctx, app_name, username, password, namespace):
|
|
468
|
+
typer.secho(f" ⚠️ Falha ao salvar secret, mas usuário foi criado.", fg=typer.colors.YELLOW)
|
|
469
|
+
|
|
470
|
+
typer.secho(f" ✓ Usuário '{username}' criado com acesso a: {', '.join(buckets)}", fg=typer.colors.GREEN)
|
|
471
|
+
|
|
472
|
+
return (username, password)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def list_minio_users(ctx: ExecutionContext) -> list[dict]:
|
|
476
|
+
"""Lista todos os usuários MinIO (exceto root)."""
|
|
477
|
+
try:
|
|
478
|
+
root_user, root_password = _get_minio_root_credentials(ctx)
|
|
479
|
+
_setup_mc_alias(ctx, root_user, root_password)
|
|
480
|
+
except RuntimeError:
|
|
481
|
+
return []
|
|
482
|
+
|
|
483
|
+
result = run_cmd(
|
|
484
|
+
["kubectl", "-n", "minio", "exec", "minio-0", "--", "mc", "admin", "user", "ls", "local", "--json"],
|
|
485
|
+
ctx,
|
|
486
|
+
check=False,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
if result.returncode != 0:
|
|
490
|
+
return []
|
|
491
|
+
|
|
492
|
+
users = []
|
|
493
|
+
for line in (result.stdout or "").strip().split("\n"):
|
|
494
|
+
if line.strip():
|
|
495
|
+
try:
|
|
496
|
+
data = json.loads(line)
|
|
497
|
+
if data.get("accessKey"):
|
|
498
|
+
users.append({
|
|
499
|
+
"username": data.get("accessKey"),
|
|
500
|
+
"status": data.get("userStatus", "enabled"),
|
|
501
|
+
"policy": data.get("policyName", ""),
|
|
502
|
+
})
|
|
503
|
+
except json.JSONDecodeError:
|
|
504
|
+
pass
|
|
505
|
+
|
|
506
|
+
return users
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def delete_minio_user(ctx: ExecutionContext, app_name: str) -> bool:
|
|
510
|
+
"""Remove usuário MinIO e seu secret associado."""
|
|
511
|
+
if app_name in MINIO_APP_USERS:
|
|
512
|
+
username = MINIO_APP_USERS[app_name]["username"]
|
|
513
|
+
else:
|
|
514
|
+
username = f"{app_name}-user"
|
|
515
|
+
|
|
516
|
+
policy_name = f"{app_name}-policy"
|
|
517
|
+
secret_name = f"minio-{app_name}-credentials"
|
|
518
|
+
|
|
519
|
+
typer.echo(f"Removendo usuário MinIO '{username}'...")
|
|
520
|
+
|
|
521
|
+
try:
|
|
522
|
+
root_user, root_password = _get_minio_root_credentials(ctx)
|
|
523
|
+
_setup_mc_alias(ctx, root_user, root_password)
|
|
524
|
+
except RuntimeError as e:
|
|
525
|
+
typer.secho(f" ✗ {e}", fg=typer.colors.RED)
|
|
526
|
+
return False
|
|
527
|
+
|
|
528
|
+
# Remove associação de política
|
|
529
|
+
run_cmd(
|
|
530
|
+
[
|
|
531
|
+
"kubectl", "-n", "minio", "exec", "minio-0", "--",
|
|
532
|
+
"mc", "admin", "policy", "detach", "local", policy_name, "--user", username,
|
|
533
|
+
],
|
|
534
|
+
ctx,
|
|
535
|
+
check=False,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
# Remove usuário
|
|
539
|
+
result = run_cmd(
|
|
540
|
+
["kubectl", "-n", "minio", "exec", "minio-0", "--", "mc", "admin", "user", "remove", "local", username],
|
|
541
|
+
ctx,
|
|
542
|
+
check=False,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
if result.returncode == 0:
|
|
546
|
+
typer.secho(f" ✓ Usuário '{username}' removido.", fg=typer.colors.GREEN)
|
|
547
|
+
|
|
548
|
+
# Remove política
|
|
549
|
+
run_cmd(
|
|
550
|
+
["kubectl", "-n", "minio", "exec", "minio-0", "--", "mc", "admin", "policy", "remove", "local", policy_name],
|
|
551
|
+
ctx,
|
|
552
|
+
check=False,
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# Remove secret K8s
|
|
556
|
+
run_cmd(
|
|
557
|
+
["kubectl", "-n", app_name, "delete", "secret", secret_name, "--ignore-not-found"],
|
|
558
|
+
ctx,
|
|
559
|
+
check=False,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
return True
|