remdb 0.3.114__py3-none-any.whl → 0.3.172__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of remdb might be problematic. Click here for more details.
- rem/agentic/agents/__init__.py +16 -0
- rem/agentic/agents/agent_manager.py +311 -0
- rem/agentic/agents/sse_simulator.py +2 -0
- rem/agentic/context.py +103 -5
- rem/agentic/context_builder.py +36 -9
- rem/agentic/mcp/tool_wrapper.py +161 -18
- rem/agentic/otel/setup.py +1 -0
- rem/agentic/providers/phoenix.py +371 -108
- rem/agentic/providers/pydantic_ai.py +172 -30
- rem/agentic/schema.py +8 -4
- rem/api/deps.py +3 -5
- rem/api/main.py +26 -4
- rem/api/mcp_router/resources.py +15 -10
- rem/api/mcp_router/server.py +11 -3
- rem/api/mcp_router/tools.py +418 -4
- rem/api/middleware/tracking.py +5 -5
- rem/api/routers/admin.py +218 -1
- rem/api/routers/auth.py +349 -6
- rem/api/routers/chat/completions.py +255 -7
- rem/api/routers/chat/models.py +81 -7
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +17 -1
- rem/api/routers/chat/streaming.py +126 -19
- rem/api/routers/feedback.py +134 -14
- rem/api/routers/messages.py +24 -15
- rem/api/routers/query.py +6 -3
- rem/auth/__init__.py +13 -3
- rem/auth/jwt.py +352 -0
- rem/auth/middleware.py +115 -10
- rem/auth/providers/__init__.py +4 -1
- rem/auth/providers/email.py +215 -0
- rem/cli/commands/README.md +42 -0
- rem/cli/commands/cluster.py +617 -168
- rem/cli/commands/configure.py +4 -7
- rem/cli/commands/db.py +66 -22
- rem/cli/commands/experiments.py +468 -76
- rem/cli/commands/schema.py +6 -5
- rem/cli/commands/session.py +336 -0
- rem/cli/dreaming.py +2 -2
- rem/cli/main.py +2 -0
- rem/config.py +8 -1
- rem/models/core/experiment.py +58 -14
- rem/models/entities/__init__.py +4 -0
- rem/models/entities/ontology.py +1 -1
- rem/models/entities/ontology_config.py +1 -1
- rem/models/entities/subscriber.py +175 -0
- rem/models/entities/user.py +1 -0
- rem/schemas/agents/core/agent-builder.yaml +235 -0
- rem/schemas/agents/examples/contract-analyzer.yaml +1 -1
- rem/schemas/agents/examples/contract-extractor.yaml +1 -1
- rem/schemas/agents/examples/cv-parser.yaml +1 -1
- rem/services/__init__.py +3 -1
- rem/services/content/service.py +4 -3
- rem/services/email/__init__.py +10 -0
- rem/services/email/service.py +513 -0
- rem/services/email/templates.py +360 -0
- rem/services/phoenix/client.py +59 -18
- rem/services/postgres/README.md +38 -0
- rem/services/postgres/diff_service.py +127 -6
- rem/services/postgres/pydantic_to_sqlalchemy.py +45 -13
- rem/services/postgres/repository.py +5 -4
- rem/services/postgres/schema_generator.py +205 -4
- rem/services/session/compression.py +120 -50
- rem/services/session/reload.py +14 -7
- rem/services/user_service.py +41 -9
- rem/settings.py +442 -23
- rem/sql/migrations/001_install.sql +156 -0
- rem/sql/migrations/002_install_models.sql +1951 -88
- rem/sql/migrations/004_cache_system.sql +548 -0
- rem/sql/migrations/005_schema_update.sql +145 -0
- rem/utils/README.md +45 -0
- rem/utils/__init__.py +18 -0
- rem/utils/files.py +157 -1
- rem/utils/schema_loader.py +139 -10
- rem/utils/sql_paths.py +146 -0
- rem/utils/vision.py +1 -1
- rem/workers/__init__.py +3 -1
- rem/workers/db_listener.py +579 -0
- rem/workers/unlogged_maintainer.py +463 -0
- {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/METADATA +218 -180
- {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/RECORD +83 -68
- {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/WHEEL +0 -0
- {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/entry_points.txt +0 -0
rem/cli/commands/cluster.py
CHANGED
|
@@ -3,10 +3,10 @@ Cluster management commands for deploying REM to Kubernetes.
|
|
|
3
3
|
|
|
4
4
|
Usage:
|
|
5
5
|
rem cluster init # Initialize cluster config
|
|
6
|
-
rem cluster generate
|
|
7
|
-
rem cluster setup-ssm
|
|
8
|
-
rem cluster
|
|
9
|
-
rem cluster
|
|
6
|
+
rem cluster generate # Generate all manifests (including SQL ConfigMap)
|
|
7
|
+
rem cluster setup-ssm # Create required SSM parameters
|
|
8
|
+
rem cluster validate # Validate deployment prerequisites
|
|
9
|
+
rem cluster env check # Validate .env for cluster deployment
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import os
|
|
@@ -299,7 +299,7 @@ def init(
|
|
|
299
299
|
click.echo(f" 1. Edit {output} to customize settings")
|
|
300
300
|
click.echo(" 2. Deploy CDK infrastructure: cd manifests/infra/cdk-eks && cdk deploy")
|
|
301
301
|
click.echo(" 3. Run: rem cluster setup-ssm")
|
|
302
|
-
click.echo(" 4. Run: rem cluster generate
|
|
302
|
+
click.echo(" 4. Run: rem cluster generate")
|
|
303
303
|
click.echo(" 5. Run: rem cluster validate")
|
|
304
304
|
|
|
305
305
|
|
|
@@ -324,19 +324,34 @@ def setup_ssm(config: Path | None, dry_run: bool, force: bool):
|
|
|
324
324
|
"""
|
|
325
325
|
Create required SSM parameters in AWS.
|
|
326
326
|
|
|
327
|
+
Reads API keys from environment variables if set:
|
|
328
|
+
- ANTHROPIC_API_KEY
|
|
329
|
+
- OPENAI_API_KEY
|
|
330
|
+
- GOOGLE_CLIENT_ID (optional)
|
|
331
|
+
- GOOGLE_CLIENT_SECRET (optional)
|
|
332
|
+
|
|
327
333
|
Creates the following parameters under the configured SSM prefix:
|
|
328
|
-
- /postgres/username (String)
|
|
334
|
+
- /postgres/username (String: remuser)
|
|
329
335
|
- /postgres/password (SecureString, auto-generated)
|
|
330
|
-
- /llm/anthropic-api-key (SecureString, placeholder)
|
|
331
|
-
- /llm/openai-api-key (SecureString, placeholder)
|
|
332
|
-
|
|
333
|
-
|
|
336
|
+
- /llm/anthropic-api-key (SecureString, from env or placeholder)
|
|
337
|
+
- /llm/openai-api-key (SecureString, from env or placeholder)
|
|
338
|
+
- /auth/session-secret (SecureString, auto-generated)
|
|
339
|
+
- /auth/google-client-id (String, from env or placeholder)
|
|
340
|
+
- /auth/google-client-secret (SecureString, from env or placeholder)
|
|
334
341
|
- /phoenix/api-key (SecureString, auto-generated)
|
|
335
342
|
- /phoenix/secret (SecureString, auto-generated)
|
|
343
|
+
- /phoenix/admin-secret (SecureString, auto-generated)
|
|
336
344
|
|
|
337
345
|
Examples:
|
|
346
|
+
# With environment variables set
|
|
347
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
348
|
+
export OPENAI_API_KEY=sk-proj-...
|
|
338
349
|
rem cluster setup-ssm
|
|
350
|
+
|
|
351
|
+
# Using config file
|
|
339
352
|
rem cluster setup-ssm --config my-cluster.yaml
|
|
353
|
+
|
|
354
|
+
# Preview without creating
|
|
340
355
|
rem cluster setup-ssm --dry-run
|
|
341
356
|
"""
|
|
342
357
|
import secrets
|
|
@@ -345,6 +360,12 @@ def setup_ssm(config: Path | None, dry_run: bool, force: bool):
|
|
|
345
360
|
prefix = cfg.get("aws", {}).get("ssmPrefix", "/rem")
|
|
346
361
|
region = cfg.get("aws", {}).get("region", "us-east-1")
|
|
347
362
|
|
|
363
|
+
# Read API keys from environment
|
|
364
|
+
anthropic_key = os.environ.get("ANTHROPIC_API_KEY", "")
|
|
365
|
+
openai_key = os.environ.get("OPENAI_API_KEY", "")
|
|
366
|
+
google_client_id = os.environ.get("GOOGLE_CLIENT_ID", "placeholder")
|
|
367
|
+
google_client_secret = os.environ.get("GOOGLE_CLIENT_SECRET", "placeholder")
|
|
368
|
+
|
|
348
369
|
click.echo()
|
|
349
370
|
click.echo("SSM Parameter Setup")
|
|
350
371
|
click.echo("=" * 60)
|
|
@@ -352,19 +373,36 @@ def setup_ssm(config: Path | None, dry_run: bool, force: bool):
|
|
|
352
373
|
click.echo(f"Region: {region}")
|
|
353
374
|
click.echo()
|
|
354
375
|
|
|
376
|
+
# Show env var status
|
|
377
|
+
click.echo("Environment variables:")
|
|
378
|
+
click.echo(f" ANTHROPIC_API_KEY: {'✓ set' if anthropic_key else '✗ not set (will use placeholder)'}")
|
|
379
|
+
click.echo(f" OPENAI_API_KEY: {'✓ set' if openai_key else '✗ not set (will use placeholder)'}")
|
|
380
|
+
click.echo(f" GOOGLE_CLIENT_ID: {'✓ set' if google_client_id != 'placeholder' else '⚠ not set (OAuth disabled)'}")
|
|
381
|
+
click.echo(f" GOOGLE_CLIENT_SECRET: {'✓ set' if google_client_secret != 'placeholder' else '⚠ not set (OAuth disabled)'}")
|
|
382
|
+
click.echo()
|
|
383
|
+
|
|
355
384
|
# Define parameters to create
|
|
356
385
|
parameters = [
|
|
357
|
-
#
|
|
358
|
-
(f"{prefix}/postgres/username", "remuser", "String", "PostgreSQL username"),
|
|
386
|
+
# PostgreSQL - username MUST be remuser to match CNPG cluster owner spec
|
|
387
|
+
(f"{prefix}/postgres/username", "remuser", "String", "PostgreSQL username (must match CNPG owner)"),
|
|
359
388
|
(f"{prefix}/postgres/password", secrets.token_urlsafe(24), "SecureString", "PostgreSQL password"),
|
|
360
|
-
# LLM keys -
|
|
361
|
-
(f"{prefix}/llm/anthropic-api-key", "REPLACE_WITH_YOUR_KEY", "SecureString", "Anthropic API key"),
|
|
362
|
-
(f"{prefix}/llm/openai-api-key", "REPLACE_WITH_YOUR_KEY", "SecureString", "OpenAI API key"),
|
|
389
|
+
# LLM keys - from env or placeholder
|
|
390
|
+
(f"{prefix}/llm/anthropic-api-key", anthropic_key or "REPLACE_WITH_YOUR_KEY", "SecureString", "Anthropic API key"),
|
|
391
|
+
(f"{prefix}/llm/openai-api-key", openai_key or "REPLACE_WITH_YOUR_KEY", "SecureString", "OpenAI API key"),
|
|
392
|
+
# Auth secrets
|
|
393
|
+
(f"{prefix}/auth/session-secret", secrets.token_urlsafe(32), "SecureString", "Session signing secret"),
|
|
394
|
+
(f"{prefix}/auth/google-client-id", google_client_id, "String", "Google OAuth client ID"),
|
|
395
|
+
(f"{prefix}/auth/google-client-secret", google_client_secret, "SecureString", "Google OAuth client secret"),
|
|
363
396
|
# Phoenix - auto-generated
|
|
364
|
-
(f"{prefix}/phoenix/api-key", secrets.
|
|
365
|
-
(f"{prefix}/phoenix/secret", secrets.
|
|
397
|
+
(f"{prefix}/phoenix/api-key", secrets.token_urlsafe(24), "SecureString", "Phoenix API key"),
|
|
398
|
+
(f"{prefix}/phoenix/secret", secrets.token_urlsafe(32), "SecureString", "Phoenix session secret"),
|
|
399
|
+
(f"{prefix}/phoenix/admin-secret", secrets.token_urlsafe(32), "SecureString", "Phoenix admin secret"),
|
|
366
400
|
]
|
|
367
401
|
|
|
402
|
+
created = 0
|
|
403
|
+
skipped = 0
|
|
404
|
+
failed = 0
|
|
405
|
+
|
|
368
406
|
for name, value, param_type, description in parameters:
|
|
369
407
|
# Check if exists
|
|
370
408
|
check_cmd = ["aws", "ssm", "get-parameter", "--name", name, "--region", region]
|
|
@@ -375,113 +413,88 @@ def setup_ssm(config: Path | None, dry_run: bool, force: bool):
|
|
|
375
413
|
|
|
376
414
|
if exists and not force:
|
|
377
415
|
click.echo(f" ⏭ {name} (exists, skipping)")
|
|
416
|
+
skipped += 1
|
|
378
417
|
continue
|
|
379
418
|
|
|
380
419
|
# Create/update parameter
|
|
381
420
|
put_cmd = [
|
|
382
421
|
"aws", "ssm", "put-parameter",
|
|
383
422
|
"--name", name,
|
|
384
|
-
"--value", value
|
|
423
|
+
"--value", value,
|
|
385
424
|
"--type", param_type,
|
|
386
425
|
"--region", region,
|
|
387
|
-
"--overwrite" if force else "",
|
|
388
426
|
"--description", description,
|
|
389
427
|
]
|
|
390
|
-
|
|
391
|
-
|
|
428
|
+
if force:
|
|
429
|
+
put_cmd.append("--overwrite")
|
|
392
430
|
|
|
393
431
|
if dry_run:
|
|
394
432
|
display_value = "***" if param_type == "SecureString" else value
|
|
395
|
-
|
|
433
|
+
if "REPLACE" in value or value == "placeholder":
|
|
434
|
+
click.secho(f" Would create: {name} = {display_value} (PLACEHOLDER)", fg="yellow")
|
|
435
|
+
else:
|
|
436
|
+
click.echo(f" Would create: {name} = {display_value}")
|
|
396
437
|
else:
|
|
397
438
|
try:
|
|
398
439
|
subprocess.run(put_cmd, check=True, capture_output=True)
|
|
399
|
-
|
|
440
|
+
if "REPLACE" in value or value == "placeholder":
|
|
441
|
+
click.secho(f" ⚠ {name} (placeholder - update later)", fg="yellow")
|
|
442
|
+
else:
|
|
443
|
+
click.secho(f" ✓ {name}", fg="green")
|
|
444
|
+
created += 1
|
|
400
445
|
except subprocess.CalledProcessError as e:
|
|
401
446
|
if "ParameterAlreadyExists" in str(e.stderr):
|
|
402
447
|
click.echo(f" ⏭ {name} (exists)")
|
|
448
|
+
skipped += 1
|
|
403
449
|
else:
|
|
404
450
|
click.secho(f" ✗ {name}: {e.stderr.decode()}", fg="red")
|
|
451
|
+
failed += 1
|
|
405
452
|
|
|
406
453
|
click.echo()
|
|
407
454
|
if dry_run:
|
|
408
455
|
click.secho("Dry run - no parameters created", fg="yellow")
|
|
409
456
|
else:
|
|
410
|
-
click.secho("✓ SSM
|
|
411
|
-
click.echo()
|
|
412
|
-
click.echo("IMPORTANT: Update placeholder API keys:")
|
|
413
|
-
click.echo(f" aws ssm put-parameter --name {prefix}/llm/anthropic-api-key --value 'sk-...' --type SecureString --overwrite")
|
|
414
|
-
click.echo(f" aws ssm put-parameter --name {prefix}/llm/openai-api-key --value 'sk-...' --type SecureString --overwrite")
|
|
415
|
-
|
|
457
|
+
click.secho(f"✓ SSM setup complete: {created} created, {skipped} skipped, {failed} failed", fg="green")
|
|
416
458
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
)
|
|
431
|
-
@click.option(
|
|
432
|
-
"--apply",
|
|
433
|
-
is_flag=True,
|
|
434
|
-
help="Apply ConfigMap directly to cluster",
|
|
435
|
-
)
|
|
436
|
-
def generate_sql_configmap(config: Path | None, output: Path | None, apply: bool):
|
|
437
|
-
"""
|
|
438
|
-
Generate Kubernetes ConfigMap from SQL migration files.
|
|
459
|
+
# Show update instructions if placeholders were used
|
|
460
|
+
if not anthropic_key or not openai_key:
|
|
461
|
+
click.echo()
|
|
462
|
+
click.secho("IMPORTANT: Update placeholder API keys:", fg="yellow")
|
|
463
|
+
if not anthropic_key:
|
|
464
|
+
click.echo(f" aws ssm put-parameter --name {prefix}/llm/anthropic-api-key --value 'sk-ant-...' --type SecureString --overwrite --region {region}")
|
|
465
|
+
if not openai_key:
|
|
466
|
+
click.echo(f" aws ssm put-parameter --name {prefix}/llm/openai-api-key --value 'sk-proj-...' --type SecureString --overwrite --region {region}")
|
|
467
|
+
click.echo()
|
|
468
|
+
click.echo("Or set environment variables and re-run with --force:")
|
|
469
|
+
click.echo(" export ANTHROPIC_API_KEY=sk-ant-...")
|
|
470
|
+
click.echo(" export OPENAI_API_KEY=sk-proj-...")
|
|
471
|
+
click.echo(" rem cluster setup-ssm --force")
|
|
439
472
|
|
|
440
|
-
Reads SQL files from rem/src/rem/sql/migrations/ and creates a ConfigMap
|
|
441
|
-
that can be used by CloudNativePG for database initialization.
|
|
442
473
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
474
|
+
def _generate_sql_configmap(project_name: str, namespace: str, output_dir: Path) -> None:
|
|
475
|
+
"""
|
|
476
|
+
Generate SQL init ConfigMap from migration files.
|
|
446
477
|
|
|
447
|
-
|
|
448
|
-
rem cluster generate-sql-configmap
|
|
449
|
-
rem cluster generate-sql-configmap --apply
|
|
450
|
-
rem cluster generate-sql-configmap -o custom-configmap.yaml
|
|
478
|
+
Called by `cluster generate` to include SQL migrations in the manifest generation.
|
|
451
479
|
"""
|
|
452
|
-
|
|
453
|
-
project_name = cfg.get("project", {}).get("name", "rem")
|
|
454
|
-
namespace = cfg.get("project", {}).get("namespace", project_name)
|
|
480
|
+
from ...utils.sql_paths import get_package_migrations_dir
|
|
455
481
|
|
|
456
|
-
|
|
457
|
-
from ...settings import settings
|
|
458
|
-
sql_dir = Path(settings.sql_dir) / "migrations"
|
|
482
|
+
sql_dir = get_package_migrations_dir()
|
|
459
483
|
|
|
460
484
|
if not sql_dir.exists():
|
|
461
|
-
click.secho(f"
|
|
462
|
-
click.echo()
|
|
463
|
-
|
|
464
|
-
click.echo(" rem db schema generate")
|
|
465
|
-
raise click.Abort()
|
|
466
|
-
|
|
467
|
-
click.echo()
|
|
468
|
-
click.echo("Generating SQL ConfigMap")
|
|
469
|
-
click.echo("=" * 60)
|
|
470
|
-
click.echo(f"Source: {sql_dir}")
|
|
471
|
-
click.echo(f"Namespace: {namespace}")
|
|
472
|
-
click.echo()
|
|
485
|
+
click.secho(f" ⚠ SQL directory not found: {sql_dir}", fg="yellow")
|
|
486
|
+
click.echo(" Run 'rem db schema generate' to create migrations")
|
|
487
|
+
return
|
|
473
488
|
|
|
474
|
-
# Read SQL files
|
|
489
|
+
# Read all SQL files in sorted order
|
|
475
490
|
sql_files = {}
|
|
476
491
|
for sql_file in sorted(sql_dir.glob("*.sql")):
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
sql_files[sql_file.name] = content
|
|
480
|
-
click.echo(f" ✓ {sql_file.name} ({len(content)} bytes)")
|
|
492
|
+
content = sql_file.read_text(encoding="utf-8")
|
|
493
|
+
sql_files[sql_file.name] = content
|
|
481
494
|
|
|
482
495
|
if not sql_files:
|
|
483
|
-
click.secho("
|
|
484
|
-
|
|
496
|
+
click.secho(" ⚠ No SQL files found in migrations directory", fg="yellow")
|
|
497
|
+
return
|
|
485
498
|
|
|
486
499
|
# Generate ConfigMap YAML
|
|
487
500
|
configmap = {
|
|
@@ -498,38 +511,20 @@ def generate_sql_configmap(config: Path | None, output: Path | None, apply: bool
|
|
|
498
511
|
"data": sql_files,
|
|
499
512
|
}
|
|
500
513
|
|
|
501
|
-
|
|
502
|
-
if output is None:
|
|
503
|
-
output = get_manifests_dir() / "application" / "rem-stack" / "components" / "postgres" / "postgres-init-configmap.yaml"
|
|
504
|
-
|
|
514
|
+
output = output_dir / "application" / "rem-stack" / "components" / "postgres" / "postgres-init-configmap.yaml"
|
|
505
515
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
506
516
|
|
|
507
517
|
with open(output, "w") as f:
|
|
508
|
-
f.write("# Auto-generated by: rem cluster generate
|
|
509
|
-
f.write("# Do not edit manually - regenerate
|
|
518
|
+
f.write("# Auto-generated by: rem cluster generate\n")
|
|
519
|
+
f.write("# Do not edit manually - regenerate with 'rem cluster generate'\n")
|
|
510
520
|
f.write("#\n")
|
|
511
521
|
f.write("# Source files:\n")
|
|
512
522
|
for name in sql_files:
|
|
513
|
-
f.write(f"# - rem/
|
|
523
|
+
f.write(f"# - rem/sql/migrations/{name}\n")
|
|
514
524
|
f.write("#\n")
|
|
515
525
|
yaml.dump(configmap, f, default_flow_style=False, sort_keys=False)
|
|
516
526
|
|
|
517
|
-
click.
|
|
518
|
-
click.secho(f"✓ Generated: {output}", fg="green")
|
|
519
|
-
|
|
520
|
-
# Apply if requested
|
|
521
|
-
if apply:
|
|
522
|
-
click.echo()
|
|
523
|
-
click.echo("Applying to cluster...")
|
|
524
|
-
try:
|
|
525
|
-
subprocess.run(
|
|
526
|
-
["kubectl", "apply", "-f", str(output)],
|
|
527
|
-
check=True,
|
|
528
|
-
)
|
|
529
|
-
click.secho("✓ ConfigMap applied", fg="green")
|
|
530
|
-
except subprocess.CalledProcessError as e:
|
|
531
|
-
click.secho(f"✗ Failed to apply: {e}", fg="red")
|
|
532
|
-
raise click.Abort()
|
|
527
|
+
click.secho(f" ✓ Generated {output.name} ({len(sql_files)} SQL files)", fg="green")
|
|
533
528
|
|
|
534
529
|
|
|
535
530
|
@click.command()
|
|
@@ -539,20 +534,31 @@ def generate_sql_configmap(config: Path | None, output: Path | None, apply: bool
|
|
|
539
534
|
type=click.Path(exists=True, path_type=Path),
|
|
540
535
|
help="Path to cluster config file",
|
|
541
536
|
)
|
|
542
|
-
|
|
537
|
+
@click.option(
|
|
538
|
+
"--pre-argocd",
|
|
539
|
+
is_flag=True,
|
|
540
|
+
help="Only check prerequisites needed before ArgoCD deployment",
|
|
541
|
+
)
|
|
542
|
+
def validate(config: Path | None, pre_argocd: bool):
|
|
543
543
|
"""
|
|
544
544
|
Validate deployment prerequisites.
|
|
545
545
|
|
|
546
546
|
Checks:
|
|
547
|
-
1. kubectl
|
|
548
|
-
2.
|
|
549
|
-
3.
|
|
550
|
-
4.
|
|
551
|
-
5.
|
|
552
|
-
6.
|
|
547
|
+
1. Required tools (kubectl, aws, openssl)
|
|
548
|
+
2. AWS credentials
|
|
549
|
+
3. Kubernetes connectivity
|
|
550
|
+
4. ArgoCD installation
|
|
551
|
+
5. Environment variables (for setup-ssm)
|
|
552
|
+
6. SSM parameters
|
|
553
|
+
7. Platform operators (ESO, CNPG, KEDA) - skipped with --pre-argocd
|
|
554
|
+
8. ClusterSecretStores - skipped with --pre-argocd
|
|
555
|
+
|
|
556
|
+
Use --pre-argocd to validate only prerequisites needed before
|
|
557
|
+
running 'rem cluster apply' for the first time.
|
|
553
558
|
|
|
554
559
|
Examples:
|
|
555
|
-
rem cluster validate
|
|
560
|
+
rem cluster validate # Full validation
|
|
561
|
+
rem cluster validate --pre-argocd # Pre-deployment checks only
|
|
556
562
|
rem cluster validate --config my-cluster.yaml
|
|
557
563
|
"""
|
|
558
564
|
cfg = load_cluster_config(config)
|
|
@@ -567,13 +573,51 @@ def validate(config: Path | None):
|
|
|
567
573
|
click.echo(f"Project: {project_name}")
|
|
568
574
|
click.echo(f"Namespace: {namespace}")
|
|
569
575
|
click.echo(f"Region: {region}")
|
|
576
|
+
if pre_argocd:
|
|
577
|
+
click.echo(f"Mode: Pre-ArgoCD (checking prerequisites only)")
|
|
570
578
|
click.echo()
|
|
571
579
|
|
|
572
580
|
errors = []
|
|
573
581
|
warnings = []
|
|
574
582
|
|
|
575
|
-
# 1. Check
|
|
576
|
-
click.echo("1.
|
|
583
|
+
# 1. Check required tools
|
|
584
|
+
click.echo("1. Required tools")
|
|
585
|
+
tools = [
|
|
586
|
+
("kubectl", ["kubectl", "version", "--client", "-o", "json"]),
|
|
587
|
+
("aws", ["aws", "--version"]),
|
|
588
|
+
("openssl", ["openssl", "version"]),
|
|
589
|
+
]
|
|
590
|
+
|
|
591
|
+
for tool, cmd in tools:
|
|
592
|
+
if shutil.which(tool):
|
|
593
|
+
click.secho(f" ✓ {tool} installed", fg="green")
|
|
594
|
+
else:
|
|
595
|
+
errors.append(f"{tool} not installed")
|
|
596
|
+
click.secho(f" ✗ {tool} not installed", fg="red")
|
|
597
|
+
|
|
598
|
+
# 2. Check AWS credentials
|
|
599
|
+
click.echo()
|
|
600
|
+
click.echo("2. AWS credentials")
|
|
601
|
+
try:
|
|
602
|
+
result = subprocess.run(
|
|
603
|
+
["aws", "sts", "get-caller-identity", "--region", region],
|
|
604
|
+
capture_output=True,
|
|
605
|
+
timeout=10,
|
|
606
|
+
)
|
|
607
|
+
if result.returncode == 0:
|
|
608
|
+
import json
|
|
609
|
+
identity = json.loads(result.stdout.decode())
|
|
610
|
+
click.secho(f" ✓ AWS credentials valid (account: {identity.get('Account', 'unknown')})", fg="green")
|
|
611
|
+
else:
|
|
612
|
+
errors.append("AWS credentials not configured")
|
|
613
|
+
click.secho(" ✗ AWS credentials not configured", fg="red")
|
|
614
|
+
except Exception as e:
|
|
615
|
+
errors.append(f"AWS CLI error: {e}")
|
|
616
|
+
click.secho(f" ✗ AWS CLI error: {e}", fg="red")
|
|
617
|
+
|
|
618
|
+
# 3. Check kubectl connectivity
|
|
619
|
+
click.echo()
|
|
620
|
+
click.echo("3. Kubernetes connectivity")
|
|
577
621
|
try:
|
|
578
622
|
result = subprocess.run(
|
|
579
623
|
["kubectl", "cluster-info"],
|
|
@@ -581,7 +625,13 @@ def validate(config: Path | None):
|
|
|
581
625
|
timeout=10,
|
|
582
626
|
)
|
|
583
627
|
if result.returncode == 0:
|
|
584
|
-
|
|
628
|
+
# Get context name
|
|
629
|
+
ctx_result = subprocess.run(
|
|
630
|
+
["kubectl", "config", "current-context"],
|
|
631
|
+
capture_output=True,
|
|
632
|
+
)
|
|
633
|
+
context = ctx_result.stdout.decode().strip() if ctx_result.returncode == 0 else "unknown"
|
|
634
|
+
click.secho(f" ✓ kubectl connected (context: {context})", fg="green")
|
|
585
635
|
else:
|
|
586
636
|
errors.append("kubectl not connected to cluster")
|
|
587
637
|
click.secho(" ✗ kubectl not connected", fg="red")
|
|
@@ -589,53 +639,65 @@ def validate(config: Path | None):
|
|
|
589
639
|
errors.append(f"kubectl error: {e}")
|
|
590
640
|
click.secho(f" ✗ kubectl error: {e}", fg="red")
|
|
591
641
|
|
|
592
|
-
#
|
|
642
|
+
# 4. Check ArgoCD installation
|
|
593
643
|
click.echo()
|
|
594
|
-
click.echo("
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
644
|
+
click.echo("4. ArgoCD installation")
|
|
645
|
+
try:
|
|
646
|
+
# Check namespace
|
|
647
|
+
result = subprocess.run(
|
|
648
|
+
["kubectl", "get", "namespace", "argocd"],
|
|
649
|
+
capture_output=True,
|
|
650
|
+
)
|
|
651
|
+
if result.returncode == 0:
|
|
652
|
+
click.secho(" ✓ ArgoCD namespace exists", fg="green")
|
|
600
653
|
|
|
601
|
-
|
|
602
|
-
try:
|
|
654
|
+
# Check server deployment
|
|
603
655
|
result = subprocess.run(
|
|
604
|
-
["kubectl", "get", "deployment",
|
|
656
|
+
["kubectl", "get", "deployment", "argocd-server", "-n", "argocd", "-o", "jsonpath={.status.readyReplicas}"],
|
|
605
657
|
capture_output=True,
|
|
606
658
|
)
|
|
607
|
-
if result.returncode == 0:
|
|
608
|
-
|
|
659
|
+
if result.returncode == 0 and result.stdout.decode().strip():
|
|
660
|
+
replicas = result.stdout.decode().strip()
|
|
661
|
+
click.secho(f" ✓ ArgoCD server running ({replicas} replica(s))", fg="green")
|
|
609
662
|
else:
|
|
610
|
-
warnings.append(
|
|
611
|
-
click.secho(
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
click.secho(
|
|
663
|
+
warnings.append("ArgoCD server not ready")
|
|
664
|
+
click.secho(" ⚠ ArgoCD server not ready", fg="yellow")
|
|
665
|
+
else:
|
|
666
|
+
errors.append("ArgoCD not installed")
|
|
667
|
+
click.secho(" ✗ ArgoCD namespace not found", fg="red")
|
|
668
|
+
click.echo(" Install with:")
|
|
669
|
+
click.echo(" kubectl create namespace argocd")
|
|
670
|
+
click.echo(" kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml")
|
|
671
|
+
except Exception as e:
|
|
672
|
+
errors.append(f"Could not check ArgoCD: {e}")
|
|
673
|
+
click.secho(f" ✗ Could not check ArgoCD: {e}", fg="red")
|
|
615
674
|
|
|
616
|
-
#
|
|
675
|
+
# 5. Check environment variables
|
|
617
676
|
click.echo()
|
|
618
|
-
click.echo("
|
|
619
|
-
|
|
677
|
+
click.echo("5. Environment variables (for setup-ssm)")
|
|
678
|
+
env_vars = [
|
|
679
|
+
("ANTHROPIC_API_KEY", True),
|
|
680
|
+
("OPENAI_API_KEY", True),
|
|
681
|
+
("GITHUB_PAT", True),
|
|
682
|
+
("GITHUB_USERNAME", True),
|
|
683
|
+
("GITHUB_REPO_URL", True),
|
|
684
|
+
("GOOGLE_CLIENT_ID", False),
|
|
685
|
+
("GOOGLE_CLIENT_SECRET", False),
|
|
686
|
+
]
|
|
620
687
|
|
|
621
|
-
for
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
)
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
warnings.append(f"ClusterSecretStore {store} not found")
|
|
631
|
-
click.secho(f" ⚠ {store} not found", fg="yellow")
|
|
632
|
-
except Exception:
|
|
633
|
-
warnings.append(f"Could not check ClusterSecretStore {store}")
|
|
634
|
-
click.secho(f" ⚠ Could not check {store}", fg="yellow")
|
|
688
|
+
for var, required in env_vars:
|
|
689
|
+
value = os.environ.get(var, "")
|
|
690
|
+
if value:
|
|
691
|
+
click.secho(f" ✓ {var} is set", fg="green")
|
|
692
|
+
elif required:
|
|
693
|
+
warnings.append(f"Environment variable not set: {var}")
|
|
694
|
+
click.secho(f" ⚠ {var} not set (required for setup-ssm)", fg="yellow")
|
|
695
|
+
else:
|
|
696
|
+
click.echo(f" - {var} not set (optional)")
|
|
635
697
|
|
|
636
|
-
#
|
|
698
|
+
# 6. Check SSM parameters
|
|
637
699
|
click.echo()
|
|
638
|
-
click.echo("
|
|
700
|
+
click.echo("6. SSM parameters")
|
|
639
701
|
required_params = [
|
|
640
702
|
f"{ssm_prefix}/postgres/username",
|
|
641
703
|
f"{ssm_prefix}/postgres/password",
|
|
@@ -645,6 +707,7 @@ def validate(config: Path | None):
|
|
|
645
707
|
f"{ssm_prefix}/llm/openai-api-key",
|
|
646
708
|
]
|
|
647
709
|
|
|
710
|
+
ssm_ok = True
|
|
648
711
|
for param in required_params:
|
|
649
712
|
try:
|
|
650
713
|
result = subprocess.run(
|
|
@@ -654,11 +717,16 @@ def validate(config: Path | None):
|
|
|
654
717
|
if result.returncode == 0:
|
|
655
718
|
click.secho(f" ✓ {param}", fg="green")
|
|
656
719
|
else:
|
|
657
|
-
|
|
658
|
-
|
|
720
|
+
if pre_argocd:
|
|
721
|
+
click.echo(f" - {param} (will be created by setup-ssm)")
|
|
722
|
+
else:
|
|
723
|
+
errors.append(f"Required SSM parameter missing: {param}")
|
|
724
|
+
click.secho(f" ✗ {param} (required)", fg="red")
|
|
725
|
+
ssm_ok = False
|
|
659
726
|
except Exception as e:
|
|
660
727
|
errors.append(f"Could not check SSM: {e}")
|
|
661
728
|
click.secho(f" ✗ AWS CLI error: {e}", fg="red")
|
|
729
|
+
ssm_ok = False
|
|
662
730
|
break
|
|
663
731
|
|
|
664
732
|
for param in optional_params:
|
|
@@ -676,11 +744,64 @@ def validate(config: Path | None):
|
|
|
676
744
|
else:
|
|
677
745
|
click.secho(f" ✓ {param}", fg="green")
|
|
678
746
|
else:
|
|
679
|
-
|
|
680
|
-
|
|
747
|
+
if pre_argocd:
|
|
748
|
+
click.echo(f" - {param} (will be created by setup-ssm)")
|
|
749
|
+
else:
|
|
750
|
+
warnings.append(f"Optional SSM parameter missing: {param}")
|
|
751
|
+
click.secho(f" ⚠ {param} (optional)", fg="yellow")
|
|
681
752
|
except Exception:
|
|
682
753
|
pass # Already reported AWS CLI issues
|
|
683
754
|
|
|
755
|
+
if not ssm_ok and pre_argocd:
|
|
756
|
+
click.echo(" Run 'rem cluster setup-ssm' to create parameters")
|
|
757
|
+
|
|
758
|
+
# Skip platform operator checks if --pre-argocd
|
|
759
|
+
if not pre_argocd:
|
|
760
|
+
# 7. Check platform operators
|
|
761
|
+
click.echo()
|
|
762
|
+
click.echo("7. Platform operators")
|
|
763
|
+
operators = [
|
|
764
|
+
("external-secrets-system", "external-secrets", "External Secrets Operator"),
|
|
765
|
+
("cnpg-system", "cnpg-controller-manager", "CloudNativePG"),
|
|
766
|
+
("keda", "keda-operator", "KEDA"),
|
|
767
|
+
("cert-manager", "cert-manager", "cert-manager"),
|
|
768
|
+
]
|
|
769
|
+
|
|
770
|
+
for ns, deployment, name in operators:
|
|
771
|
+
try:
|
|
772
|
+
result = subprocess.run(
|
|
773
|
+
["kubectl", "get", "deployment", deployment, "-n", ns],
|
|
774
|
+
capture_output=True,
|
|
775
|
+
)
|
|
776
|
+
if result.returncode == 0:
|
|
777
|
+
click.secho(f" ✓ {name}", fg="green")
|
|
778
|
+
else:
|
|
779
|
+
warnings.append(f"{name} not found in {ns}")
|
|
780
|
+
click.secho(f" ⚠ {name} not found", fg="yellow")
|
|
781
|
+
except Exception:
|
|
782
|
+
warnings.append(f"Could not check {name}")
|
|
783
|
+
click.secho(f" ⚠ Could not check {name}", fg="yellow")
|
|
784
|
+
|
|
785
|
+
# 8. Check ClusterSecretStores
|
|
786
|
+
click.echo()
|
|
787
|
+
click.echo("8. ClusterSecretStores")
|
|
788
|
+
stores = ["aws-parameter-store", "kubernetes-secrets"]
|
|
789
|
+
|
|
790
|
+
for store in stores:
|
|
791
|
+
try:
|
|
792
|
+
result = subprocess.run(
|
|
793
|
+
["kubectl", "get", "clustersecretstore", store],
|
|
794
|
+
capture_output=True,
|
|
795
|
+
)
|
|
796
|
+
if result.returncode == 0:
|
|
797
|
+
click.secho(f" ✓ {store}", fg="green")
|
|
798
|
+
else:
|
|
799
|
+
warnings.append(f"ClusterSecretStore {store} not found")
|
|
800
|
+
click.secho(f" ⚠ {store} not found", fg="yellow")
|
|
801
|
+
except Exception:
|
|
802
|
+
warnings.append(f"Could not check ClusterSecretStore {store}")
|
|
803
|
+
click.secho(f" ⚠ Could not check {store}", fg="yellow")
|
|
804
|
+
|
|
684
805
|
# Summary
|
|
685
806
|
click.echo()
|
|
686
807
|
click.echo("=" * 60)
|
|
@@ -692,14 +813,21 @@ def validate(config: Path | None):
|
|
|
692
813
|
raise click.Abort()
|
|
693
814
|
elif warnings:
|
|
694
815
|
click.secho(f"⚠ Validation passed with {len(warnings)} warning(s)", fg="yellow")
|
|
695
|
-
for warning in warnings:
|
|
816
|
+
for warning in warnings[:5]:
|
|
696
817
|
click.echo(f" - {warning}")
|
|
818
|
+
if len(warnings) > 5:
|
|
819
|
+
click.echo(f" ... and {len(warnings) - 5} more")
|
|
697
820
|
else:
|
|
698
821
|
click.secho("✓ All checks passed", fg="green")
|
|
699
822
|
|
|
700
823
|
click.echo()
|
|
701
|
-
|
|
702
|
-
|
|
824
|
+
if pre_argocd:
|
|
825
|
+
click.echo("Next steps:")
|
|
826
|
+
click.echo(" 1. rem cluster setup-ssm # Create SSM parameters")
|
|
827
|
+
click.echo(" 2. rem cluster apply # Deploy ArgoCD apps")
|
|
828
|
+
else:
|
|
829
|
+
click.echo("Ready to deploy:")
|
|
830
|
+
click.echo(" rem cluster apply")
|
|
703
831
|
|
|
704
832
|
|
|
705
833
|
@click.command()
|
|
@@ -723,6 +851,7 @@ def generate(config: Path | None, output_dir: Path | None):
|
|
|
723
851
|
Reads cluster-config.yaml and generates/updates:
|
|
724
852
|
- ArgoCD Application manifests
|
|
725
853
|
- ClusterSecretStore configurations
|
|
854
|
+
- SQL init ConfigMap (from rem/sql/migrations/*.sql)
|
|
726
855
|
- Kustomization patches
|
|
727
856
|
|
|
728
857
|
Examples:
|
|
@@ -790,13 +919,333 @@ def generate(config: Path | None, output_dir: Path | None):
|
|
|
790
919
|
f.write(content)
|
|
791
920
|
click.secho(f" ✓ Updated {css.name}", fg="green")
|
|
792
921
|
|
|
922
|
+
# Generate SQL init ConfigMap from migrations
|
|
923
|
+
_generate_sql_configmap(project_name, namespace, output_dir)
|
|
924
|
+
|
|
793
925
|
click.echo()
|
|
794
926
|
click.secho("✓ Manifests generated", fg="green")
|
|
795
927
|
click.echo()
|
|
796
928
|
click.echo("Next steps:")
|
|
797
929
|
click.echo(" 1. Review generated manifests")
|
|
798
930
|
click.echo(" 2. Commit changes to git")
|
|
799
|
-
click.echo(" 3. Deploy:
|
|
931
|
+
click.echo(" 3. Deploy: rem cluster apply")
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
@click.command()
|
|
935
|
+
@click.option(
|
|
936
|
+
"--config",
|
|
937
|
+
"-c",
|
|
938
|
+
type=click.Path(exists=True, path_type=Path),
|
|
939
|
+
help="Path to cluster config file",
|
|
940
|
+
)
|
|
941
|
+
@click.option(
|
|
942
|
+
"--dry-run",
|
|
943
|
+
is_flag=True,
|
|
944
|
+
help="Show what would be deployed without executing",
|
|
945
|
+
)
|
|
946
|
+
@click.option(
|
|
947
|
+
"--skip-platform",
|
|
948
|
+
is_flag=True,
|
|
949
|
+
help="Skip deploying platform-apps (only deploy rem-stack)",
|
|
950
|
+
)
|
|
951
|
+
def apply(config: Path | None, dry_run: bool, skip_platform: bool):
|
|
952
|
+
"""
|
|
953
|
+
Deploy ArgoCD applications to the cluster.
|
|
954
|
+
|
|
955
|
+
This command:
|
|
956
|
+
1. Creates ArgoCD repository secret (for private repo access)
|
|
957
|
+
2. Creates the application namespace
|
|
958
|
+
3. Deploys platform-apps (app-of-apps for operators)
|
|
959
|
+
4. Deploys rem-stack application
|
|
960
|
+
|
|
961
|
+
Required environment variables:
|
|
962
|
+
- GITHUB_REPO_URL: Git repository URL
|
|
963
|
+
- GITHUB_PAT: GitHub Personal Access Token
|
|
964
|
+
- GITHUB_USERNAME: GitHub username
|
|
965
|
+
|
|
966
|
+
Examples:
|
|
967
|
+
# Full deployment
|
|
968
|
+
rem cluster apply
|
|
969
|
+
|
|
970
|
+
# Preview what would be deployed
|
|
971
|
+
rem cluster apply --dry-run
|
|
972
|
+
|
|
973
|
+
# Only deploy rem-stack (platform already exists)
|
|
974
|
+
rem cluster apply --skip-platform
|
|
975
|
+
"""
|
|
976
|
+
cfg = load_cluster_config(config)
|
|
977
|
+
project_name = cfg.get("project", {}).get("name", "rem")
|
|
978
|
+
namespace = cfg.get("project", {}).get("namespace", project_name)
|
|
979
|
+
git_repo = cfg.get("git", {}).get("repoURL", "")
|
|
980
|
+
|
|
981
|
+
# Get credentials from environment, with fallback to gh CLI
|
|
982
|
+
github_repo_url = os.environ.get("GITHUB_REPO_URL", git_repo)
|
|
983
|
+
github_pat = os.environ.get("GITHUB_PAT", "")
|
|
984
|
+
github_username = os.environ.get("GITHUB_USERNAME", "")
|
|
985
|
+
|
|
986
|
+
# Auto-detect from gh CLI if not set
|
|
987
|
+
if not github_pat or not github_username:
|
|
988
|
+
try:
|
|
989
|
+
# Try to get from gh CLI
|
|
990
|
+
gh_user = subprocess.run(
|
|
991
|
+
["gh", "api", "user", "--jq", ".login"],
|
|
992
|
+
capture_output=True, text=True, timeout=10
|
|
993
|
+
)
|
|
994
|
+
gh_token = subprocess.run(
|
|
995
|
+
["gh", "auth", "token"],
|
|
996
|
+
capture_output=True, text=True, timeout=10
|
|
997
|
+
)
|
|
998
|
+
if gh_user.returncode == 0 and gh_token.returncode == 0:
|
|
999
|
+
if not github_username:
|
|
1000
|
+
github_username = gh_user.stdout.strip()
|
|
1001
|
+
if not github_pat:
|
|
1002
|
+
github_pat = gh_token.stdout.strip()
|
|
1003
|
+
click.secho(" ℹ Using credentials from gh CLI", fg="cyan")
|
|
1004
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
1005
|
+
pass # gh CLI not available
|
|
1006
|
+
|
|
1007
|
+
# Info about token type
|
|
1008
|
+
if github_pat:
|
|
1009
|
+
if github_pat.startswith("gho_"):
|
|
1010
|
+
click.secho(" ℹ Using OAuth token from gh CLI", fg="cyan")
|
|
1011
|
+
elif github_pat.startswith("ghp_"):
|
|
1012
|
+
click.secho(" ℹ Using Personal Access Token", fg="cyan")
|
|
1013
|
+
elif github_pat.startswith("github_pat_"):
|
|
1014
|
+
click.secho(" ℹ Using Fine-grained Personal Access Token", fg="cyan")
|
|
1015
|
+
|
|
1016
|
+
# Auto-detect git remote if repo URL not set
|
|
1017
|
+
if not github_repo_url:
|
|
1018
|
+
try:
|
|
1019
|
+
result = subprocess.run(
|
|
1020
|
+
["git", "remote", "get-url", "origin"],
|
|
1021
|
+
capture_output=True, text=True, timeout=5
|
|
1022
|
+
)
|
|
1023
|
+
if result.returncode == 0:
|
|
1024
|
+
github_repo_url = result.stdout.strip()
|
|
1025
|
+
click.secho(f" ℹ Using repo URL from git remote: {github_repo_url}", fg="cyan")
|
|
1026
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
1027
|
+
pass
|
|
1028
|
+
|
|
1029
|
+
click.echo()
|
|
1030
|
+
click.echo("ArgoCD Application Deployment")
|
|
1031
|
+
click.echo("=" * 60)
|
|
1032
|
+
|
|
1033
|
+
# Pre-validation
|
|
1034
|
+
click.echo("Pre-flight checks:")
|
|
1035
|
+
errors = 0
|
|
1036
|
+
|
|
1037
|
+
# Check kubectl
|
|
1038
|
+
result = subprocess.run(["which", "kubectl"], capture_output=True)
|
|
1039
|
+
if result.returncode != 0:
|
|
1040
|
+
click.secho(" ✗ kubectl not found", fg="red")
|
|
1041
|
+
errors += 1
|
|
1042
|
+
else:
|
|
1043
|
+
click.secho(" ✓ kubectl available", fg="green")
|
|
1044
|
+
|
|
1045
|
+
# Check cluster access
|
|
1046
|
+
result = subprocess.run(
|
|
1047
|
+
["kubectl", "cluster-info"],
|
|
1048
|
+
capture_output=True,
|
|
1049
|
+
timeout=10,
|
|
1050
|
+
)
|
|
1051
|
+
if result.returncode != 0:
|
|
1052
|
+
click.secho(" ✗ Cannot connect to Kubernetes cluster", fg="red")
|
|
1053
|
+
click.echo(" Run: aws eks update-kubeconfig --name <cluster> --profile rem")
|
|
1054
|
+
errors += 1
|
|
1055
|
+
else:
|
|
1056
|
+
click.secho(" ✓ Kubernetes cluster accessible", fg="green")
|
|
1057
|
+
|
|
1058
|
+
# Check argocd namespace exists
|
|
1059
|
+
result = subprocess.run(
|
|
1060
|
+
["kubectl", "get", "namespace", "argocd"],
|
|
1061
|
+
capture_output=True,
|
|
1062
|
+
)
|
|
1063
|
+
if result.returncode != 0:
|
|
1064
|
+
click.secho(" ✗ argocd namespace not found", fg="red")
|
|
1065
|
+
click.echo(" ArgoCD should be installed by CDK (ENABLE_ARGOCD=true)")
|
|
1066
|
+
errors += 1
|
|
1067
|
+
else:
|
|
1068
|
+
click.secho(" ✓ argocd namespace exists", fg="green")
|
|
1069
|
+
|
|
1070
|
+
if errors > 0:
|
|
1071
|
+
click.echo()
|
|
1072
|
+
click.secho(f"Pre-flight failed with {errors} error(s)", fg="red")
|
|
1073
|
+
raise click.Abort()
|
|
1074
|
+
|
|
1075
|
+
click.echo()
|
|
1076
|
+
click.echo(f"Project: {project_name}")
|
|
1077
|
+
click.echo(f"Namespace: {namespace}")
|
|
1078
|
+
click.echo(f"Repository: {github_repo_url}")
|
|
1079
|
+
if dry_run:
|
|
1080
|
+
click.secho("Mode: DRY RUN (no changes will be made)", fg="yellow")
|
|
1081
|
+
click.echo()
|
|
1082
|
+
|
|
1083
|
+
# Validate required values
|
|
1084
|
+
if not github_repo_url:
|
|
1085
|
+
click.secho("✗ GITHUB_REPO_URL not set", fg="red")
|
|
1086
|
+
click.echo(" Set via environment variable or cluster-config.yaml")
|
|
1087
|
+
raise click.Abort()
|
|
1088
|
+
|
|
1089
|
+
if not github_pat or not github_username:
|
|
1090
|
+
click.secho("⚠ GITHUB_PAT or GITHUB_USERNAME not set", fg="yellow")
|
|
1091
|
+
click.echo(" Private repos will not be accessible without credentials")
|
|
1092
|
+
if not click.confirm("Continue without repo credentials?"):
|
|
1093
|
+
raise click.Abort()
|
|
1094
|
+
|
|
1095
|
+
manifests_dir = get_manifests_dir()
|
|
1096
|
+
|
|
1097
|
+
# Step 1: Create ArgoCD repository secret
|
|
1098
|
+
click.echo("1. ArgoCD repository secret")
|
|
1099
|
+
if github_pat and github_username:
|
|
1100
|
+
# Check if secret exists
|
|
1101
|
+
result = subprocess.run(
|
|
1102
|
+
["kubectl", "get", "secret", "repo-reminiscent", "-n", "argocd"],
|
|
1103
|
+
capture_output=True,
|
|
1104
|
+
)
|
|
1105
|
+
secret_exists = result.returncode == 0
|
|
1106
|
+
|
|
1107
|
+
if secret_exists:
|
|
1108
|
+
click.echo(" ⏭ Secret 'repo-reminiscent' exists (skipping)")
|
|
1109
|
+
else:
|
|
1110
|
+
if dry_run:
|
|
1111
|
+
click.echo(" Would create: secret/repo-reminiscent in argocd namespace")
|
|
1112
|
+
else:
|
|
1113
|
+
# Create the secret
|
|
1114
|
+
create_cmd = [
|
|
1115
|
+
"kubectl", "create", "secret", "generic", "repo-reminiscent",
|
|
1116
|
+
"--namespace", "argocd",
|
|
1117
|
+
f"--from-literal=url={github_repo_url}",
|
|
1118
|
+
f"--from-literal=username={github_username}",
|
|
1119
|
+
f"--from-literal=password={github_pat}",
|
|
1120
|
+
"--from-literal=type=git",
|
|
1121
|
+
"--dry-run=client", "-o", "yaml",
|
|
1122
|
+
]
|
|
1123
|
+
# Pipe to kubectl apply
|
|
1124
|
+
create_result = subprocess.run(create_cmd, capture_output=True)
|
|
1125
|
+
if create_result.returncode == 0:
|
|
1126
|
+
apply_result = subprocess.run(
|
|
1127
|
+
["kubectl", "apply", "-f", "-"],
|
|
1128
|
+
input=create_result.stdout,
|
|
1129
|
+
capture_output=True,
|
|
1130
|
+
)
|
|
1131
|
+
if apply_result.returncode == 0:
|
|
1132
|
+
# Label it as ArgoCD repo secret
|
|
1133
|
+
subprocess.run([
|
|
1134
|
+
"kubectl", "label", "secret", "repo-reminiscent",
|
|
1135
|
+
"-n", "argocd",
|
|
1136
|
+
"argocd.argoproj.io/secret-type=repository",
|
|
1137
|
+
"--overwrite",
|
|
1138
|
+
], capture_output=True)
|
|
1139
|
+
click.secho(" ✓ Created secret 'repo-reminiscent'", fg="green")
|
|
1140
|
+
else:
|
|
1141
|
+
click.secho(f" ✗ Failed to create secret: {apply_result.stderr.decode()}", fg="red")
|
|
1142
|
+
raise click.Abort()
|
|
1143
|
+
else:
|
|
1144
|
+
click.echo(" ⏭ Skipping (no credentials provided)")
|
|
1145
|
+
|
|
1146
|
+
# Step 2: Create namespace
|
|
1147
|
+
click.echo()
|
|
1148
|
+
click.echo("2. Application namespace")
|
|
1149
|
+
result = subprocess.run(
|
|
1150
|
+
["kubectl", "get", "namespace", namespace],
|
|
1151
|
+
capture_output=True,
|
|
1152
|
+
)
|
|
1153
|
+
if result.returncode == 0:
|
|
1154
|
+
click.echo(f" ⏭ Namespace '{namespace}' exists")
|
|
1155
|
+
else:
|
|
1156
|
+
if dry_run:
|
|
1157
|
+
click.echo(f" Would create: namespace/{namespace}")
|
|
1158
|
+
else:
|
|
1159
|
+
result = subprocess.run(
|
|
1160
|
+
["kubectl", "create", "namespace", namespace],
|
|
1161
|
+
capture_output=True,
|
|
1162
|
+
)
|
|
1163
|
+
if result.returncode == 0:
|
|
1164
|
+
click.secho(f" ✓ Created namespace '{namespace}'", fg="green")
|
|
1165
|
+
else:
|
|
1166
|
+
click.secho(f" ✗ Failed to create namespace: {result.stderr.decode()}", fg="red")
|
|
1167
|
+
raise click.Abort()
|
|
1168
|
+
|
|
1169
|
+
# Step 3: Deploy platform-apps (app-of-apps)
|
|
1170
|
+
if not skip_platform:
|
|
1171
|
+
click.echo()
|
|
1172
|
+
click.echo("3. Platform apps (app-of-apps)")
|
|
1173
|
+
platform_app = manifests_dir / "platform" / "argocd" / "app-of-apps.yaml"
|
|
1174
|
+
|
|
1175
|
+
if not platform_app.exists():
|
|
1176
|
+
click.secho(f" ✗ Not found: {platform_app}", fg="red")
|
|
1177
|
+
raise click.Abort()
|
|
1178
|
+
|
|
1179
|
+
if dry_run:
|
|
1180
|
+
click.echo(f" Would apply: {platform_app}")
|
|
1181
|
+
else:
|
|
1182
|
+
result = subprocess.run(
|
|
1183
|
+
["kubectl", "apply", "-f", str(platform_app)],
|
|
1184
|
+
capture_output=True,
|
|
1185
|
+
)
|
|
1186
|
+
if result.returncode == 0:
|
|
1187
|
+
click.secho(" ✓ Applied platform-apps", fg="green")
|
|
1188
|
+
else:
|
|
1189
|
+
click.secho(f" ✗ Failed: {result.stderr.decode()}", fg="red")
|
|
1190
|
+
raise click.Abort()
|
|
1191
|
+
|
|
1192
|
+
# Wait for critical platform apps
|
|
1193
|
+
if not dry_run:
|
|
1194
|
+
click.echo()
|
|
1195
|
+
click.echo(" Waiting for cert-manager...")
|
|
1196
|
+
for _ in range(30): # 5 minutes max
|
|
1197
|
+
result = subprocess.run(
|
|
1198
|
+
["kubectl", "get", "application", "cert-manager", "-n", "argocd",
|
|
1199
|
+
"-o", "jsonpath={.status.health.status}"],
|
|
1200
|
+
capture_output=True,
|
|
1201
|
+
)
|
|
1202
|
+
status = result.stdout.decode().strip()
|
|
1203
|
+
if status == "Healthy":
|
|
1204
|
+
click.secho(" ✓ cert-manager is healthy", fg="green")
|
|
1205
|
+
break
|
|
1206
|
+
click.echo(f" ... cert-manager status: {status or 'Unknown'}")
|
|
1207
|
+
import time
|
|
1208
|
+
time.sleep(10)
|
|
1209
|
+
else:
|
|
1210
|
+
click.secho(" ⚠ cert-manager not healthy yet (continuing anyway)", fg="yellow")
|
|
1211
|
+
|
|
1212
|
+
# Step 4: Deploy rem-stack
|
|
1213
|
+
click.echo()
|
|
1214
|
+
click.echo("4. REM stack application" if not skip_platform else "3. REM stack application")
|
|
1215
|
+
rem_stack_app = manifests_dir / "application" / "rem-stack" / "argocd-staging.yaml"
|
|
1216
|
+
|
|
1217
|
+
if not rem_stack_app.exists():
|
|
1218
|
+
click.secho(f" ✗ Not found: {rem_stack_app}", fg="red")
|
|
1219
|
+
raise click.Abort()
|
|
1220
|
+
|
|
1221
|
+
if dry_run:
|
|
1222
|
+
click.echo(f" Would apply: {rem_stack_app}")
|
|
1223
|
+
else:
|
|
1224
|
+
result = subprocess.run(
|
|
1225
|
+
["kubectl", "apply", "-f", str(rem_stack_app)],
|
|
1226
|
+
capture_output=True,
|
|
1227
|
+
)
|
|
1228
|
+
if result.returncode == 0:
|
|
1229
|
+
click.secho(" ✓ Applied rem-stack-staging", fg="green")
|
|
1230
|
+
else:
|
|
1231
|
+
click.secho(f" ✗ Failed: {result.stderr.decode()}", fg="red")
|
|
1232
|
+
raise click.Abort()
|
|
1233
|
+
|
|
1234
|
+
# Summary
|
|
1235
|
+
click.echo()
|
|
1236
|
+
click.echo("=" * 60)
|
|
1237
|
+
if dry_run:
|
|
1238
|
+
click.secho("Dry run complete - no changes made", fg="yellow")
|
|
1239
|
+
else:
|
|
1240
|
+
click.secho("✓ Deployment initiated", fg="green")
|
|
1241
|
+
click.echo()
|
|
1242
|
+
click.echo("Monitor progress:")
|
|
1243
|
+
click.echo(" kubectl get applications -n argocd")
|
|
1244
|
+
click.echo(" watch kubectl get pods -n " + namespace)
|
|
1245
|
+
click.echo()
|
|
1246
|
+
click.echo("ArgoCD UI:")
|
|
1247
|
+
click.echo(" kubectl port-forward svc/argocd-server -n argocd 8080:443")
|
|
1248
|
+
click.echo(" # Get password: kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d")
|
|
800
1249
|
|
|
801
1250
|
|
|
802
1251
|
# =============================================================================
|
|
@@ -1092,8 +1541,8 @@ def env_check(env_file: Path | None, environment: str, strict: bool):
|
|
|
1092
1541
|
@click.option(
|
|
1093
1542
|
"--namespace",
|
|
1094
1543
|
"-n",
|
|
1095
|
-
default="
|
|
1096
|
-
help="Kubernetes namespace (default:
|
|
1544
|
+
default="rem",
|
|
1545
|
+
help="Kubernetes namespace (default: rem)",
|
|
1097
1546
|
)
|
|
1098
1547
|
@click.option(
|
|
1099
1548
|
"--exclude-secrets",
|
|
@@ -1240,8 +1689,8 @@ def env_generate(
|
|
|
1240
1689
|
@click.option(
|
|
1241
1690
|
"--namespace",
|
|
1242
1691
|
"-n",
|
|
1243
|
-
default="
|
|
1244
|
-
help="Kubernetes namespace (default:
|
|
1692
|
+
default="rem",
|
|
1693
|
+
help="Kubernetes namespace (default: rem)",
|
|
1245
1694
|
)
|
|
1246
1695
|
def env_diff(env_file: Path | None, configmap: str, namespace: str):
|
|
1247
1696
|
"""
|
|
@@ -1353,7 +1802,7 @@ def register_commands(cluster_group):
|
|
|
1353
1802
|
"""Register all cluster commands."""
|
|
1354
1803
|
cluster_group.add_command(init)
|
|
1355
1804
|
cluster_group.add_command(setup_ssm)
|
|
1356
|
-
cluster_group.add_command(generate_sql_configmap)
|
|
1357
1805
|
cluster_group.add_command(validate)
|
|
1358
1806
|
cluster_group.add_command(generate)
|
|
1807
|
+
cluster_group.add_command(apply)
|
|
1359
1808
|
cluster_group.add_command(env)
|