raijin-server 0.1.0__py3-none-any.whl → 0.2.0__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 +58 -4
  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 +5 -0
  25. raijin_server-0.2.0.dist-info/METADATA +407 -0
  26. raijin_server-0.2.0.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.0.dist-info}/WHEEL +0 -0
  30. {raijin_server-0.1.0.dist-info → raijin_server-0.2.0.dist-info}/entry_points.txt +0 -0
  31. {raijin_server-0.1.0.dist-info → raijin_server-0.2.0.dist-info}/licenses/LICENSE +0 -0
  32. {raijin_server-0.1.0.dist-info → raijin_server-0.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,233 @@
1
+ """Provisiona dashboards opinativos do Grafana e alertas padrao do Prometheus/Alertmanager."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import textwrap
7
+ from pathlib import Path
8
+
9
+ import typer
10
+
11
+ from raijin_server.utils import (
12
+ ExecutionContext,
13
+ kubectl_apply,
14
+ kubectl_create_ns,
15
+ require_root,
16
+ write_file,
17
+ )
18
+
19
+ MANIFEST_PATH = Path("/tmp/raijin-observability-dashboards.yaml")
20
+
21
+ CLUSTER_DASHBOARD = {
22
+ "title": "Raijin Cluster Overview",
23
+ "uid": "raijin-cluster",
24
+ "timezone": "browser",
25
+ "schemaVersion": 39,
26
+ "panels": [
27
+ {
28
+ "type": "timeseries",
29
+ "title": "Node CPU %",
30
+ "datasource": "Prometheus",
31
+ "targets": [
32
+ {
33
+ "expr": '100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)',
34
+ "legendFormat": "{{instance}}",
35
+ }
36
+ ],
37
+ },
38
+ {
39
+ "type": "timeseries",
40
+ "title": "Node Memory %",
41
+ "datasource": "Prometheus",
42
+ "targets": [
43
+ {
44
+ "expr": '((node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes) * 100',
45
+ "legendFormat": "{{instance}}",
46
+ }
47
+ ],
48
+ },
49
+ {
50
+ "type": "stat",
51
+ "title": "API Server Errors (5m)",
52
+ "datasource": "Prometheus",
53
+ "targets": [
54
+ {
55
+ "expr": 'sum(rate(apiserver_request_total{code=~"5.."}[5m]))',
56
+ }
57
+ ],
58
+ },
59
+ ],
60
+ }
61
+
62
+ SLO_DASHBOARD = {
63
+ "title": "Raijin SLO - Workloads",
64
+ "uid": "raijin-slo",
65
+ "schemaVersion": 39,
66
+ "panels": [
67
+ {
68
+ "type": "timeseries",
69
+ "title": "Pod Restarts (1h)",
70
+ "datasource": "Prometheus",
71
+ "targets": [
72
+ {
73
+ "expr": 'increase(kube_pod_container_status_restarts_total[1h])',
74
+ "legendFormat": "{{namespace}}/{{pod}}",
75
+ }
76
+ ],
77
+ },
78
+ {
79
+ "type": "timeseries",
80
+ "title": "Ingress HTTP 5xx Rate",
81
+ "datasource": "Prometheus",
82
+ "targets": [
83
+ {
84
+ "expr": 'sum(rate(traefik_service_requests_total{code=~"5.."}[5m])) by (service)',
85
+ "legendFormat": "{{service}}",
86
+ }
87
+ ],
88
+ },
89
+ {
90
+ "type": "gauge",
91
+ "title": "Cluster CPU Pressure",
92
+ "datasource": "Prometheus",
93
+ "targets": [
94
+ {
95
+ "expr": 'avg(node_pressure_cpu_waiting_seconds_total)',
96
+ }
97
+ ],
98
+ },
99
+ ],
100
+ }
101
+
102
+
103
+ def _json_block(obj: dict) -> str:
104
+ return json.dumps(obj, indent=2, separators=(",", ": "))
105
+
106
+
107
+ def _alertmanager_block(contact_email: str, webhook_url: str) -> str:
108
+ receivers = ["receivers:", " - name: default"]
109
+ if contact_email:
110
+ receivers.append(" email_configs:")
111
+ receivers.append(f" - to: {contact_email}")
112
+ receivers.append(" send_resolved: true")
113
+ if webhook_url:
114
+ receivers.append(" webhook_configs:")
115
+ receivers.append(f" - url: {webhook_url}")
116
+ receivers.append(" send_resolved: true")
117
+ receivers_str = "\n".join(receivers) if len(receivers) > 2 else "receivers:\n - name: default"
118
+ return textwrap.dedent(
119
+ f"""
120
+ route:
121
+ receiver: default
122
+ group_by: ['alertname']
123
+ group_wait: 30s
124
+ group_interval: 5m
125
+ repeat_interval: 4h
126
+ {receivers_str}
127
+ """
128
+ ).strip()
129
+
130
+
131
+ def _build_manifest(namespace: str, contact_email: str, webhook_url: str) -> str:
132
+ cluster_json = textwrap.indent(_json_block(CLUSTER_DASHBOARD), " ")
133
+ slo_json = textwrap.indent(_json_block(SLO_DASHBOARD), " ")
134
+ alertmanager_yaml = textwrap.indent(_alertmanager_block(contact_email, webhook_url), " ")
135
+
136
+ prometheus_rules = textwrap.dedent(
137
+ f"""
138
+ apiVersion: monitoring.coreos.com/v1
139
+ kind: PrometheusRule
140
+ metadata:
141
+ name: raijin-default-alerts
142
+ namespace: {namespace}
143
+ spec:
144
+ groups:
145
+ - name: raijin-cluster-health
146
+ rules:
147
+ - alert: NodeDown
148
+ expr: kube_node_status_condition{{condition="Ready",status="true"}} == 0
149
+ for: 5m
150
+ labels:
151
+ severity: critical
152
+ annotations:
153
+ summary: "Node fora do cluster"
154
+ description: "O node {{ $labels.node }} nao reporta Ready ha 5 minutos."
155
+ - alert: HighNodeCPU
156
+ expr: avg(node_load1) by (instance) > count(node_cpu_seconds_total{{mode="system"}}) by (instance)
157
+ for: 10m
158
+ labels:
159
+ severity: warning
160
+ annotations:
161
+ summary: "CPU elevada em {{ $labels.instance }}"
162
+ description: "Load1 superior ao numero de cores ha 10 minutos."
163
+ - alert: APIServerErrorRate
164
+ expr: sum(rate(apiserver_request_total{{code=~"5.."}}[5m])) > 5
165
+ for: 5m
166
+ labels:
167
+ severity: warning
168
+ annotations:
169
+ summary: "API Server retornando 5xx"
170
+ description: "Taxa de erros 5xx do API Server acima de 5 req/s."
171
+ """
172
+ ).strip()
173
+
174
+ grafana_cm = textwrap.dedent(
175
+ f"""
176
+ apiVersion: v1
177
+ kind: ConfigMap
178
+ metadata:
179
+ name: grafana-dashboards-raijin
180
+ namespace: {namespace}
181
+ labels:
182
+ grafana_dashboard: "1"
183
+ data:
184
+ cluster-overview.json: |
185
+ {cluster_json}
186
+ slo-workloads.json: |
187
+ {slo_json}
188
+ """
189
+ ).strip()
190
+
191
+ alertmanager_cm = textwrap.dedent(
192
+ f"""
193
+ apiVersion: v1
194
+ kind: ConfigMap
195
+ metadata:
196
+ name: alertmanager-raijin
197
+ namespace: {namespace}
198
+ labels:
199
+ app.kubernetes.io/name: alertmanager
200
+ data:
201
+ alertmanager.yml: |
202
+ {alertmanager_yaml}
203
+ """
204
+ ).strip()
205
+
206
+ documents = "\n---\n".join([grafana_cm, alertmanager_cm, prometheus_rules])
207
+ return documents + "\n"
208
+
209
+
210
+ def run(ctx: ExecutionContext) -> None:
211
+ require_root(ctx)
212
+ typer.echo("Aplicando dashboards e alertas opinativos...")
213
+
214
+ namespace = typer.prompt("Namespace de observabilidade", default="observability")
215
+ contact_email = typer.prompt(
216
+ "Email para receber alertas (ENTER para ignorar)",
217
+ default="",
218
+ )
219
+ webhook_url = typer.prompt(
220
+ "Webhook HTTP (Slack/Teams) para Alertmanager (ENTER para ignorar)",
221
+ default="",
222
+ )
223
+
224
+ kubectl_create_ns(namespace, ctx)
225
+
226
+ manifest = _build_manifest(namespace, contact_email, webhook_url)
227
+ write_file(MANIFEST_PATH, manifest, ctx)
228
+ kubectl_apply(str(MANIFEST_PATH), ctx)
229
+
230
+ typer.secho("Dashboards e alertas aplicados no namespace de observabilidade.", fg=typer.colors.GREEN)
231
+ typer.echo("Grafana: ConfigMap 'grafana-dashboards-raijin'")
232
+ typer.echo("Alertmanager: ConfigMap 'alertmanager-raijin'")
233
+ typer.echo("Prometheus: PrometheusRule 'raijin-default-alerts'")
@@ -0,0 +1,218 @@
1
+ """Provisiona ingressos seguros para Grafana, Prometheus e Alertmanager."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import subprocess
7
+ from pathlib import Path
8
+ from typing import Dict, List
9
+
10
+ import typer
11
+
12
+ from raijin_server.utils import (
13
+ ExecutionContext,
14
+ kubectl_apply,
15
+ kubectl_create_ns,
16
+ require_root,
17
+ write_file,
18
+ )
19
+
20
+ MANIFEST_PATH = Path("/tmp/raijin-observability-ingress.yaml")
21
+
22
+ COMPONENTS: List[Dict[str, object]] = [
23
+ {
24
+ "key": "grafana",
25
+ "label": "Grafana",
26
+ "service": "grafana",
27
+ "port": 80,
28
+ "default_host": "grafana.example.com",
29
+ "default_tls": "grafana-tls",
30
+ "auth_secret": "grafana-basic-auth",
31
+ "middleware": "grafana-auth",
32
+ },
33
+ {
34
+ "key": "prometheus",
35
+ "label": "Prometheus",
36
+ "service": "kube-prometheus-stack-prometheus",
37
+ "port": 9090,
38
+ "default_host": "prometheus.example.com",
39
+ "default_tls": "prometheus-tls",
40
+ "auth_secret": "prometheus-basic-auth",
41
+ "middleware": "prometheus-auth",
42
+ },
43
+ {
44
+ "key": "alertmanager",
45
+ "label": "Alertmanager",
46
+ "service": "kube-prometheus-stack-alertmanager",
47
+ "port": 9093,
48
+ "default_host": "alerts.example.com",
49
+ "default_tls": "alertmanager-tls",
50
+ "auth_secret": "alertmanager-basic-auth",
51
+ "middleware": "alertmanager-auth",
52
+ },
53
+ ]
54
+
55
+
56
+ def _generate_htpasswd(username: str, password: str) -> str:
57
+ try:
58
+ result = subprocess.run(
59
+ ["openssl", "passwd", "-6", password],
60
+ capture_output=True,
61
+ text=True,
62
+ check=True,
63
+ )
64
+ hashed = result.stdout.strip()
65
+ except Exception:
66
+ import crypt
67
+
68
+ hashed = crypt.crypt(password, crypt.mksalt(crypt.METHOD_SHA512))
69
+ return f"{username}:{hashed}"
70
+
71
+
72
+ def _build_manifest(
73
+ namespace: str,
74
+ ingress_class: str,
75
+ services: List[Dict[str, object]],
76
+ encoded_users: str,
77
+ issuer_name: str,
78
+ issuer_kind: str,
79
+ ) -> str:
80
+ documents: List[str] = []
81
+
82
+ for svc in services:
83
+ host = svc["host"]
84
+ tls_secret = svc["tls_secret"]
85
+ auth_secret = svc["auth_secret"]
86
+ middleware = svc["middleware"]
87
+ service_name = svc["service"]
88
+ port = svc["port"]
89
+ name = svc["key"]
90
+
91
+ documents.append(
92
+ f"""apiVersion: v1
93
+ kind: Secret
94
+ metadata:
95
+ name: {auth_secret}
96
+ namespace: {namespace}
97
+ type: Opaque
98
+ data:
99
+ users: {encoded_users}
100
+ """
101
+ )
102
+
103
+ documents.append(
104
+ f"""apiVersion: traefik.containo.us/v1alpha1
105
+ kind: Middleware
106
+ metadata:
107
+ name: {middleware}
108
+ namespace: {namespace}
109
+ spec:
110
+ basicAuth:
111
+ secret: {auth_secret}
112
+ removeHeader: true
113
+ """
114
+ )
115
+
116
+ if issuer_name:
117
+ documents.append(
118
+ f"""apiVersion: cert-manager.io/v1
119
+ kind: Certificate
120
+ metadata:
121
+ name: {name}-ingress-cert
122
+ namespace: {namespace}
123
+ spec:
124
+ secretName: {tls_secret}
125
+ dnsNames:
126
+ - {host}
127
+ issuerRef:
128
+ name: {issuer_name}
129
+ kind: {issuer_kind}
130
+ """
131
+ )
132
+
133
+ documents.append(
134
+ f"""apiVersion: networking.k8s.io/v1
135
+ kind: Ingress
136
+ metadata:
137
+ name: {name}-secure
138
+ namespace: {namespace}
139
+ annotations:
140
+ traefik.ingress.kubernetes.io/router.middlewares: {namespace}-{middleware}@kubernetescrd
141
+ spec:
142
+ ingressClassName: {ingress_class}
143
+ rules:
144
+ - host: {host}
145
+ http:
146
+ paths:
147
+ - path: /
148
+ pathType: Prefix
149
+ backend:
150
+ service:
151
+ name: {service_name}
152
+ port:
153
+ number: {port}
154
+ tls:
155
+ - secretName: {tls_secret}
156
+ hosts:
157
+ - {host}
158
+ """
159
+ )
160
+
161
+ return "---\n".join(documents)
162
+
163
+
164
+ def run(ctx: ExecutionContext) -> None:
165
+ require_root(ctx)
166
+ typer.echo("Provisionando ingress seguro para observabilidade...")
167
+
168
+ namespace = typer.prompt("Namespace dos componentes", default="observability")
169
+ ingress_class = typer.prompt("IngressClass dedicada", default="traefik")
170
+ username = typer.prompt("Usuario para basic auth", default="observability")
171
+ password = typer.prompt(
172
+ "Senha para basic auth",
173
+ hide_input=True,
174
+ confirmation_prompt=True,
175
+ )
176
+
177
+ configured: List[Dict[str, object]] = []
178
+ for comp in COMPONENTS:
179
+ host = typer.prompt(f"Host para {comp['label']}", default=comp["default_host"])
180
+ tls_secret = typer.prompt(
181
+ f"Secret TLS para {comp['label']}",
182
+ default=comp["default_tls"],
183
+ )
184
+ configured.append({**comp, "host": host, "tls_secret": tls_secret})
185
+
186
+ issue_certs = typer.confirm("Gerar Certificates via cert-manager?", default=True)
187
+ issuer_name = ""
188
+ issuer_kind = "ClusterIssuer"
189
+ if issue_certs:
190
+ issuer_name = typer.prompt("Nome do issuer (cert-manager)", default="letsencrypt-prod")
191
+ issuer_kind = typer.prompt("Tipo do issuer (Issuer/ClusterIssuer)", default="ClusterIssuer")
192
+
193
+ secret_line = _generate_htpasswd(username, password)
194
+ encoded_users = base64.b64encode(secret_line.encode()).decode()
195
+
196
+ kubectl_create_ns(namespace, ctx)
197
+
198
+ manifest = _build_manifest(
199
+ namespace,
200
+ ingress_class,
201
+ configured,
202
+ encoded_users,
203
+ issuer_name,
204
+ issuer_kind,
205
+ )
206
+
207
+ write_file(MANIFEST_PATH, manifest, ctx)
208
+ kubectl_apply(str(MANIFEST_PATH), ctx)
209
+
210
+ typer.secho("Ingress seguro aplicado com sucesso.", fg=typer.colors.GREEN)
211
+ typer.echo("Hosts publicados:")
212
+ for svc in configured:
213
+ typer.echo(f" - {svc['label']}: https://{svc['host']}")
214
+ if not issuer_name:
215
+ typer.secho(
216
+ "Certificados nao foram criados automaticamente. Certifique-se de que os secrets TLS existem.",
217
+ fg=typer.colors.YELLOW,
218
+ )
@@ -0,0 +1,142 @@
1
+ """Sanitizacao do servidor antes de nova instalacao Kubernetes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ import typer
9
+
10
+ from raijin_server.utils import ExecutionContext, require_root, run_cmd
11
+
12
+ SYSTEMD_SERVICES = [
13
+ "kubelet",
14
+ "containerd",
15
+ ]
16
+
17
+ APT_PACKAGES = [
18
+ "kubeadm",
19
+ "kubelet",
20
+ "kubectl",
21
+ "kubernetes-cni",
22
+ "cri-tools",
23
+ "containerd",
24
+ ]
25
+
26
+ CLEAN_PATHS = [
27
+ "/etc/kubernetes",
28
+ "/etc/cni/net.d",
29
+ "/etc/calico",
30
+ "/var/lib/etcd",
31
+ "/var/lib/kubelet",
32
+ "/var/lib/cni",
33
+ "/var/lib/containerd",
34
+ "/var/run/kubernetes",
35
+ "/var/lib/dockershim",
36
+ ]
37
+
38
+ TOOL_BINARIES = [
39
+ "/usr/local/bin/kubectl",
40
+ "/usr/local/bin/helm",
41
+ "/usr/local/bin/istioctl",
42
+ "/usr/local/bin/velero",
43
+ ]
44
+
45
+ APT_MARKERS = [
46
+ "/etc/apt/sources.list.d/kubernetes.list",
47
+ "/etc/apt/keyrings/kubernetes-apt-keyring.gpg",
48
+ ]
49
+
50
+
51
+ def _stop_services(ctx: ExecutionContext) -> None:
52
+ typer.echo("Parando serviços relacionados (kubelet, containerd)...")
53
+ for service in SYSTEMD_SERVICES:
54
+ run_cmd(["systemctl", "stop", service], ctx, check=False)
55
+ run_cmd(["systemctl", "disable", service], ctx, check=False)
56
+
57
+
58
+ def _kubeadm_reset(ctx: ExecutionContext) -> None:
59
+ typer.echo("Executando kubeadm reset...")
60
+ if shutil.which("kubeadm") or ctx.dry_run:
61
+ run_cmd(["kubeadm", "reset", "-f"], ctx, check=False)
62
+ else:
63
+ typer.echo("kubeadm nao encontrado, pulando reset.")
64
+
65
+
66
+ def _flush_iptables(ctx: ExecutionContext) -> None:
67
+ typer.echo("Limpando regras iptables/ip6tables e IPVS...")
68
+ tables = ["filter", "nat", "mangle", "raw"]
69
+ if shutil.which("iptables") or ctx.dry_run:
70
+ for table in tables:
71
+ run_cmd(["iptables", "-t", table, "-F"], ctx, check=False)
72
+ run_cmd(["iptables", "-t", table, "-X"], ctx, check=False)
73
+ if shutil.which("ip6tables") or ctx.dry_run:
74
+ for table in tables:
75
+ run_cmd(["ip6tables", "-t", table, "-F"], ctx, check=False)
76
+ run_cmd(["ip6tables", "-t", table, "-X"], ctx, check=False)
77
+ if shutil.which("ipvsadm") or ctx.dry_run:
78
+ run_cmd(["ipvsadm", "--clear"], ctx, check=False)
79
+
80
+
81
+ def _purge_packages(ctx: ExecutionContext) -> None:
82
+ typer.echo("Removendo pacotes kube* e containerd...")
83
+ run_cmd(["apt-get", "purge", "-y", *APT_PACKAGES], ctx, check=False)
84
+ run_cmd(["apt-get", "autoremove", "-y"], ctx, check=False)
85
+ run_cmd(["apt-get", "clean"], ctx, check=False)
86
+
87
+
88
+ def _remove_paths(ctx: ExecutionContext) -> None:
89
+ typer.echo("Removendo arquivos e diretorios residuais...")
90
+ for path in CLEAN_PATHS:
91
+ run_cmd(["rm", "-rf", path], ctx, check=False)
92
+ # Remove kubeconfigs de root e usuarios em /home
93
+ run_cmd(["rm", "-rf", "/root/.kube"], ctx, check=False)
94
+ home = Path("/home")
95
+ if home.exists() and home.is_dir():
96
+ for entry in home.iterdir():
97
+ candidate = entry / ".kube"
98
+ run_cmd(["rm", "-rf", str(candidate)], ctx, check=False)
99
+
100
+
101
+ def _remove_tool_binaries(ctx: ExecutionContext) -> None:
102
+ typer.echo("Removendo binarios antigos (kubectl, helm, istioctl, velero)...")
103
+ for binary in TOOL_BINARIES:
104
+ run_cmd(["rm", "-f", binary], ctx, check=False)
105
+
106
+
107
+ def _remove_apt_markers(ctx: ExecutionContext) -> None:
108
+ typer.echo("Limpando lista e chave do repositório Kubernetes...")
109
+ for marker in APT_MARKERS:
110
+ run_cmd(["rm", "-f", marker], ctx, check=False)
111
+
112
+
113
+ def run(ctx: ExecutionContext) -> None:
114
+ """Remove instalacoes antigas de Kubernetes antes de reinstalar."""
115
+ require_root(ctx)
116
+
117
+ typer.secho("\n=== Sanitizacao do ambiente Kubernetes ===", fg=typer.colors.CYAN, bold=True)
118
+ typer.echo(
119
+ "Este passo remove clusters existentes, pacotes kube* e arquivos residuais."
120
+ " Use apenas em servidores dedicados ao Raijin Server."
121
+ )
122
+
123
+ if ctx.dry_run:
124
+ typer.echo("[dry-run] Confirmacao automatica para demonstracao.")
125
+ else:
126
+ proceed = typer.confirm(
127
+ "Deseja realmente limpar o servidor antes de uma nova instalacao?",
128
+ default=False,
129
+ )
130
+ if not proceed:
131
+ typer.echo("Sanitizacao cancelada pelo usuario.")
132
+ return
133
+
134
+ _stop_services(ctx)
135
+ _kubeadm_reset(ctx)
136
+ _flush_iptables(ctx)
137
+ _purge_packages(ctx)
138
+ _remove_paths(ctx)
139
+ _remove_tool_binaries(ctx)
140
+ _remove_apt_markers(ctx)
141
+
142
+ typer.secho("\n✓ Sanitizacao concluida. Servidor pronto para novo full_install.", fg=typer.colors.GREEN, bold=True)
@@ -0,0 +1,109 @@
1
+ """Automacao de sealed-secrets e external-secrets via Helm.
2
+
3
+ Instala os controladores necessários para criptografar e consumir segredos
4
+ em clusters Kubernetes. Inclui opcionalmente a exportacao do certificado
5
+ publico do sealed-secrets para permitir geracao de manifests lacrados.
6
+ """
7
+
8
+ from pathlib import Path
9
+
10
+ import typer
11
+
12
+ from raijin_server.utils import (
13
+ ExecutionContext,
14
+ ensure_tool,
15
+ helm_upgrade_install,
16
+ require_root,
17
+ run_cmd,
18
+ )
19
+
20
+ SEALED_NAMESPACE = "kube-system"
21
+ ESO_NAMESPACE = "external-secrets"
22
+
23
+
24
+ def _export_sealed_cert(namespace: str, ctx: ExecutionContext) -> None:
25
+ """Exporta o certificado publico do sealed-secrets para um caminho local."""
26
+
27
+ default_path = Path("/tmp/sealed-secrets-cert.pem")
28
+ dest = typer.prompt(
29
+ "Caminho para salvar o certificado publico do sealed-secrets",
30
+ default=str(default_path),
31
+ )
32
+ typer.echo(f"Exportando certificado para {dest}...")
33
+ cmd = [
34
+ "kubectl",
35
+ "-n",
36
+ namespace,
37
+ "get",
38
+ "secret",
39
+ "-l",
40
+ "sealedsecrets.bitnami.com/sealed-secrets-key",
41
+ "-o",
42
+ r"jsonpath={.items[0].data.tls\.crt}",
43
+ ]
44
+ result = run_cmd(cmd, ctx, check=False)
45
+ if result.returncode != 0:
46
+ typer.secho("Nao foi possivel obter o certificado (tente novamente apos o pod estar Ready).", fg=typer.colors.YELLOW)
47
+ return
48
+
49
+ try:
50
+ import base64
51
+
52
+ cert_b64 = result.stdout.strip()
53
+ cert_bytes = base64.b64decode(cert_b64)
54
+ Path(dest).write_bytes(cert_bytes)
55
+ typer.secho(f"✓ Certificado salvo em {dest}", fg=typer.colors.GREEN)
56
+ except Exception as exc:
57
+ typer.secho(f"Falha ao decodificar/salvar certificado: {exc}", fg=typer.colors.YELLOW)
58
+
59
+
60
+ def run(ctx: ExecutionContext) -> None:
61
+ require_root(ctx)
62
+ ensure_tool("kubectl", ctx, install_hint="Instale kubectl ou habilite dry-run.")
63
+ ensure_tool("helm", ctx, install_hint="Instale helm ou habilite dry-run.")
64
+
65
+ typer.echo("Instalando sealed-secrets e external-secrets...")
66
+
67
+ sealed_ns = typer.prompt("Namespace para sealed-secrets", default=SEALED_NAMESPACE)
68
+ eso_ns = typer.prompt("Namespace para external-secrets", default=ESO_NAMESPACE)
69
+
70
+ # sealed-secrets
71
+ typer.secho("\n== Sealed Secrets ==", fg=typer.colors.CYAN, bold=True)
72
+ helm_upgrade_install(
73
+ "sealed-secrets",
74
+ "sealed-secrets",
75
+ sealed_ns,
76
+ ctx,
77
+ repo="bitnami-labs",
78
+ repo_url="https://bitnami-labs.github.io/sealed-secrets",
79
+ create_namespace=True,
80
+ )
81
+
82
+ typer.echo(
83
+ "Para criar sealed-secrets a partir do seu desktop, exporte o certificado publico e use kubeseal."
84
+ )
85
+ if typer.confirm("Exportar certificado publico agora?", default=True):
86
+ _export_sealed_cert(sealed_ns, ctx)
87
+
88
+ # external-secrets
89
+ typer.secho("\n== External Secrets Operator ==", fg=typer.colors.CYAN, bold=True)
90
+ extra_args = ["--set", "installCRDs=true"]
91
+ helm_upgrade_install(
92
+ "external-secrets",
93
+ "external-secrets",
94
+ eso_ns,
95
+ ctx,
96
+ repo="external-secrets",
97
+ repo_url="https://charts.external-secrets.io",
98
+ create_namespace=True,
99
+ extra_args=extra_args,
100
+ )
101
+
102
+ typer.echo(
103
+ "External Secrets Operator instalado. Configure um SecretStore/ClusterSecretStore conforme seu provedor (AWS/GCP/Vault)."
104
+ )
105
+
106
+ typer.secho("\nDicas rapidas:", fg=typer.colors.GREEN)
107
+ typer.echo("- Gere sealed-secrets localmente: kubeseal --controller-namespace <ns> --controller-name sealed-secrets < secret.yaml > sealed.yaml")
108
+ typer.echo("- Para ESO: crie um SecretStore apontando para seu backend e um ExternalSecret referenciando os keys.")
109
+