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.

Files changed (83) hide show
  1. rem/agentic/agents/__init__.py +16 -0
  2. rem/agentic/agents/agent_manager.py +311 -0
  3. rem/agentic/agents/sse_simulator.py +2 -0
  4. rem/agentic/context.py +103 -5
  5. rem/agentic/context_builder.py +36 -9
  6. rem/agentic/mcp/tool_wrapper.py +161 -18
  7. rem/agentic/otel/setup.py +1 -0
  8. rem/agentic/providers/phoenix.py +371 -108
  9. rem/agentic/providers/pydantic_ai.py +172 -30
  10. rem/agentic/schema.py +8 -4
  11. rem/api/deps.py +3 -5
  12. rem/api/main.py +26 -4
  13. rem/api/mcp_router/resources.py +15 -10
  14. rem/api/mcp_router/server.py +11 -3
  15. rem/api/mcp_router/tools.py +418 -4
  16. rem/api/middleware/tracking.py +5 -5
  17. rem/api/routers/admin.py +218 -1
  18. rem/api/routers/auth.py +349 -6
  19. rem/api/routers/chat/completions.py +255 -7
  20. rem/api/routers/chat/models.py +81 -7
  21. rem/api/routers/chat/otel_utils.py +33 -0
  22. rem/api/routers/chat/sse_events.py +17 -1
  23. rem/api/routers/chat/streaming.py +126 -19
  24. rem/api/routers/feedback.py +134 -14
  25. rem/api/routers/messages.py +24 -15
  26. rem/api/routers/query.py +6 -3
  27. rem/auth/__init__.py +13 -3
  28. rem/auth/jwt.py +352 -0
  29. rem/auth/middleware.py +115 -10
  30. rem/auth/providers/__init__.py +4 -1
  31. rem/auth/providers/email.py +215 -0
  32. rem/cli/commands/README.md +42 -0
  33. rem/cli/commands/cluster.py +617 -168
  34. rem/cli/commands/configure.py +4 -7
  35. rem/cli/commands/db.py +66 -22
  36. rem/cli/commands/experiments.py +468 -76
  37. rem/cli/commands/schema.py +6 -5
  38. rem/cli/commands/session.py +336 -0
  39. rem/cli/dreaming.py +2 -2
  40. rem/cli/main.py +2 -0
  41. rem/config.py +8 -1
  42. rem/models/core/experiment.py +58 -14
  43. rem/models/entities/__init__.py +4 -0
  44. rem/models/entities/ontology.py +1 -1
  45. rem/models/entities/ontology_config.py +1 -1
  46. rem/models/entities/subscriber.py +175 -0
  47. rem/models/entities/user.py +1 -0
  48. rem/schemas/agents/core/agent-builder.yaml +235 -0
  49. rem/schemas/agents/examples/contract-analyzer.yaml +1 -1
  50. rem/schemas/agents/examples/contract-extractor.yaml +1 -1
  51. rem/schemas/agents/examples/cv-parser.yaml +1 -1
  52. rem/services/__init__.py +3 -1
  53. rem/services/content/service.py +4 -3
  54. rem/services/email/__init__.py +10 -0
  55. rem/services/email/service.py +513 -0
  56. rem/services/email/templates.py +360 -0
  57. rem/services/phoenix/client.py +59 -18
  58. rem/services/postgres/README.md +38 -0
  59. rem/services/postgres/diff_service.py +127 -6
  60. rem/services/postgres/pydantic_to_sqlalchemy.py +45 -13
  61. rem/services/postgres/repository.py +5 -4
  62. rem/services/postgres/schema_generator.py +205 -4
  63. rem/services/session/compression.py +120 -50
  64. rem/services/session/reload.py +14 -7
  65. rem/services/user_service.py +41 -9
  66. rem/settings.py +442 -23
  67. rem/sql/migrations/001_install.sql +156 -0
  68. rem/sql/migrations/002_install_models.sql +1951 -88
  69. rem/sql/migrations/004_cache_system.sql +548 -0
  70. rem/sql/migrations/005_schema_update.sql +145 -0
  71. rem/utils/README.md +45 -0
  72. rem/utils/__init__.py +18 -0
  73. rem/utils/files.py +157 -1
  74. rem/utils/schema_loader.py +139 -10
  75. rem/utils/sql_paths.py +146 -0
  76. rem/utils/vision.py +1 -1
  77. rem/workers/__init__.py +3 -1
  78. rem/workers/db_listener.py +579 -0
  79. rem/workers/unlogged_maintainer.py +463 -0
  80. {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/METADATA +218 -180
  81. {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/RECORD +83 -68
  82. {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/WHEEL +0 -0
  83. {remdb-0.3.114.dist-info → remdb-0.3.172.dist-info}/entry_points.txt +0 -0
@@ -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 --config file.yaml # Generate manifests from config
7
- rem cluster setup-ssm # Create required SSM parameters
8
- rem cluster generate-sql-configmap # Generate postgres init ConfigMap
9
- rem cluster validate # Validate deployment prerequisites
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-sql-configmap --apply")
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
- Optional Phoenix parameters:
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
- # Required
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 - placeholders that user must fill in
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.token_hex(16), "SecureString", "Phoenix API key"),
365
- (f"{prefix}/phoenix/secret", secrets.token_hex(32), "SecureString", "Phoenix session secret"),
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 if "REPLACE" not in value else 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
- # Remove empty strings
391
- put_cmd = [c for c in put_cmd if c]
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
- click.echo(f" Would create: {name} = {display_value}")
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
- click.secho(f" ✓ {name}", fg="green")
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 parameters configured", fg="green")
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
- @click.command("generate-sql-configmap")
418
- @click.option(
419
- "--config",
420
- "-c",
421
- type=click.Path(exists=True, path_type=Path),
422
- help="Path to cluster config file",
423
- )
424
- @click.option(
425
- "--output",
426
- "-o",
427
- type=click.Path(path_type=Path),
428
- default=None,
429
- help="Output path for ConfigMap YAML",
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
- The ConfigMap includes:
444
- - 001_install.sql: Core infrastructure (extensions, functions)
445
- - 002_install_models.sql: Entity tables (from Pydantic models)
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
- Examples:
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
- cfg = load_cluster_config(config)
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
- # Find SQL directory
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" SQL directory not found: {sql_dir}", fg="red")
462
- click.echo()
463
- click.echo("Generate migrations first with:")
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
- if sql_file.name.startswith(("001_", "002_")):
478
- content = sql_file.read_text(encoding="utf-8")
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(" No SQL files found (001_*.sql, 002_*.sql)", fg="red")
484
- raise click.Abort()
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
- # Output
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-sql-configmap\n")
509
- f.write("# Do not edit manually - regenerate from SQL migrations\n")
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/src/rem/sql/migrations/{name}\n")
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.echo()
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
- def validate(config: Path | None):
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 connectivity
548
- 2. Required namespaces exist
549
- 3. Platform operators installed (ESO, CNPG, KEDA)
550
- 4. ClusterSecretStores configured
551
- 5. SSM parameters exist
552
- 6. Pod Identity associations
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 kubectl connectivity
576
- click.echo("1. Kubernetes connectivity")
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
- click.secho(" ✓ kubectl connected", fg="green")
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
- # 2. Check platform operators
642
+ # 4. Check ArgoCD installation
593
643
  click.echo()
594
- click.echo("2. Platform operators")
595
- operators = [
596
- ("external-secrets-system", "external-secrets", "External Secrets Operator"),
597
- ("cnpg-system", "cnpg-controller-manager", "CloudNativePG"),
598
- ("keda", "keda-operator", "KEDA"),
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
- for ns, deployment, name in operators:
602
- try:
654
+ # Check server deployment
603
655
  result = subprocess.run(
604
- ["kubectl", "get", "deployment", deployment, "-n", ns],
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
- click.secho(f" ✓ {name}", fg="green")
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(f"{name} not found in {ns}")
611
- click.secho(f" ⚠ {name} not found", fg="yellow")
612
- except Exception:
613
- warnings.append(f"Could not check {name}")
614
- click.secho(f" Could not check {name}", fg="yellow")
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
- # 3. Check ClusterSecretStores
675
+ # 5. Check environment variables
617
676
  click.echo()
618
- click.echo("3. ClusterSecretStores")
619
- stores = ["aws-parameter-store", "kubernetes-secrets"]
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 store in stores:
622
- try:
623
- result = subprocess.run(
624
- ["kubectl", "get", "clustersecretstore", store],
625
- capture_output=True,
626
- )
627
- if result.returncode == 0:
628
- click.secho(f" ✓ {store}", fg="green")
629
- else:
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
- # 4. Check SSM parameters
698
+ # 6. Check SSM parameters
637
699
  click.echo()
638
- click.echo("4. SSM parameters")
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
- errors.append(f"Required SSM parameter missing: {param}")
658
- click.secho(f" {param} (required)", fg="red")
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
- warnings.append(f"Optional SSM parameter missing: {param}")
680
- click.secho(f" {param} (optional)", fg="yellow")
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
- click.echo("Ready to deploy:")
702
- click.echo(f" kubectl apply -f manifests/application/rem-stack/argocd-staging.yaml")
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: kubectl apply -f manifests/application/rem-stack/argocd-staging.yaml")
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="siggy",
1096
- help="Kubernetes namespace (default: siggy)",
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="siggy",
1244
- help="Kubernetes namespace (default: siggy)",
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)