raijin-server 0.2.3__py3-none-any.whl → 0.2.5__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 +77 -0
- raijin_server/healthchecks.py +61 -2
- raijin_server/modules/apokolips_demo.py +40 -4
- raijin_server/modules/cert_manager.py +950 -71
- raijin_server/modules/full_install.py +44 -1
- raijin_server/utils.py +79 -2
- {raijin_server-0.2.3.dist-info → raijin_server-0.2.5.dist-info}/METADATA +65 -1
- {raijin_server-0.2.3.dist-info → raijin_server-0.2.5.dist-info}/RECORD +13 -13
- {raijin_server-0.2.3.dist-info → raijin_server-0.2.5.dist-info}/WHEEL +0 -0
- {raijin_server-0.2.3.dist-info → raijin_server-0.2.5.dist-info}/entry_points.txt +0 -0
- {raijin_server-0.2.3.dist-info → raijin_server-0.2.5.dist-info}/licenses/LICENSE +0 -0
- {raijin_server-0.2.3.dist-info → raijin_server-0.2.5.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,22 @@
|
|
|
1
|
-
"""Instala e configura cert-manager com emissores ACME (HTTP-01 ou DNS-01).
|
|
1
|
+
"""Instala e configura cert-manager com emissores ACME (HTTP-01 ou DNS-01).
|
|
2
2
|
|
|
3
|
+
Este módulo oferece:
|
|
4
|
+
- Instalação do cert-manager via Helm com verificação completa de readiness
|
|
5
|
+
- Múltiplos provedores DNS (Cloudflare, Route53, DigitalOcean, Azure)
|
|
6
|
+
- Verificação REAL do webhook (testa com dry-run antes de aplicar)
|
|
7
|
+
- Modo não-interativo para automação completa
|
|
8
|
+
- Diagnóstico detalhado de problemas
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import subprocess
|
|
15
|
+
import time
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from enum import Enum
|
|
3
18
|
from pathlib import Path
|
|
19
|
+
from typing import Callable, Optional, List
|
|
4
20
|
|
|
5
21
|
import typer
|
|
6
22
|
|
|
@@ -8,9 +24,10 @@ from raijin_server.utils import (
|
|
|
8
24
|
ExecutionContext,
|
|
9
25
|
ensure_tool,
|
|
10
26
|
helm_upgrade_install,
|
|
11
|
-
kubectl_apply,
|
|
12
27
|
require_root,
|
|
28
|
+
run_cmd,
|
|
13
29
|
write_file,
|
|
30
|
+
logger,
|
|
14
31
|
)
|
|
15
32
|
|
|
16
33
|
CHART_REPO = "https://charts.jetstack.io"
|
|
@@ -18,73 +35,233 @@ CHART_NAME = "cert-manager"
|
|
|
18
35
|
NAMESPACE = "cert-manager"
|
|
19
36
|
MANIFEST_PATH = Path("/tmp/raijin-cert-manager-issuer.yaml")
|
|
20
37
|
|
|
38
|
+
# Timeouts mais generosos para ambientes lentos
|
|
39
|
+
WEBHOOK_READY_TIMEOUT = 600 # 10 minutos
|
|
40
|
+
POD_READY_TIMEOUT = 300 # 5 minutos
|
|
41
|
+
CRD_READY_TIMEOUT = 180 # 3 minutos
|
|
21
42
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
43
|
+
|
|
44
|
+
class DNSProvider(str, Enum):
|
|
45
|
+
"""Provedores DNS suportados para challenge DNS-01."""
|
|
46
|
+
CLOUDFLARE = "cloudflare"
|
|
47
|
+
ROUTE53 = "route53"
|
|
48
|
+
DIGITALOCEAN = "digitalocean"
|
|
49
|
+
AZURE = "azure"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ChallengeType(str, Enum):
|
|
53
|
+
"""Tipos de challenge ACME suportados."""
|
|
54
|
+
HTTP01 = "http01"
|
|
55
|
+
DNS01 = "dns01"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class IssuerConfig:
|
|
60
|
+
"""Configuração para criação de ClusterIssuer."""
|
|
61
|
+
name: str
|
|
62
|
+
email: str
|
|
63
|
+
staging: bool = False
|
|
64
|
+
challenge_type: ChallengeType = ChallengeType.HTTP01
|
|
65
|
+
ingress_class: str = "traefik"
|
|
66
|
+
dns_provider: Optional[DNSProvider] = None
|
|
67
|
+
secret_name: str = "dns-api-credentials"
|
|
68
|
+
region: str = "us-east-1"
|
|
69
|
+
hosted_zone_id: str = ""
|
|
70
|
+
resource_group: str = ""
|
|
71
|
+
subscription_id: str = ""
|
|
72
|
+
tenant_id: str = ""
|
|
73
|
+
client_id: str = ""
|
|
74
|
+
# Credentials (transient, not persisted)
|
|
75
|
+
_credentials: dict = field(default_factory=dict)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _get_acme_server(staging: bool) -> str:
|
|
79
|
+
"""Retorna URL do servidor ACME."""
|
|
80
|
+
if staging:
|
|
81
|
+
return "https://acme-staging-v02.api.letsencrypt.org/directory"
|
|
82
|
+
return "https://acme-v02.api.letsencrypt.org/directory"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# =============================================================================
|
|
86
|
+
# Builders de Manifests YAML
|
|
87
|
+
# =============================================================================
|
|
88
|
+
|
|
89
|
+
def _build_http01_issuer(config: IssuerConfig) -> str:
|
|
28
90
|
return f"""apiVersion: cert-manager.io/v1
|
|
29
91
|
kind: ClusterIssuer
|
|
30
92
|
metadata:
|
|
31
|
-
name: {name}
|
|
93
|
+
name: {config.name}
|
|
32
94
|
spec:
|
|
33
95
|
acme:
|
|
34
|
-
email: {email}
|
|
35
|
-
server: {
|
|
96
|
+
email: {config.email}
|
|
97
|
+
server: {_get_acme_server(config.staging)}
|
|
36
98
|
privateKeySecretRef:
|
|
37
|
-
name: {name}
|
|
99
|
+
name: {config.name}-account-key
|
|
38
100
|
solvers:
|
|
39
101
|
- http01:
|
|
40
102
|
ingress:
|
|
41
|
-
class: {ingress_class}
|
|
103
|
+
class: {config.ingress_class}
|
|
42
104
|
"""
|
|
43
105
|
|
|
44
106
|
|
|
45
|
-
def
|
|
46
|
-
server = (
|
|
47
|
-
"https://acme-staging-v02.api.letsencrypt.org/directory"
|
|
48
|
-
if staging
|
|
49
|
-
else "https://acme-v02.api.letsencrypt.org/directory"
|
|
50
|
-
)
|
|
107
|
+
def _build_cloudflare_issuer(config: IssuerConfig) -> str:
|
|
51
108
|
return f"""apiVersion: cert-manager.io/v1
|
|
52
109
|
kind: ClusterIssuer
|
|
53
110
|
metadata:
|
|
54
|
-
name: {name}
|
|
111
|
+
name: {config.name}
|
|
55
112
|
spec:
|
|
56
113
|
acme:
|
|
57
|
-
email: {email}
|
|
58
|
-
server: {
|
|
114
|
+
email: {config.email}
|
|
115
|
+
server: {_get_acme_server(config.staging)}
|
|
59
116
|
privateKeySecretRef:
|
|
60
|
-
name: {name}
|
|
117
|
+
name: {config.name}-account-key
|
|
61
118
|
solvers:
|
|
62
119
|
- dns01:
|
|
63
120
|
cloudflare:
|
|
64
121
|
apiTokenSecretRef:
|
|
65
|
-
name: {secret_name}
|
|
122
|
+
name: {config.secret_name}
|
|
66
123
|
key: api-token
|
|
67
124
|
"""
|
|
68
125
|
|
|
69
126
|
|
|
70
|
-
def
|
|
71
|
-
|
|
127
|
+
def _build_route53_issuer(config: IssuerConfig) -> str:
|
|
128
|
+
hosted_zone = ""
|
|
129
|
+
if config.hosted_zone_id:
|
|
130
|
+
hosted_zone = f"\n hostedZoneID: {config.hosted_zone_id}"
|
|
131
|
+
|
|
132
|
+
return f"""apiVersion: cert-manager.io/v1
|
|
133
|
+
kind: ClusterIssuer
|
|
134
|
+
metadata:
|
|
135
|
+
name: {config.name}
|
|
136
|
+
spec:
|
|
137
|
+
acme:
|
|
138
|
+
email: {config.email}
|
|
139
|
+
server: {_get_acme_server(config.staging)}
|
|
140
|
+
privateKeySecretRef:
|
|
141
|
+
name: {config.name}-account-key
|
|
142
|
+
solvers:
|
|
143
|
+
- dns01:
|
|
144
|
+
route53:
|
|
145
|
+
region: {config.region}{hosted_zone}
|
|
146
|
+
accessKeyIDSecretRef:
|
|
147
|
+
name: {config.secret_name}
|
|
148
|
+
key: access-key-id
|
|
149
|
+
secretAccessKeySecretRef:
|
|
150
|
+
name: {config.secret_name}
|
|
151
|
+
key: secret-access-key
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _build_digitalocean_issuer(config: IssuerConfig) -> str:
|
|
156
|
+
return f"""apiVersion: cert-manager.io/v1
|
|
157
|
+
kind: ClusterIssuer
|
|
158
|
+
metadata:
|
|
159
|
+
name: {config.name}
|
|
160
|
+
spec:
|
|
161
|
+
acme:
|
|
162
|
+
email: {config.email}
|
|
163
|
+
server: {_get_acme_server(config.staging)}
|
|
164
|
+
privateKeySecretRef:
|
|
165
|
+
name: {config.name}-account-key
|
|
166
|
+
solvers:
|
|
167
|
+
- dns01:
|
|
168
|
+
digitalocean:
|
|
169
|
+
tokenSecretRef:
|
|
170
|
+
name: {config.secret_name}
|
|
171
|
+
key: access-token
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _build_azure_issuer(config: IssuerConfig) -> str:
|
|
176
|
+
return f"""apiVersion: cert-manager.io/v1
|
|
177
|
+
kind: ClusterIssuer
|
|
178
|
+
metadata:
|
|
179
|
+
name: {config.name}
|
|
180
|
+
spec:
|
|
181
|
+
acme:
|
|
182
|
+
email: {config.email}
|
|
183
|
+
server: {_get_acme_server(config.staging)}
|
|
184
|
+
privateKeySecretRef:
|
|
185
|
+
name: {config.name}-account-key
|
|
186
|
+
solvers:
|
|
187
|
+
- dns01:
|
|
188
|
+
azureDNS:
|
|
189
|
+
subscriptionID: {config.subscription_id}
|
|
190
|
+
resourceGroupName: {config.resource_group}
|
|
191
|
+
hostedZoneName: {config.name}
|
|
192
|
+
environment: AzurePublicCloud
|
|
193
|
+
clientID: {config.client_id}
|
|
194
|
+
tenantID: {config.tenant_id}
|
|
195
|
+
clientSecretSecretRef:
|
|
196
|
+
name: {config.secret_name}
|
|
197
|
+
key: client-secret
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _build_secret(config: IssuerConfig) -> Optional[str]:
|
|
202
|
+
"""Gera Secret baseado no provider DNS."""
|
|
203
|
+
if config.challenge_type != ChallengeType.DNS01:
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
creds = config._credentials
|
|
207
|
+
|
|
208
|
+
if config.dns_provider == DNSProvider.CLOUDFLARE:
|
|
209
|
+
return f"""apiVersion: v1
|
|
72
210
|
kind: Secret
|
|
73
211
|
metadata:
|
|
74
|
-
name: {secret_name}
|
|
212
|
+
name: {config.secret_name}
|
|
75
213
|
namespace: {NAMESPACE}
|
|
76
214
|
type: Opaque
|
|
77
215
|
stringData:
|
|
78
|
-
api-token: {api_token}
|
|
216
|
+
api-token: {creds.get('api_token', '')}
|
|
79
217
|
"""
|
|
218
|
+
|
|
219
|
+
elif config.dns_provider == DNSProvider.ROUTE53:
|
|
220
|
+
return f"""apiVersion: v1
|
|
221
|
+
kind: Secret
|
|
222
|
+
metadata:
|
|
223
|
+
name: {config.secret_name}
|
|
224
|
+
namespace: {NAMESPACE}
|
|
225
|
+
type: Opaque
|
|
226
|
+
stringData:
|
|
227
|
+
access-key-id: {creds.get('access_key', '')}
|
|
228
|
+
secret-access-key: {creds.get('secret_key', '')}
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
elif config.dns_provider == DNSProvider.DIGITALOCEAN:
|
|
232
|
+
return f"""apiVersion: v1
|
|
233
|
+
kind: Secret
|
|
234
|
+
metadata:
|
|
235
|
+
name: {config.secret_name}
|
|
236
|
+
namespace: {NAMESPACE}
|
|
237
|
+
type: Opaque
|
|
238
|
+
stringData:
|
|
239
|
+
access-token: {creds.get('token', '')}
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
elif config.dns_provider == DNSProvider.AZURE:
|
|
243
|
+
return f"""apiVersion: v1
|
|
244
|
+
kind: Secret
|
|
245
|
+
metadata:
|
|
246
|
+
name: {config.secret_name}
|
|
247
|
+
namespace: {NAMESPACE}
|
|
248
|
+
type: Opaque
|
|
249
|
+
stringData:
|
|
250
|
+
client-secret: {creds.get('client_secret', '')}
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
return None
|
|
254
|
+
|
|
80
255
|
|
|
256
|
+
# =============================================================================
|
|
257
|
+
# Verificações de Estado
|
|
258
|
+
# =============================================================================
|
|
81
259
|
|
|
82
260
|
def _check_cluster_available(ctx: ExecutionContext) -> bool:
|
|
83
|
-
"""Verifica se o cluster Kubernetes
|
|
261
|
+
"""Verifica se o cluster Kubernetes está acessível."""
|
|
84
262
|
if ctx.dry_run:
|
|
85
263
|
return True
|
|
86
264
|
try:
|
|
87
|
-
import subprocess
|
|
88
265
|
result = subprocess.run(
|
|
89
266
|
["kubectl", "cluster-info"],
|
|
90
267
|
capture_output=True,
|
|
@@ -95,62 +272,764 @@ def _check_cluster_available(ctx: ExecutionContext) -> bool:
|
|
|
95
272
|
return False
|
|
96
273
|
|
|
97
274
|
|
|
275
|
+
def _check_crds_installed() -> bool:
|
|
276
|
+
"""Verifica se os CRDs do cert-manager estão instalados."""
|
|
277
|
+
required_crds = [
|
|
278
|
+
"certificates.cert-manager.io",
|
|
279
|
+
"clusterissuers.cert-manager.io",
|
|
280
|
+
"issuers.cert-manager.io",
|
|
281
|
+
]
|
|
282
|
+
try:
|
|
283
|
+
for crd in required_crds:
|
|
284
|
+
result = subprocess.run(
|
|
285
|
+
["kubectl", "get", "crd", crd],
|
|
286
|
+
capture_output=True,
|
|
287
|
+
timeout=10,
|
|
288
|
+
)
|
|
289
|
+
if result.returncode != 0:
|
|
290
|
+
return False
|
|
291
|
+
return True
|
|
292
|
+
except Exception:
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _check_deployment_ready(name: str, namespace: str) -> bool:
|
|
297
|
+
"""Verifica se um deployment está com todas as replicas ready."""
|
|
298
|
+
try:
|
|
299
|
+
result = subprocess.run(
|
|
300
|
+
[
|
|
301
|
+
"kubectl", "get", "deployment", name,
|
|
302
|
+
"-n", namespace,
|
|
303
|
+
"-o", "jsonpath={.status.readyReplicas}/{.spec.replicas}"
|
|
304
|
+
],
|
|
305
|
+
capture_output=True,
|
|
306
|
+
text=True,
|
|
307
|
+
timeout=10,
|
|
308
|
+
)
|
|
309
|
+
if result.returncode != 0:
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
parts = result.stdout.strip().split("/")
|
|
313
|
+
if len(parts) != 2:
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
ready = int(parts[0]) if parts[0] else 0
|
|
317
|
+
desired = int(parts[1]) if parts[1] else 1
|
|
318
|
+
return ready >= desired
|
|
319
|
+
except Exception:
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _check_webhook_endpoint_ready() -> bool:
|
|
324
|
+
"""Verifica se o endpoint do webhook tem IPs atribuídos."""
|
|
325
|
+
try:
|
|
326
|
+
result = subprocess.run(
|
|
327
|
+
[
|
|
328
|
+
"kubectl", "get", "endpoints", "cert-manager-webhook",
|
|
329
|
+
"-n", NAMESPACE,
|
|
330
|
+
"-o", "jsonpath={.subsets[0].addresses[0].ip}"
|
|
331
|
+
],
|
|
332
|
+
capture_output=True,
|
|
333
|
+
text=True,
|
|
334
|
+
timeout=10,
|
|
335
|
+
)
|
|
336
|
+
return bool(result.stdout.strip())
|
|
337
|
+
except Exception:
|
|
338
|
+
return False
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _test_webhook_connectivity() -> bool:
|
|
342
|
+
"""Testa se o webhook está realmente respondendo usando dry-run."""
|
|
343
|
+
try:
|
|
344
|
+
# Cria um ClusterIssuer de teste para validar o webhook
|
|
345
|
+
test_manifest = """apiVersion: cert-manager.io/v1
|
|
346
|
+
kind: ClusterIssuer
|
|
347
|
+
metadata:
|
|
348
|
+
name: test-webhook-connectivity
|
|
349
|
+
spec:
|
|
350
|
+
acme:
|
|
351
|
+
email: test@test.com
|
|
352
|
+
server: https://acme-staging-v02.api.letsencrypt.org/directory
|
|
353
|
+
privateKeySecretRef:
|
|
354
|
+
name: test-key
|
|
355
|
+
solvers:
|
|
356
|
+
- http01:
|
|
357
|
+
ingress:
|
|
358
|
+
class: nginx
|
|
359
|
+
"""
|
|
360
|
+
# Usa --dry-run=server para testar se o webhook responde
|
|
361
|
+
result = subprocess.run(
|
|
362
|
+
["kubectl", "apply", "--dry-run=server", "-f", "-"],
|
|
363
|
+
input=test_manifest,
|
|
364
|
+
capture_output=True,
|
|
365
|
+
text=True,
|
|
366
|
+
timeout=30,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Se passou no dry-run, o webhook está funcionando
|
|
370
|
+
if result.returncode == 0:
|
|
371
|
+
return True
|
|
372
|
+
|
|
373
|
+
# Verifica se o erro é de conexão com webhook
|
|
374
|
+
stderr = result.stderr.lower()
|
|
375
|
+
if "connection refused" in stderr or "no endpoints" in stderr:
|
|
376
|
+
return False
|
|
377
|
+
|
|
378
|
+
# Outros erros (validação, etc) significam que o webhook respondeu
|
|
379
|
+
return True
|
|
380
|
+
except subprocess.TimeoutExpired:
|
|
381
|
+
return False
|
|
382
|
+
except Exception:
|
|
383
|
+
return False
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _wait_for_webhook_ready(ctx: ExecutionContext, timeout: int = WEBHOOK_READY_TIMEOUT) -> bool:
|
|
387
|
+
"""Aguarda até que o webhook do cert-manager esteja totalmente operacional."""
|
|
388
|
+
if ctx.dry_run:
|
|
389
|
+
typer.echo("[dry-run] Aguardando webhook...")
|
|
390
|
+
return True
|
|
391
|
+
|
|
392
|
+
typer.secho("\n⏳ Aguardando cert-manager ficar pronto...", fg=typer.colors.CYAN)
|
|
393
|
+
|
|
394
|
+
start_time = time.time()
|
|
395
|
+
interval = 15
|
|
396
|
+
|
|
397
|
+
stages = [
|
|
398
|
+
("CRDs instalados", _check_crds_installed),
|
|
399
|
+
("Deployment cert-manager ready", lambda: _check_deployment_ready("cert-manager", NAMESPACE)),
|
|
400
|
+
("Deployment cert-manager-webhook ready", lambda: _check_deployment_ready("cert-manager-webhook", NAMESPACE)),
|
|
401
|
+
("Deployment cert-manager-cainjector ready", lambda: _check_deployment_ready("cert-manager-cainjector", NAMESPACE)),
|
|
402
|
+
("Endpoint webhook disponível", _check_webhook_endpoint_ready),
|
|
403
|
+
("Webhook respondendo (teste de conectividade)", _test_webhook_connectivity),
|
|
404
|
+
]
|
|
405
|
+
|
|
406
|
+
for stage_name, check_fn in stages:
|
|
407
|
+
typer.echo(f"\n → Verificando: {stage_name}")
|
|
408
|
+
|
|
409
|
+
while True:
|
|
410
|
+
elapsed = int(time.time() - start_time)
|
|
411
|
+
remaining = timeout - elapsed
|
|
412
|
+
|
|
413
|
+
if remaining <= 0:
|
|
414
|
+
typer.secho(f" ✗ TIMEOUT após {timeout}s", fg=typer.colors.RED)
|
|
415
|
+
return False
|
|
416
|
+
|
|
417
|
+
try:
|
|
418
|
+
if check_fn():
|
|
419
|
+
typer.secho(f" ✓ {stage_name}", fg=typer.colors.GREEN)
|
|
420
|
+
break
|
|
421
|
+
except Exception as e:
|
|
422
|
+
logger.debug(f"Verificação falhou: {e}")
|
|
423
|
+
|
|
424
|
+
typer.echo(f" ... aguardando ({elapsed}s / {timeout}s)")
|
|
425
|
+
time.sleep(interval)
|
|
426
|
+
|
|
427
|
+
typer.secho("\n✓ Cert-manager totalmente operacional!", fg=typer.colors.GREEN, bold=True)
|
|
428
|
+
return True
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
# =============================================================================
|
|
432
|
+
# Instalação e Configuração
|
|
433
|
+
# =============================================================================
|
|
434
|
+
|
|
435
|
+
def _install_cert_manager_helm(ctx: ExecutionContext) -> bool:
|
|
436
|
+
"""Instala cert-manager via Helm."""
|
|
437
|
+
typer.secho("\n📦 Instalando cert-manager via Helm...", fg=typer.colors.CYAN, bold=True)
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
# O helm_upgrade_install agora limpa releases pendentes automaticamente
|
|
441
|
+
helm_upgrade_install(
|
|
442
|
+
release="cert-manager",
|
|
443
|
+
chart=CHART_NAME,
|
|
444
|
+
namespace=NAMESPACE,
|
|
445
|
+
ctx=ctx,
|
|
446
|
+
repo="jetstack",
|
|
447
|
+
repo_url=CHART_REPO,
|
|
448
|
+
create_namespace=True,
|
|
449
|
+
extra_args=[
|
|
450
|
+
"--set", "installCRDs=true",
|
|
451
|
+
"--set", "webhook.timeoutSeconds=30",
|
|
452
|
+
"--set", "startupapicheck.timeout=5m",
|
|
453
|
+
"--set", "startupapicheck.enabled=true",
|
|
454
|
+
# Aumenta recursos para ambientes mais lentos
|
|
455
|
+
"--set", "webhook.replicaCount=1",
|
|
456
|
+
"--set", "cainjector.replicaCount=1",
|
|
457
|
+
"--wait", # Espera o Helm considerar o release deployed
|
|
458
|
+
"--timeout", "10m",
|
|
459
|
+
],
|
|
460
|
+
)
|
|
461
|
+
return True
|
|
462
|
+
except Exception as e:
|
|
463
|
+
typer.secho(f"✗ Erro na instalação do Helm: {e}", fg=typer.colors.RED)
|
|
464
|
+
ctx.errors.append(f"cert-manager: falha na instalação Helm - {e}")
|
|
465
|
+
return False
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _apply_manifest_with_retry(
|
|
469
|
+
manifest: str,
|
|
470
|
+
ctx: ExecutionContext,
|
|
471
|
+
description: str = "manifest",
|
|
472
|
+
max_retries: int = 10,
|
|
473
|
+
retry_interval: int = 30,
|
|
474
|
+
) -> bool:
|
|
475
|
+
"""Aplica um manifest com retry inteligente."""
|
|
476
|
+
if ctx.dry_run:
|
|
477
|
+
typer.echo(f"[dry-run] Aplicando {description}")
|
|
478
|
+
return True
|
|
479
|
+
|
|
480
|
+
manifest_path = MANIFEST_PATH
|
|
481
|
+
manifest_path.write_text(manifest)
|
|
482
|
+
|
|
483
|
+
for attempt in range(1, max_retries + 1):
|
|
484
|
+
typer.echo(f"\n Aplicando {description} (tentativa {attempt}/{max_retries})...")
|
|
485
|
+
|
|
486
|
+
try:
|
|
487
|
+
result = subprocess.run(
|
|
488
|
+
["kubectl", "apply", "-f", str(manifest_path)],
|
|
489
|
+
capture_output=True,
|
|
490
|
+
text=True,
|
|
491
|
+
timeout=60,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
if result.returncode == 0:
|
|
495
|
+
typer.secho(f" ✓ {description} aplicado com sucesso!", fg=typer.colors.GREEN)
|
|
496
|
+
return True
|
|
497
|
+
|
|
498
|
+
stderr = result.stderr.lower()
|
|
499
|
+
|
|
500
|
+
# Verifica se é erro de webhook
|
|
501
|
+
if "connection refused" in stderr or "failed to call webhook" in stderr:
|
|
502
|
+
typer.secho(
|
|
503
|
+
f" ⚠ Webhook ainda não está pronto...",
|
|
504
|
+
fg=typer.colors.YELLOW,
|
|
505
|
+
)
|
|
506
|
+
if attempt < max_retries:
|
|
507
|
+
typer.echo(f" Aguardando {retry_interval}s antes de tentar novamente...")
|
|
508
|
+
time.sleep(retry_interval)
|
|
509
|
+
continue
|
|
510
|
+
|
|
511
|
+
# Outro tipo de erro
|
|
512
|
+
typer.secho(f" ✗ Erro: {result.stderr[:200]}", fg=typer.colors.RED)
|
|
513
|
+
if attempt < max_retries:
|
|
514
|
+
time.sleep(retry_interval)
|
|
515
|
+
|
|
516
|
+
except subprocess.TimeoutExpired:
|
|
517
|
+
typer.secho(" ⚠ Timeout ao aplicar manifest", fg=typer.colors.YELLOW)
|
|
518
|
+
if attempt < max_retries:
|
|
519
|
+
time.sleep(retry_interval)
|
|
520
|
+
except Exception as e:
|
|
521
|
+
typer.secho(f" ✗ Erro inesperado: {e}", fg=typer.colors.RED)
|
|
522
|
+
if attempt < max_retries:
|
|
523
|
+
time.sleep(retry_interval)
|
|
524
|
+
|
|
525
|
+
typer.secho(f"✗ Falha ao aplicar {description} após {max_retries} tentativas", fg=typer.colors.RED)
|
|
526
|
+
return False
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _build_issuer_manifests(config: IssuerConfig) -> str:
|
|
530
|
+
"""Constrói todos os manifests necessários para o issuer."""
|
|
531
|
+
manifests = []
|
|
532
|
+
|
|
533
|
+
# Secret (se necessário)
|
|
534
|
+
secret = _build_secret(config)
|
|
535
|
+
if secret:
|
|
536
|
+
manifests.append(secret)
|
|
537
|
+
|
|
538
|
+
# ClusterIssuer
|
|
539
|
+
if config.challenge_type == ChallengeType.HTTP01:
|
|
540
|
+
manifests.append(_build_http01_issuer(config))
|
|
541
|
+
elif config.dns_provider == DNSProvider.CLOUDFLARE:
|
|
542
|
+
manifests.append(_build_cloudflare_issuer(config))
|
|
543
|
+
elif config.dns_provider == DNSProvider.ROUTE53:
|
|
544
|
+
manifests.append(_build_route53_issuer(config))
|
|
545
|
+
elif config.dns_provider == DNSProvider.DIGITALOCEAN:
|
|
546
|
+
manifests.append(_build_digitalocean_issuer(config))
|
|
547
|
+
elif config.dns_provider == DNSProvider.AZURE:
|
|
548
|
+
manifests.append(_build_azure_issuer(config))
|
|
549
|
+
|
|
550
|
+
return "---\n".join(manifests)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
# =============================================================================
|
|
554
|
+
# Interface Interativa
|
|
555
|
+
# =============================================================================
|
|
556
|
+
|
|
557
|
+
def _collect_issuer_config_interactive() -> Optional[IssuerConfig]:
|
|
558
|
+
"""Coleta configuração do issuer interativamente."""
|
|
559
|
+
typer.secho("\n🔧 Configuração do ClusterIssuer", fg=typer.colors.CYAN, bold=True)
|
|
560
|
+
|
|
561
|
+
email = typer.prompt(
|
|
562
|
+
"Email para ACME (Let's Encrypt)",
|
|
563
|
+
default="admin@example.com",
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
if "@" not in email or "example.com" in email:
|
|
567
|
+
typer.secho(
|
|
568
|
+
"⚠ Use um email válido para receber notificações de expiração",
|
|
569
|
+
fg=typer.colors.YELLOW,
|
|
570
|
+
)
|
|
571
|
+
if not typer.confirm("Continuar mesmo assim?", default=False):
|
|
572
|
+
return None
|
|
573
|
+
|
|
574
|
+
solver = typer.prompt(
|
|
575
|
+
"Tipo de challenge (http01/dns01)",
|
|
576
|
+
default="http01",
|
|
577
|
+
).lower()
|
|
578
|
+
|
|
579
|
+
staging = typer.confirm(
|
|
580
|
+
"Usar servidor de staging? (para testes)",
|
|
581
|
+
default=True,
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
if staging:
|
|
585
|
+
typer.secho(
|
|
586
|
+
"ℹ Staging gera certificados de teste (não válidos para produção)",
|
|
587
|
+
fg=typer.colors.CYAN,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
config = IssuerConfig(name="", email=email, staging=staging)
|
|
591
|
+
|
|
592
|
+
if solver == "dns01":
|
|
593
|
+
config.challenge_type = ChallengeType.DNS01
|
|
594
|
+
|
|
595
|
+
typer.echo("\nProvedores DNS disponíveis:")
|
|
596
|
+
typer.echo(" 1. Cloudflare")
|
|
597
|
+
typer.echo(" 2. AWS Route53")
|
|
598
|
+
typer.echo(" 3. DigitalOcean")
|
|
599
|
+
typer.echo(" 4. Azure DNS")
|
|
600
|
+
|
|
601
|
+
choice = typer.prompt("Escolha (1-4)", default="1")
|
|
602
|
+
|
|
603
|
+
if choice == "1":
|
|
604
|
+
config.dns_provider = DNSProvider.CLOUDFLARE
|
|
605
|
+
config.name = typer.prompt("Nome do ClusterIssuer", default="letsencrypt-cloudflare")
|
|
606
|
+
config.secret_name = typer.prompt("Nome do Secret", default="cloudflare-api-token")
|
|
607
|
+
config._credentials["api_token"] = typer.prompt("API Token do Cloudflare", hide_input=True)
|
|
608
|
+
|
|
609
|
+
elif choice == "2":
|
|
610
|
+
config.dns_provider = DNSProvider.ROUTE53
|
|
611
|
+
config.name = typer.prompt("Nome do ClusterIssuer", default="letsencrypt-route53")
|
|
612
|
+
config.secret_name = typer.prompt("Nome do Secret", default="route53-credentials")
|
|
613
|
+
config.region = typer.prompt("AWS Region", default="us-east-1")
|
|
614
|
+
config.hosted_zone_id = typer.prompt("Hosted Zone ID (opcional)", default="")
|
|
615
|
+
config._credentials["access_key"] = typer.prompt("AWS Access Key ID", hide_input=True)
|
|
616
|
+
config._credentials["secret_key"] = typer.prompt("AWS Secret Access Key", hide_input=True)
|
|
617
|
+
|
|
618
|
+
elif choice == "3":
|
|
619
|
+
config.dns_provider = DNSProvider.DIGITALOCEAN
|
|
620
|
+
config.name = typer.prompt("Nome do ClusterIssuer", default="letsencrypt-digitalocean")
|
|
621
|
+
config.secret_name = typer.prompt("Nome do Secret", default="digitalocean-token")
|
|
622
|
+
config._credentials["token"] = typer.prompt("DigitalOcean API Token", hide_input=True)
|
|
623
|
+
|
|
624
|
+
elif choice == "4":
|
|
625
|
+
config.dns_provider = DNSProvider.AZURE
|
|
626
|
+
config.name = typer.prompt("Nome do ClusterIssuer", default="letsencrypt-azure")
|
|
627
|
+
config.secret_name = typer.prompt("Nome do Secret", default="azure-dns-credentials")
|
|
628
|
+
config.subscription_id = typer.prompt("Azure Subscription ID")
|
|
629
|
+
config.resource_group = typer.prompt("Resource Group")
|
|
630
|
+
config.tenant_id = typer.prompt("Tenant ID")
|
|
631
|
+
config.client_id = typer.prompt("Client ID (App Registration)")
|
|
632
|
+
config._credentials["client_secret"] = typer.prompt("Client Secret", hide_input=True)
|
|
633
|
+
else:
|
|
634
|
+
typer.secho("Opção inválida", fg=typer.colors.RED)
|
|
635
|
+
return None
|
|
636
|
+
else:
|
|
637
|
+
config.challenge_type = ChallengeType.HTTP01
|
|
638
|
+
config.name = typer.prompt("Nome do ClusterIssuer", default="letsencrypt-http")
|
|
639
|
+
config.ingress_class = typer.prompt("IngressClass", default="traefik")
|
|
640
|
+
|
|
641
|
+
return config
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _print_next_steps(config: IssuerConfig) -> None:
|
|
645
|
+
"""Exibe próximos passos após configuração."""
|
|
646
|
+
typer.secho("\n📚 Próximos Passos", fg=typer.colors.CYAN, bold=True)
|
|
647
|
+
typer.echo("─" * 50)
|
|
648
|
+
|
|
649
|
+
typer.echo(f"""
|
|
650
|
+
Para usar certificados TLS em seus Ingresses:
|
|
651
|
+
|
|
652
|
+
1. Adicione a annotation no Ingress:
|
|
653
|
+
annotations:
|
|
654
|
+
cert-manager.io/cluster-issuer: "{config.name}"
|
|
655
|
+
|
|
656
|
+
2. Configure o TLS no Ingress:
|
|
657
|
+
tls:
|
|
658
|
+
- hosts:
|
|
659
|
+
- seu-dominio.com
|
|
660
|
+
secretName: seu-dominio-tls
|
|
661
|
+
|
|
662
|
+
3. Verifique o status do certificado:
|
|
663
|
+
kubectl get certificate -A
|
|
664
|
+
kubectl describe certificate <nome> -n <namespace>
|
|
665
|
+
|
|
666
|
+
4. Para debug de problemas:
|
|
667
|
+
kubectl describe clusterissuer {config.name}
|
|
668
|
+
kubectl get challenges -A
|
|
669
|
+
kubectl get orders -A
|
|
670
|
+
""")
|
|
671
|
+
|
|
672
|
+
if config.staging:
|
|
673
|
+
typer.secho(
|
|
674
|
+
"⚠ Você está usando o servidor STAGING!\n"
|
|
675
|
+
" Os certificados NÃO são válidos para produção.\n"
|
|
676
|
+
" Após testar, recrie o issuer sem staging.",
|
|
677
|
+
fg=typer.colors.YELLOW,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
typer.echo("─" * 50)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
# =============================================================================
|
|
684
|
+
# Status e Diagnóstico
|
|
685
|
+
# =============================================================================
|
|
686
|
+
|
|
687
|
+
def _get_cert_manager_status(ctx: ExecutionContext) -> dict:
|
|
688
|
+
"""Obtém status detalhado do cert-manager."""
|
|
689
|
+
status = {
|
|
690
|
+
"installed": False,
|
|
691
|
+
"version": "",
|
|
692
|
+
"pods": [],
|
|
693
|
+
"webhook_ready": False,
|
|
694
|
+
"crds_installed": False,
|
|
695
|
+
"issuers": [],
|
|
696
|
+
"certificates": [],
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if ctx.dry_run:
|
|
700
|
+
return status
|
|
701
|
+
|
|
702
|
+
try:
|
|
703
|
+
# Verifica versão via Helm
|
|
704
|
+
result = subprocess.run(
|
|
705
|
+
["helm", "list", "-n", NAMESPACE, "-o", "json"],
|
|
706
|
+
capture_output=True,
|
|
707
|
+
text=True,
|
|
708
|
+
timeout=15,
|
|
709
|
+
)
|
|
710
|
+
if result.returncode == 0:
|
|
711
|
+
releases = json.loads(result.stdout)
|
|
712
|
+
for r in releases:
|
|
713
|
+
if r.get("name") == "cert-manager":
|
|
714
|
+
status["installed"] = True
|
|
715
|
+
status["version"] = r.get("app_version", "")
|
|
716
|
+
break
|
|
717
|
+
|
|
718
|
+
# Verifica pods
|
|
719
|
+
result = subprocess.run(
|
|
720
|
+
[
|
|
721
|
+
"kubectl", "get", "pods", "-n", NAMESPACE,
|
|
722
|
+
"-o", "jsonpath={range .items[*]}{.metadata.name}:{.status.phase}\\n{end}"
|
|
723
|
+
],
|
|
724
|
+
capture_output=True,
|
|
725
|
+
text=True,
|
|
726
|
+
timeout=10,
|
|
727
|
+
)
|
|
728
|
+
if result.returncode == 0:
|
|
729
|
+
for line in result.stdout.strip().split("\n"):
|
|
730
|
+
if ":" in line:
|
|
731
|
+
name, phase = line.split(":", 1)
|
|
732
|
+
status["pods"].append({"name": name, "phase": phase})
|
|
733
|
+
|
|
734
|
+
status["webhook_ready"] = _test_webhook_connectivity()
|
|
735
|
+
status["crds_installed"] = _check_crds_installed()
|
|
736
|
+
|
|
737
|
+
# Lista ClusterIssuers
|
|
738
|
+
result = subprocess.run(
|
|
739
|
+
["kubectl", "get", "clusterissuers", "-o", "jsonpath={.items[*].metadata.name}"],
|
|
740
|
+
capture_output=True,
|
|
741
|
+
text=True,
|
|
742
|
+
timeout=10,
|
|
743
|
+
)
|
|
744
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
745
|
+
status["issuers"] = result.stdout.strip().split()
|
|
746
|
+
|
|
747
|
+
# Lista Certificates
|
|
748
|
+
result = subprocess.run(
|
|
749
|
+
[
|
|
750
|
+
"kubectl", "get", "certificates", "-A",
|
|
751
|
+
"-o", "jsonpath={range .items[*]}{.metadata.namespace}/{.metadata.name}:{.status.conditions[0].status}\\n{end}"
|
|
752
|
+
],
|
|
753
|
+
capture_output=True,
|
|
754
|
+
text=True,
|
|
755
|
+
timeout=10,
|
|
756
|
+
)
|
|
757
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
758
|
+
for line in result.stdout.strip().split("\n"):
|
|
759
|
+
if ":" in line:
|
|
760
|
+
name, ready = line.split(":", 1)
|
|
761
|
+
status["certificates"].append({"name": name, "ready": ready})
|
|
762
|
+
|
|
763
|
+
except Exception as e:
|
|
764
|
+
logger.error(f"Erro ao obter status do cert-manager: {e}")
|
|
765
|
+
|
|
766
|
+
return status
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def _print_status(status: dict) -> None:
|
|
770
|
+
"""Exibe status formatado do cert-manager."""
|
|
771
|
+
typer.secho("\n📊 Status do Cert-Manager", fg=typer.colors.CYAN, bold=True)
|
|
772
|
+
typer.echo("─" * 50)
|
|
773
|
+
|
|
774
|
+
if status["installed"]:
|
|
775
|
+
typer.secho(f" ✓ Instalado: versão {status['version']}", fg=typer.colors.GREEN)
|
|
776
|
+
else:
|
|
777
|
+
typer.secho(" ✗ Não instalado", fg=typer.colors.RED)
|
|
778
|
+
return
|
|
779
|
+
|
|
780
|
+
typer.echo("\n Pods:")
|
|
781
|
+
for pod in status["pods"]:
|
|
782
|
+
color = typer.colors.GREEN if pod["phase"] == "Running" else typer.colors.YELLOW
|
|
783
|
+
typer.secho(f" • {pod['name']}: {pod['phase']}", fg=color)
|
|
784
|
+
|
|
785
|
+
if status["webhook_ready"]:
|
|
786
|
+
typer.secho(" ✓ Webhook: Operacional", fg=typer.colors.GREEN)
|
|
787
|
+
else:
|
|
788
|
+
typer.secho(" ✗ Webhook: Não está respondendo", fg=typer.colors.RED)
|
|
789
|
+
|
|
790
|
+
if status["crds_installed"]:
|
|
791
|
+
typer.secho(" ✓ CRDs: Instalados", fg=typer.colors.GREEN)
|
|
792
|
+
else:
|
|
793
|
+
typer.secho(" ✗ CRDs: Não encontrados", fg=typer.colors.RED)
|
|
794
|
+
|
|
795
|
+
if status["issuers"]:
|
|
796
|
+
typer.echo("\n ClusterIssuers:")
|
|
797
|
+
for issuer in status["issuers"]:
|
|
798
|
+
typer.echo(f" • {issuer}")
|
|
799
|
+
else:
|
|
800
|
+
typer.echo("\n ClusterIssuers: Nenhum configurado")
|
|
801
|
+
|
|
802
|
+
if status["certificates"]:
|
|
803
|
+
typer.echo("\n Certificates:")
|
|
804
|
+
for cert in status["certificates"]:
|
|
805
|
+
color = typer.colors.GREEN if cert["ready"] == "True" else typer.colors.YELLOW
|
|
806
|
+
typer.secho(f" • {cert['name']}: {cert['ready']}", fg=color)
|
|
807
|
+
|
|
808
|
+
typer.echo("─" * 50)
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _diagnose_problems(ctx: ExecutionContext) -> None:
|
|
812
|
+
"""Diagnóstico detalhado de problemas comuns."""
|
|
813
|
+
typer.secho("\n🔍 Diagnóstico de Problemas", fg=typer.colors.YELLOW, bold=True)
|
|
814
|
+
|
|
815
|
+
if ctx.dry_run:
|
|
816
|
+
typer.echo("[dry-run] Diagnóstico ignorado")
|
|
817
|
+
return
|
|
818
|
+
|
|
819
|
+
problems = []
|
|
820
|
+
|
|
821
|
+
# Verifica conectividade do cluster
|
|
822
|
+
if not _check_cluster_available(ctx):
|
|
823
|
+
problems.append("Cluster Kubernetes não acessível")
|
|
824
|
+
|
|
825
|
+
# Verifica namespace
|
|
826
|
+
result = subprocess.run(
|
|
827
|
+
["kubectl", "get", "namespace", NAMESPACE],
|
|
828
|
+
capture_output=True,
|
|
829
|
+
timeout=10,
|
|
830
|
+
)
|
|
831
|
+
if result.returncode != 0:
|
|
832
|
+
problems.append(f"Namespace '{NAMESPACE}' não existe")
|
|
833
|
+
|
|
834
|
+
# Verifica eventos recentes com erros
|
|
835
|
+
typer.echo("\n Eventos recentes:")
|
|
836
|
+
try:
|
|
837
|
+
result = subprocess.run(
|
|
838
|
+
[
|
|
839
|
+
"kubectl", "get", "events", "-n", NAMESPACE,
|
|
840
|
+
"--sort-by=.lastTimestamp",
|
|
841
|
+
"--field-selector=type!=Normal",
|
|
842
|
+
],
|
|
843
|
+
capture_output=True,
|
|
844
|
+
text=True,
|
|
845
|
+
timeout=15,
|
|
846
|
+
)
|
|
847
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
848
|
+
for line in result.stdout.strip().split("\n")[:10]:
|
|
849
|
+
typer.echo(f" {line[:120]}")
|
|
850
|
+
else:
|
|
851
|
+
typer.echo(" Nenhum evento de erro recente")
|
|
852
|
+
except Exception:
|
|
853
|
+
pass
|
|
854
|
+
|
|
855
|
+
# Verifica logs do webhook
|
|
856
|
+
typer.echo("\n Logs recentes do webhook:")
|
|
857
|
+
try:
|
|
858
|
+
result = subprocess.run(
|
|
859
|
+
[
|
|
860
|
+
"kubectl", "logs", "-n", NAMESPACE,
|
|
861
|
+
"-l", "app.kubernetes.io/component=webhook",
|
|
862
|
+
"--tail=15"
|
|
863
|
+
],
|
|
864
|
+
capture_output=True,
|
|
865
|
+
text=True,
|
|
866
|
+
timeout=15,
|
|
867
|
+
)
|
|
868
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
869
|
+
for line in result.stdout.strip().split("\n")[-10:]:
|
|
870
|
+
typer.echo(f" {line[:120]}")
|
|
871
|
+
else:
|
|
872
|
+
typer.echo(" Nenhum log disponível")
|
|
873
|
+
except Exception:
|
|
874
|
+
pass
|
|
875
|
+
|
|
876
|
+
# Verifica describe dos deployments
|
|
877
|
+
typer.echo("\n Status dos Deployments:")
|
|
878
|
+
for deploy in ["cert-manager", "cert-manager-webhook", "cert-manager-cainjector"]:
|
|
879
|
+
try:
|
|
880
|
+
result = subprocess.run(
|
|
881
|
+
[
|
|
882
|
+
"kubectl", "get", "deployment", deploy, "-n", NAMESPACE,
|
|
883
|
+
"-o", "jsonpath={.status.conditions[?(@.type=='Available')].status}"
|
|
884
|
+
],
|
|
885
|
+
capture_output=True,
|
|
886
|
+
text=True,
|
|
887
|
+
timeout=10,
|
|
888
|
+
)
|
|
889
|
+
status = result.stdout.strip() if result.returncode == 0 else "Unknown"
|
|
890
|
+
color = typer.colors.GREEN if status == "True" else typer.colors.RED
|
|
891
|
+
typer.secho(f" {deploy}: Available={status}", fg=color)
|
|
892
|
+
except Exception:
|
|
893
|
+
typer.secho(f" {deploy}: Erro ao verificar", fg=typer.colors.RED)
|
|
894
|
+
|
|
895
|
+
if problems:
|
|
896
|
+
typer.secho("\n Problemas detectados:", fg=typer.colors.RED)
|
|
897
|
+
for p in problems:
|
|
898
|
+
typer.echo(f" ✗ {p}")
|
|
899
|
+
else:
|
|
900
|
+
typer.secho("\n Nenhum problema óbvio detectado", fg=typer.colors.GREEN)
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
# =============================================================================
|
|
904
|
+
# Entry Points
|
|
905
|
+
# =============================================================================
|
|
906
|
+
|
|
98
907
|
def run(ctx: ExecutionContext) -> None:
|
|
908
|
+
"""Execução principal do módulo cert-manager."""
|
|
99
909
|
require_root(ctx)
|
|
100
910
|
ensure_tool("helm", ctx, install_hint="Instale helm ou use --dry-run para simular.")
|
|
101
911
|
ensure_tool("kubectl", ctx, install_hint="Instale kubectl ou use --dry-run para simular.")
|
|
102
912
|
|
|
103
|
-
# Verifica
|
|
913
|
+
# Verifica cluster
|
|
104
914
|
if not _check_cluster_available(ctx):
|
|
105
915
|
typer.secho(
|
|
106
|
-
"✗ Cluster Kubernetes
|
|
916
|
+
"✗ Cluster Kubernetes não acessível. Execute 'kubernetes' primeiro.",
|
|
107
917
|
fg=typer.colors.RED,
|
|
108
918
|
)
|
|
109
|
-
|
|
110
|
-
" Verifique: kubectl cluster-info",
|
|
111
|
-
fg=typer.colors.YELLOW,
|
|
112
|
-
)
|
|
113
|
-
ctx.errors.append("cert-manager: cluster nao acessivel")
|
|
919
|
+
ctx.errors.append("cert-manager: cluster não acessível")
|
|
114
920
|
raise typer.Exit(code=1)
|
|
115
921
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
922
|
+
# Mostra status atual
|
|
923
|
+
status = _get_cert_manager_status(ctx)
|
|
924
|
+
_print_status(status)
|
|
925
|
+
|
|
926
|
+
# Verifica se já está instalado e funcionando
|
|
927
|
+
if status["installed"] and status["webhook_ready"]:
|
|
928
|
+
typer.echo("\nCert-manager já está instalado e operacional.")
|
|
929
|
+
if not typer.confirm("Deseja adicionar um novo ClusterIssuer?", default=True):
|
|
930
|
+
return
|
|
931
|
+
else:
|
|
932
|
+
# Instala cert-manager
|
|
933
|
+
if not _install_cert_manager_helm(ctx):
|
|
934
|
+
_diagnose_problems(ctx)
|
|
935
|
+
raise typer.Exit(code=1)
|
|
936
|
+
|
|
937
|
+
# Aguarda ficar totalmente operacional
|
|
938
|
+
if not ctx.dry_run:
|
|
939
|
+
if not _wait_for_webhook_ready(ctx):
|
|
940
|
+
typer.secho(
|
|
941
|
+
"\n✗ Cert-manager não ficou pronto no tempo esperado.",
|
|
942
|
+
fg=typer.colors.RED,
|
|
943
|
+
)
|
|
944
|
+
_diagnose_problems(ctx)
|
|
945
|
+
raise typer.Exit(code=1)
|
|
946
|
+
|
|
947
|
+
# Configura ClusterIssuer
|
|
948
|
+
config = _collect_issuer_config_interactive()
|
|
949
|
+
if config is None:
|
|
950
|
+
typer.secho("Configuração cancelada", fg=typer.colors.YELLOW)
|
|
951
|
+
return
|
|
952
|
+
|
|
953
|
+
# Constrói e aplica manifests
|
|
954
|
+
manifests = _build_issuer_manifests(config)
|
|
955
|
+
|
|
956
|
+
if _apply_manifest_with_retry(manifests, ctx, f"ClusterIssuer '{config.name}'"):
|
|
957
|
+
_print_next_steps(config)
|
|
958
|
+
typer.secho("\n✓ Cert-manager configurado com sucesso!", fg=typer.colors.GREEN, bold=True)
|
|
959
|
+
else:
|
|
960
|
+
_diagnose_problems(ctx)
|
|
961
|
+
ctx.errors.append("cert-manager: falha ao criar ClusterIssuer")
|
|
130
962
|
|
|
131
|
-
issuer_docs = []
|
|
132
963
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
964
|
+
def status(ctx: ExecutionContext) -> None:
|
|
965
|
+
"""Exibe status detalhado do cert-manager."""
|
|
966
|
+
ensure_tool("kubectl", ctx)
|
|
967
|
+
status_data = _get_cert_manager_status(ctx)
|
|
968
|
+
_print_status(status_data)
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def diagnose(ctx: ExecutionContext) -> None:
|
|
972
|
+
"""Executa diagnóstico completo do cert-manager."""
|
|
973
|
+
ensure_tool("kubectl", ctx)
|
|
974
|
+
status_data = _get_cert_manager_status(ctx)
|
|
975
|
+
_print_status(status_data)
|
|
976
|
+
_diagnose_problems(ctx)
|
|
139
977
|
|
|
140
|
-
issuer_docs.append(_build_cloudflare_secret(secret_name, api_token))
|
|
141
|
-
issuer_docs.append(_build_cloudflare_dns01(issuer_name, email, secret_name, staging))
|
|
142
|
-
else:
|
|
143
|
-
typer.secho("HTTP-01 selecionado (Ingress)", fg=typer.colors.CYAN)
|
|
144
|
-
issuer_name = typer.prompt("Nome do ClusterIssuer", default="letsencrypt-http")
|
|
145
|
-
staging = typer.confirm("Usar endpoint de staging? (para testes)", default=False)
|
|
146
|
-
ingress_class = typer.prompt("IngressClass para resolver HTTP-01", default="traefik")
|
|
147
978
|
|
|
148
|
-
|
|
979
|
+
def install_only(ctx: ExecutionContext) -> bool:
|
|
980
|
+
"""Instala apenas o cert-manager sem configurar issuer (para uso em full_install)."""
|
|
981
|
+
require_root(ctx)
|
|
982
|
+
ensure_tool("helm", ctx)
|
|
983
|
+
ensure_tool("kubectl", ctx)
|
|
984
|
+
|
|
985
|
+
if not _check_cluster_available(ctx):
|
|
986
|
+
ctx.errors.append("cert-manager: cluster não acessível")
|
|
987
|
+
return False
|
|
988
|
+
|
|
989
|
+
# Verifica se já está instalado
|
|
990
|
+
status = _get_cert_manager_status(ctx)
|
|
991
|
+
if status["installed"] and status["webhook_ready"]:
|
|
992
|
+
typer.secho("✓ Cert-manager já está instalado e operacional", fg=typer.colors.GREEN)
|
|
993
|
+
return True
|
|
994
|
+
|
|
995
|
+
# Instala
|
|
996
|
+
if not _install_cert_manager_helm(ctx):
|
|
997
|
+
return False
|
|
998
|
+
|
|
999
|
+
# Aguarda
|
|
1000
|
+
if not ctx.dry_run:
|
|
1001
|
+
return _wait_for_webhook_ready(ctx)
|
|
1002
|
+
|
|
1003
|
+
return True
|
|
149
1004
|
|
|
150
|
-
manifest = "---\n".join(issuer_docs)
|
|
151
|
-
write_file(MANIFEST_PATH, manifest, ctx)
|
|
152
|
-
kubectl_apply(str(MANIFEST_PATH), ctx)
|
|
153
1005
|
|
|
154
|
-
|
|
155
|
-
|
|
1006
|
+
def create_issuer(
|
|
1007
|
+
ctx: ExecutionContext,
|
|
1008
|
+
name: str,
|
|
1009
|
+
email: str,
|
|
1010
|
+
challenge_type: str = "http01",
|
|
1011
|
+
staging: bool = True,
|
|
1012
|
+
ingress_class: str = "traefik",
|
|
1013
|
+
dns_provider: Optional[str] = None,
|
|
1014
|
+
secret_name: str = "dns-api-credentials",
|
|
1015
|
+
credentials: Optional[dict] = None,
|
|
1016
|
+
) -> bool:
|
|
1017
|
+
"""Cria um ClusterIssuer programaticamente (para uso em automação)."""
|
|
1018
|
+
ensure_tool("kubectl", ctx)
|
|
1019
|
+
|
|
1020
|
+
config = IssuerConfig(
|
|
1021
|
+
name=name,
|
|
1022
|
+
email=email,
|
|
1023
|
+
staging=staging,
|
|
1024
|
+
challenge_type=ChallengeType(challenge_type),
|
|
1025
|
+
ingress_class=ingress_class,
|
|
1026
|
+
dns_provider=DNSProvider(dns_provider) if dns_provider else None,
|
|
1027
|
+
secret_name=secret_name,
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
if credentials:
|
|
1031
|
+
config._credentials = credentials
|
|
1032
|
+
|
|
1033
|
+
manifests = _build_issuer_manifests(config)
|
|
1034
|
+
return _apply_manifest_with_retry(manifests, ctx, f"ClusterIssuer '{name}'")
|
|
156
1035
|
|