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
|
@@ -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
|
+
|