remdb 0.3.118__py3-none-any.whl → 0.3.141__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 (40) hide show
  1. rem/agentic/agents/sse_simulator.py +2 -0
  2. rem/agentic/context.py +23 -3
  3. rem/agentic/mcp/tool_wrapper.py +126 -15
  4. rem/agentic/otel/setup.py +1 -0
  5. rem/agentic/providers/phoenix.py +371 -108
  6. rem/agentic/providers/pydantic_ai.py +122 -43
  7. rem/agentic/schema.py +4 -1
  8. rem/api/mcp_router/tools.py +13 -2
  9. rem/api/routers/chat/completions.py +250 -4
  10. rem/api/routers/chat/models.py +81 -7
  11. rem/api/routers/chat/otel_utils.py +33 -0
  12. rem/api/routers/chat/sse_events.py +17 -1
  13. rem/api/routers/chat/streaming.py +35 -1
  14. rem/api/routers/feedback.py +134 -14
  15. rem/cli/commands/cluster.py +590 -82
  16. rem/cli/commands/configure.py +3 -4
  17. rem/cli/commands/experiments.py +436 -30
  18. rem/cli/commands/session.py +336 -0
  19. rem/cli/dreaming.py +2 -2
  20. rem/cli/main.py +2 -0
  21. rem/config.py +8 -1
  22. rem/models/core/experiment.py +54 -0
  23. rem/models/entities/ontology.py +1 -1
  24. rem/models/entities/ontology_config.py +1 -1
  25. rem/schemas/agents/examples/contract-analyzer.yaml +1 -1
  26. rem/schemas/agents/examples/contract-extractor.yaml +1 -1
  27. rem/schemas/agents/examples/cv-parser.yaml +1 -1
  28. rem/services/phoenix/client.py +59 -18
  29. rem/services/session/compression.py +7 -0
  30. rem/settings.py +236 -13
  31. rem/sql/migrations/002_install_models.sql +91 -91
  32. rem/sql/migrations/004_cache_system.sql +1 -1
  33. rem/utils/schema_loader.py +94 -3
  34. rem/utils/vision.py +1 -1
  35. rem/workers/__init__.py +2 -1
  36. rem/workers/db_listener.py +579 -0
  37. {remdb-0.3.118.dist-info → remdb-0.3.141.dist-info}/METADATA +156 -144
  38. {remdb-0.3.118.dist-info → remdb-0.3.141.dist-info}/RECORD +40 -37
  39. {remdb-0.3.118.dist-info → remdb-0.3.141.dist-info}/WHEEL +0 -0
  40. {remdb-0.3.118.dist-info → remdb-0.3.141.dist-info}/entry_points.txt +0 -0
@@ -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,43 +413,62 @@ 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")
457
+ click.secho(f"✓ SSM setup complete: {created} created, {skipped} skipped, {failed} failed", fg="green")
458
+
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")
415
472
 
416
473
 
417
474
  def _generate_sql_configmap(project_name: str, namespace: str, output_dir: Path) -> None:
@@ -477,20 +534,31 @@ def _generate_sql_configmap(project_name: str, namespace: str, output_dir: Path)
477
534
  type=click.Path(exists=True, path_type=Path),
478
535
  help="Path to cluster config file",
479
536
  )
480
- 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):
481
543
  """
482
544
  Validate deployment prerequisites.
483
545
 
484
546
  Checks:
485
- 1. kubectl connectivity
486
- 2. Required namespaces exist
487
- 3. Platform operators installed (ESO, CNPG, KEDA)
488
- 4. ClusterSecretStores configured
489
- 5. SSM parameters exist
490
- 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.
491
558
 
492
559
  Examples:
493
- rem cluster validate
560
+ rem cluster validate # Full validation
561
+ rem cluster validate --pre-argocd # Pre-deployment checks only
494
562
  rem cluster validate --config my-cluster.yaml
495
563
  """
496
564
  cfg = load_cluster_config(config)
@@ -505,13 +573,51 @@ def validate(config: Path | None):
505
573
  click.echo(f"Project: {project_name}")
506
574
  click.echo(f"Namespace: {namespace}")
507
575
  click.echo(f"Region: {region}")
576
+ if pre_argocd:
577
+ click.echo(f"Mode: Pre-ArgoCD (checking prerequisites only)")
508
578
  click.echo()
509
579
 
510
580
  errors = []
511
581
  warnings = []
512
582
 
513
- # 1. Check kubectl connectivity
514
- 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")
515
621
  try:
516
622
  result = subprocess.run(
517
623
  ["kubectl", "cluster-info"],
@@ -519,7 +625,13 @@ def validate(config: Path | None):
519
625
  timeout=10,
520
626
  )
521
627
  if result.returncode == 0:
522
- 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")
523
635
  else:
524
636
  errors.append("kubectl not connected to cluster")
525
637
  click.secho(" ✗ kubectl not connected", fg="red")
@@ -527,53 +639,65 @@ def validate(config: Path | None):
527
639
  errors.append(f"kubectl error: {e}")
528
640
  click.secho(f" ✗ kubectl error: {e}", fg="red")
529
641
 
530
- # 2. Check platform operators
642
+ # 4. Check ArgoCD installation
531
643
  click.echo()
532
- click.echo("2. Platform operators")
533
- operators = [
534
- ("external-secrets-system", "external-secrets", "External Secrets Operator"),
535
- ("cnpg-system", "cnpg-controller-manager", "CloudNativePG"),
536
- ("keda", "keda-operator", "KEDA"),
537
- ]
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")
538
653
 
539
- for ns, deployment, name in operators:
540
- try:
654
+ # Check server deployment
541
655
  result = subprocess.run(
542
- ["kubectl", "get", "deployment", deployment, "-n", ns],
656
+ ["kubectl", "get", "deployment", "argocd-server", "-n", "argocd", "-o", "jsonpath={.status.readyReplicas}"],
543
657
  capture_output=True,
544
658
  )
545
- if result.returncode == 0:
546
- 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")
547
662
  else:
548
- warnings.append(f"{name} not found in {ns}")
549
- click.secho(f" ⚠ {name} not found", fg="yellow")
550
- except Exception:
551
- warnings.append(f"Could not check {name}")
552
- 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")
553
674
 
554
- # 3. Check ClusterSecretStores
675
+ # 5. Check environment variables
555
676
  click.echo()
556
- click.echo("3. ClusterSecretStores")
557
- 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
+ ]
558
687
 
559
- for store in stores:
560
- try:
561
- result = subprocess.run(
562
- ["kubectl", "get", "clustersecretstore", store],
563
- capture_output=True,
564
- )
565
- if result.returncode == 0:
566
- click.secho(f" ✓ {store}", fg="green")
567
- else:
568
- warnings.append(f"ClusterSecretStore {store} not found")
569
- click.secho(f" ⚠ {store} not found", fg="yellow")
570
- except Exception:
571
- warnings.append(f"Could not check ClusterSecretStore {store}")
572
- 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)")
573
697
 
574
- # 4. Check SSM parameters
698
+ # 6. Check SSM parameters
575
699
  click.echo()
576
- click.echo("4. SSM parameters")
700
+ click.echo("6. SSM parameters")
577
701
  required_params = [
578
702
  f"{ssm_prefix}/postgres/username",
579
703
  f"{ssm_prefix}/postgres/password",
@@ -583,6 +707,7 @@ def validate(config: Path | None):
583
707
  f"{ssm_prefix}/llm/openai-api-key",
584
708
  ]
585
709
 
710
+ ssm_ok = True
586
711
  for param in required_params:
587
712
  try:
588
713
  result = subprocess.run(
@@ -592,11 +717,16 @@ def validate(config: Path | None):
592
717
  if result.returncode == 0:
593
718
  click.secho(f" ✓ {param}", fg="green")
594
719
  else:
595
- errors.append(f"Required SSM parameter missing: {param}")
596
- 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
597
726
  except Exception as e:
598
727
  errors.append(f"Could not check SSM: {e}")
599
728
  click.secho(f" ✗ AWS CLI error: {e}", fg="red")
729
+ ssm_ok = False
600
730
  break
601
731
 
602
732
  for param in optional_params:
@@ -614,11 +744,64 @@ def validate(config: Path | None):
614
744
  else:
615
745
  click.secho(f" ✓ {param}", fg="green")
616
746
  else:
617
- warnings.append(f"Optional SSM parameter missing: {param}")
618
- 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")
619
752
  except Exception:
620
753
  pass # Already reported AWS CLI issues
621
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
+
622
805
  # Summary
623
806
  click.echo()
624
807
  click.echo("=" * 60)
@@ -630,14 +813,21 @@ def validate(config: Path | None):
630
813
  raise click.Abort()
631
814
  elif warnings:
632
815
  click.secho(f"⚠ Validation passed with {len(warnings)} warning(s)", fg="yellow")
633
- for warning in warnings:
816
+ for warning in warnings[:5]:
634
817
  click.echo(f" - {warning}")
818
+ if len(warnings) > 5:
819
+ click.echo(f" ... and {len(warnings) - 5} more")
635
820
  else:
636
821
  click.secho("✓ All checks passed", fg="green")
637
822
 
638
823
  click.echo()
639
- click.echo("Ready to deploy:")
640
- 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")
641
831
 
642
832
 
643
833
  @click.command()
@@ -738,7 +928,324 @@ def generate(config: Path | None, output_dir: Path | None):
738
928
  click.echo("Next steps:")
739
929
  click.echo(" 1. Review generated manifests")
740
930
  click.echo(" 2. Commit changes to git")
741
- 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")
742
1249
 
743
1250
 
744
1251
  # =============================================================================
@@ -1034,8 +1541,8 @@ def env_check(env_file: Path | None, environment: str, strict: bool):
1034
1541
  @click.option(
1035
1542
  "--namespace",
1036
1543
  "-n",
1037
- default="siggy",
1038
- help="Kubernetes namespace (default: siggy)",
1544
+ default="rem",
1545
+ help="Kubernetes namespace (default: rem)",
1039
1546
  )
1040
1547
  @click.option(
1041
1548
  "--exclude-secrets",
@@ -1182,8 +1689,8 @@ def env_generate(
1182
1689
  @click.option(
1183
1690
  "--namespace",
1184
1691
  "-n",
1185
- default="siggy",
1186
- help="Kubernetes namespace (default: siggy)",
1692
+ default="rem",
1693
+ help="Kubernetes namespace (default: rem)",
1187
1694
  )
1188
1695
  def env_diff(env_file: Path | None, configmap: str, namespace: str):
1189
1696
  """
@@ -1297,4 +1804,5 @@ def register_commands(cluster_group):
1297
1804
  cluster_group.add_command(setup_ssm)
1298
1805
  cluster_group.add_command(validate)
1299
1806
  cluster_group.add_command(generate)
1807
+ cluster_group.add_command(apply)
1300
1808
  cluster_group.add_command(env)