mcp-souschef 2.1.2__py3-none-any.whl → 2.5.3__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.
souschef/deployment.py CHANGED
@@ -16,7 +16,12 @@ from souschef.core.constants import (
16
16
  CHEF_ROLE_PREFIX,
17
17
  METADATA_FILENAME,
18
18
  )
19
- from souschef.core.path_utils import _normalize_path, _safe_join
19
+ from souschef.core.errors import (
20
+ format_error_with_context,
21
+ validate_cookbook_structure,
22
+ validate_directory_exists,
23
+ )
24
+ from souschef.core.path_utils import _safe_join
20
25
 
21
26
  # Maximum length for attribute values in Chef attribute parsing
22
27
  # Prevents ReDoS attacks from extremely long attribute declarations
@@ -38,10 +43,14 @@ def generate_awx_job_template_from_cookbook(
38
43
  Survey specs auto-generated from cookbook attributes when include_survey=True.
39
44
  """
40
45
  try:
41
- cookbook = _normalize_path(cookbook_path)
42
- if not cookbook.exists():
43
- return f"Cookbook not found at {cookbook_path}"
46
+ # Validate inputs
47
+ if not cookbook_name or not cookbook_name.strip():
48
+ return (
49
+ "Error: Cookbook name cannot be empty\n\n"
50
+ "Suggestion: Provide a valid cookbook name"
51
+ )
44
52
 
53
+ cookbook = validate_cookbook_structure(cookbook_path)
45
54
  cookbook_analysis = _analyze_cookbook_for_awx(cookbook, cookbook_name)
46
55
  job_template = _generate_awx_job_template(
47
56
  cookbook_analysis, cookbook_name, target_environment, include_survey
@@ -71,7 +80,9 @@ awx-cli job_templates create \\
71
80
  {_format_cookbook_analysis(cookbook_analysis)}
72
81
  """
73
82
  except Exception as e:
74
- return f"AWX template generation failed for {cookbook_name}: {e}"
83
+ return format_error_with_context(
84
+ e, f"generating AWX job template for {cookbook_name}", cookbook_path
85
+ )
75
86
 
76
87
 
77
88
  def generate_awx_workflow_from_chef_runlist(
@@ -84,9 +95,30 @@ def generate_awx_workflow_from_chef_runlist(
84
95
  Workflows preserve runlist execution order with success/failure paths.
85
96
  """
86
97
  try:
98
+ # Validate inputs
99
+ if not runlist_content or not runlist_content.strip():
100
+ return (
101
+ "Error: Runlist content cannot be empty\n\n"
102
+ "Suggestion: Provide a valid Chef runlist "
103
+ "(e.g., 'recipe[cookbook::recipe]' or JSON array)"
104
+ )
105
+
106
+ if not workflow_name or not workflow_name.strip():
107
+ return (
108
+ "Error: Workflow name cannot be empty\n\n"
109
+ "Suggestion: Provide a descriptive name for the AWX workflow"
110
+ )
111
+
87
112
  # Parse runlist
88
113
  runlist = _parse_chef_runlist(runlist_content)
89
114
 
115
+ if not runlist:
116
+ return (
117
+ "Error: Runlist parsing resulted in no items\n\n"
118
+ "Suggestion: Check runlist format. Expected 'recipe[name]' "
119
+ "or 'role[name]' entries"
120
+ )
121
+
90
122
  # Generate workflow template
91
123
  workflow_template = _generate_awx_workflow_template(
92
124
  runlist, workflow_name, environment
@@ -115,7 +147,9 @@ def generate_awx_workflow_from_chef_runlist(
115
147
  4. Test execution with survey parameters
116
148
  """
117
149
  except Exception as e:
118
- return f"Workflow generation failed: {e}"
150
+ return format_error_with_context(
151
+ e, f"generating AWX workflow from runlist for {workflow_name}"
152
+ )
119
153
 
120
154
 
121
155
  def generate_awx_project_from_cookbooks(
@@ -138,9 +172,16 @@ def generate_awx_project_from_cookbooks(
138
172
 
139
173
  """
140
174
  try:
141
- cookbooks_path = _normalize_path(cookbooks_directory)
142
- if not cookbooks_path.exists():
143
- return f"Error: Cookbooks directory not found: {cookbooks_directory}"
175
+ # Validate inputs
176
+ if not project_name or not project_name.strip():
177
+ return (
178
+ "Error: Project name cannot be empty\n\n"
179
+ "Suggestion: Provide a descriptive name for the AWX project"
180
+ )
181
+
182
+ cookbooks_path = validate_directory_exists(
183
+ cookbooks_directory, "cookbooks directory"
184
+ )
144
185
 
145
186
  # Analyze all cookbooks
146
187
  cookbooks_analysis = _analyze_cookbooks_directory(cookbooks_path)
@@ -181,7 +222,11 @@ def generate_awx_project_from_cookbooks(
181
222
  5. Set up inventories and credentials
182
223
  """
183
224
  except Exception as e:
184
- return f"Project configuration failed: {e}"
225
+ return format_error_with_context(
226
+ e,
227
+ f"generating AWX project configuration for {project_name}",
228
+ cookbooks_directory,
229
+ )
185
230
 
186
231
 
187
232
  def generate_awx_inventory_source_from_chef(
@@ -200,6 +245,21 @@ def generate_awx_inventory_source_from_chef(
200
245
 
201
246
  """
202
247
  try:
248
+ # Validate inputs
249
+ if not chef_server_url or not chef_server_url.strip():
250
+ return (
251
+ "Error: Chef server URL cannot be empty\n\n"
252
+ "Suggestion: Provide a valid Chef server URL "
253
+ "(e.g., https://chef.example.com)"
254
+ )
255
+
256
+ if not chef_server_url.startswith("https://"):
257
+ return (
258
+ f"Error: Invalid Chef server URL: {chef_server_url}\n\n"
259
+ "Suggestion: URL must use HTTPS protocol for security "
260
+ "(e.g., https://chef.example.com)"
261
+ )
262
+
203
263
  # Generate inventory source configuration
204
264
  inventory_source = _generate_chef_inventory_source(
205
265
  chef_server_url, sync_schedule
@@ -240,7 +300,9 @@ def generate_awx_inventory_source_from_chef(
240
300
  - CHEF_CLIENT_KEY: ${{{{chef_client_key}}}}
241
301
  """
242
302
  except Exception as e:
243
- return f"Inventory source generation failed: {e}"
303
+ return format_error_with_context(
304
+ e, "generating AWX inventory source from Chef server", chef_server_url
305
+ )
244
306
 
245
307
 
246
308
  # Deployment Strategy Functions
@@ -256,9 +318,15 @@ def convert_chef_deployment_to_ansible_strategy(
256
318
  Override auto-detection by specifying explicit pattern.
257
319
  """
258
320
  try:
259
- cookbook = _normalize_path(cookbook_path)
260
- if not cookbook.exists():
261
- return f"Error: Cookbook path not found: {cookbook_path}"
321
+ cookbook = validate_cookbook_structure(cookbook_path)
322
+
323
+ # Validate deployment pattern
324
+ valid_patterns = ["auto", "blue_green", "canary", "rolling_update"]
325
+ if deployment_pattern not in valid_patterns:
326
+ return (
327
+ f"Error: Invalid deployment pattern '{deployment_pattern}'\n\n"
328
+ f"Suggestion: Use one of {', '.join(valid_patterns)}"
329
+ )
262
330
 
263
331
  # Analyze Chef deployment pattern
264
332
  pattern_analysis = _analyze_chef_deployment_pattern(cookbook)
@@ -289,7 +357,9 @@ def convert_chef_deployment_to_ansible_strategy(
289
357
  {_generate_deployment_migration_recommendations(pattern_analysis)}
290
358
  """
291
359
  except Exception as e:
292
- return f"Deployment pattern conversion failed: {e}"
360
+ return format_error_with_context(
361
+ e, "converting Chef deployment pattern to Ansible strategy", cookbook_path
362
+ )
293
363
 
294
364
 
295
365
  def generate_blue_green_deployment_playbook(
@@ -307,6 +377,21 @@ def generate_blue_green_deployment_playbook(
307
377
 
308
378
  """
309
379
  try:
380
+ # Validate inputs
381
+ if not app_name or not app_name.strip():
382
+ return (
383
+ "Error: Application name cannot be empty\n\n"
384
+ "Suggestion: Provide a descriptive name for the application "
385
+ "being deployed"
386
+ )
387
+
388
+ if not health_check_url.startswith("/"):
389
+ return (
390
+ f"Error: Health check URL must be a path starting with '/': "
391
+ f"{health_check_url}\n\n"
392
+ "Suggestion: Use a relative path like '/health' or '/api/health'"
393
+ )
394
+
310
395
  # Generate main deployment playbook
311
396
  playbook = _generate_blue_green_playbook(app_name, health_check_url)
312
397
 
@@ -347,26 +432,127 @@ def generate_blue_green_deployment_playbook(
347
432
  - Blue and green environments provisioned
348
433
  """
349
434
  except Exception as e:
350
- return f"Failed to generate blue/green playbook: {e}"
435
+ return format_error_with_context(
436
+ e, f"generating blue/green deployment playbook for {app_name}"
437
+ )
351
438
 
352
439
 
353
- def generate_canary_deployment_strategy(
354
- app_name: str, canary_percentage: int = 10, rollout_steps: str = "10,25,50,100"
355
- ) -> str:
440
+ def _validate_canary_inputs(
441
+ app_name: str, canary_percentage: int, rollout_steps: str
442
+ ) -> tuple[list[int] | None, str | None]:
356
443
  """
357
- Generate canary deployment with progressive rollout.
444
+ Validate canary deployment inputs.
445
+
446
+ Args:
447
+ app_name: Application name
448
+ canary_percentage: Initial canary percentage
449
+ rollout_steps: Comma-separated rollout steps
450
+
451
+ Returns:
452
+ Tuple of (parsed steps list, error message). If error, steps is None.
358
453
 
359
- Starts at canary_percentage, progresses through rollout_steps.
360
- Includes monitoring checks and automatic rollback on failure.
361
454
  """
455
+ # Validate app name
456
+ if not app_name or not app_name.strip():
457
+ return None, (
458
+ "Error: Application name cannot be empty\n\n"
459
+ "Suggestion: Provide a descriptive name for the application"
460
+ )
461
+
462
+ # Validate canary percentage
463
+ if not (1 <= canary_percentage <= 100):
464
+ return None, (
465
+ f"Error: Canary percentage must be between 1 and 100, "
466
+ f"got {canary_percentage}\n\n"
467
+ "Suggestion: Start with 10% for safety"
468
+ )
469
+
470
+ # Parse and validate rollout steps
362
471
  try:
363
- # Parse rollout steps
364
472
  steps = [int(s.strip()) for s in rollout_steps.split(",")]
473
+ if not all(1 <= s <= 100 for s in steps):
474
+ raise ValueError("Steps must be between 1 and 100")
475
+ if steps != sorted(steps):
476
+ return None, (
477
+ "Error: Rollout steps must be in ascending order: "
478
+ f"{rollout_steps}\n\n"
479
+ "Suggestion: Use format like '10,25,50,100'"
480
+ )
481
+ return steps, None
482
+ except ValueError as e:
483
+ return (
484
+ None,
485
+ f"Error: Invalid rollout steps '{rollout_steps}': {e}\n\n"
486
+ "Suggestion: Use comma-separated percentages like '10,25,50,100'",
487
+ )
365
488
 
366
- # Generate canary strategy
367
- strategy = _generate_canary_strategy(app_name, canary_percentage, steps)
368
489
 
369
- return f"""# Canary Deployment Strategy
490
+ def _build_canary_workflow_guide(canary_percentage: int, steps: list[int]) -> str:
491
+ """
492
+ Build deployment workflow guide.
493
+
494
+ Args:
495
+ canary_percentage: Initial canary percentage
496
+ steps: List of rollout step percentages
497
+
498
+ Returns:
499
+ Formatted workflow guide
500
+
501
+ """
502
+ workflow = f"""## Deployment Workflow:
503
+ 1. Deploy canary at {canary_percentage}%: `ansible-playbook deploy_canary.yml`
504
+ 2. Monitor metrics: `ansible-playbook monitor_canary.yml`
505
+ 3. Progressive rollout: `ansible-playbook progressive_rollout.yml`
506
+ """
507
+
508
+ # Add step details
509
+ for i, step_pct in enumerate(steps, 1):
510
+ workflow += f" - Step {i}: {step_pct}% traffic"
511
+ if i == len(steps):
512
+ workflow += " (full rollout)"
513
+ workflow += "\n"
514
+
515
+ workflow += """4. Rollback if issues: `ansible-playbook rollback_canary.yml`
516
+
517
+ ## Monitoring Points:
518
+ - Error rate comparison (canary vs stable)
519
+ - Response time percentiles (p50, p95, p99)
520
+ - Resource utilization (CPU, memory)
521
+ - Custom business metrics
522
+
523
+ ## Rollback Triggers:
524
+ - Error rate increase > 5%
525
+ - Response time degradation > 20%
526
+ - Failed health checks
527
+ - Manual trigger
528
+ """
529
+ return workflow
530
+
531
+
532
+ def _format_canary_output(
533
+ app_name: str,
534
+ canary_percentage: int,
535
+ rollout_steps: str,
536
+ steps: list[int],
537
+ strategy: dict,
538
+ ) -> str:
539
+ """
540
+ Format complete canary deployment output.
541
+
542
+ Args:
543
+ app_name: Application name
544
+ canary_percentage: Initial canary percentage
545
+ rollout_steps: Original rollout steps string
546
+ steps: Parsed rollout steps
547
+ strategy: Generated strategy dict
548
+
549
+ Returns:
550
+ Formatted output string
551
+
552
+ """
553
+ workflow = _build_canary_workflow_guide(canary_percentage, steps)
554
+
555
+ return f"""# Canary Deployment Strategy
370
556
  # Application: {app_name}
371
557
  # Initial Canary: {canary_percentage}%
372
558
  # Rollout Steps: {rollout_steps}
@@ -391,30 +577,53 @@ def generate_canary_deployment_strategy(
391
577
  {strategy["rollback"]}
392
578
  ```
393
579
 
394
- ## Deployment Workflow:
395
- 1. Deploy canary at {canary_percentage}%: `ansible-playbook deploy_canary.yml`
396
- 2. Monitor metrics: `ansible-playbook monitor_canary.yml`
397
- 3. Progressive rollout: `ansible-playbook progressive_rollout.yml`
398
- - Step 1: {steps[0]}% traffic
399
- - Step 2: {steps[1]}% traffic
400
- - Step 3: {steps[2]}% traffic
401
- - Step 4: {steps[3]}% traffic (full rollout)
402
- 4. Rollback if issues: `ansible-playbook rollback_canary.yml`
580
+ {workflow}"""
403
581
 
404
- ## Monitoring Points:
405
- - Error rate comparison (canary vs stable)
406
- - Response time percentiles (p50, p95, p99)
407
- - Resource utilization (CPU, memory)
408
- - Custom business metrics
409
582
 
410
- ## Rollback Triggers:
411
- - Error rate increase > 5%
412
- - Response time degradation > 20%
413
- - Failed health checks
414
- - Manual trigger
415
- """
583
+ def generate_canary_deployment_strategy(
584
+ app_name: str, canary_percentage: int = 10, rollout_steps: str = "10,25,50,100"
585
+ ) -> str:
586
+ """
587
+ Generate canary deployment with progressive rollout.
588
+
589
+ Starts at canary_percentage, progresses through rollout_steps.
590
+ Includes monitoring checks and automatic rollback on failure.
591
+
592
+ Args:
593
+ app_name: Name of the application
594
+ canary_percentage: Initial canary traffic percentage (1-100)
595
+ rollout_steps: Comma-separated progressive rollout steps
596
+
597
+ Returns:
598
+ Formatted canary deployment strategy with playbooks
599
+
600
+ """
601
+ try:
602
+ # Validate inputs
603
+ steps, error = _validate_canary_inputs(
604
+ app_name, canary_percentage, rollout_steps
605
+ )
606
+ if error:
607
+ return error
608
+
609
+ assert steps is not None, "steps must be non-None after successful validation"
610
+
611
+ # Generate canary strategy
612
+ strategy = _generate_canary_strategy(app_name, canary_percentage, steps)
613
+
614
+ # Format output
615
+ return _format_canary_output(
616
+ app_name,
617
+ canary_percentage,
618
+ rollout_steps,
619
+ steps,
620
+ strategy,
621
+ )
622
+
416
623
  except Exception as e:
417
- return f"Canary deployment generation failed: {e}"
624
+ return format_error_with_context(
625
+ e, f"generating canary deployment strategy for {app_name}"
626
+ )
418
627
 
419
628
 
420
629
  def analyze_chef_application_patterns(
@@ -427,9 +636,15 @@ def analyze_chef_application_patterns(
427
636
  Application type helps tune recommendations for web/database/service workloads.
428
637
  """
429
638
  try:
430
- cookbook = _normalize_path(cookbook_path)
431
- if not cookbook.exists():
432
- return f"Error: Cookbook path not found: {cookbook_path}"
639
+ cookbook = validate_cookbook_structure(cookbook_path)
640
+
641
+ # Validate application type
642
+ valid_app_types = ["web_application", "database", "service", "batch", "api"]
643
+ if application_type not in valid_app_types:
644
+ return (
645
+ f"Error: Invalid application type '{application_type}'\n\n"
646
+ f"Suggestion: Use one of {', '.join(valid_app_types)}"
647
+ )
433
648
 
434
649
  # Analyze cookbook for application patterns
435
650
  analysis = _analyze_application_cookbook(cookbook, application_type)
@@ -460,83 +675,156 @@ def analyze_chef_application_patterns(
460
675
  5. Document lessons learned and iterate
461
676
  """
462
677
  except Exception as e:
463
- return f"Couldn't analyze cookbook patterns: {e}"
678
+ return format_error_with_context(
679
+ e,
680
+ f"analyzing Chef application patterns for {application_type}",
681
+ cookbook_path,
682
+ )
464
683
 
465
684
 
466
685
  # AWX Helper Functions
467
686
 
468
687
 
469
- def _analyze_cookbook_for_awx(cookbook_path: Path, cookbook_name: str) -> dict:
470
- """Analyze Chef cookbook structure for AWX job template generation."""
471
- analysis: dict[str, Any] = {
472
- "name": cookbook_name,
473
- "recipes": [],
474
- "attributes": {},
475
- "dependencies": [],
476
- "templates": [],
477
- "files": [],
478
- "survey_fields": [],
479
- }
688
+ def _analyze_recipes(cookbook_path: Path) -> list[dict[str, Any]]:
689
+ """
690
+ Analyze recipes directory for AWX job steps.
480
691
 
481
- # Check for recipes to convert into AWX job steps
692
+ Args:
693
+ cookbook_path: Path to cookbook root
694
+
695
+ Returns:
696
+ List of recipe metadata dicts
697
+
698
+ """
699
+ recipes = []
482
700
  recipes_dir = _safe_join(cookbook_path, "recipes")
483
701
  if recipes_dir.exists():
484
702
  for recipe_file in recipes_dir.glob("*.rb"):
485
- recipe_name = recipe_file.stem
486
- analysis["recipes"].append(
703
+ recipes.append(
487
704
  {
488
- "name": recipe_name,
705
+ "name": recipe_file.stem,
489
706
  "file": str(recipe_file),
490
707
  "size": recipe_file.stat().st_size,
491
708
  }
492
709
  )
710
+ return recipes
711
+
712
+
713
+ def _analyze_attributes_for_survey(
714
+ cookbook_path: Path,
715
+ ) -> tuple[dict[str, Any], list[dict[str, Any]]]:
716
+ """
717
+ Analyze attributes directory for survey field generation.
493
718
 
494
- # Analyze attributes for survey generation
719
+ Args:
720
+ cookbook_path: Path to cookbook root
721
+
722
+ Returns:
723
+ Tuple of (attributes dict, survey fields list)
724
+
725
+ """
726
+ attributes = {}
727
+ survey_fields = []
495
728
  attributes_dir = _safe_join(cookbook_path, "attributes")
729
+
496
730
  if attributes_dir.exists():
497
731
  for attr_file in attributes_dir.glob("*.rb"):
498
732
  try:
499
733
  with attr_file.open("r") as f:
500
734
  content = f.read()
501
735
 
502
- # Extract attribute declarations for survey
503
- attributes = _extract_cookbook_attributes(content)
504
- analysis["attributes"].update(attributes)
736
+ # Extract attribute declarations
737
+ attrs = _extract_cookbook_attributes(content)
738
+ attributes.update(attrs)
505
739
 
506
- # Generate survey fields from attributes
507
- survey_fields = _generate_survey_fields_from_attributes(attributes)
508
- analysis["survey_fields"].extend(survey_fields)
740
+ # Generate survey fields
741
+ fields = _generate_survey_fields_from_attributes(attrs)
742
+ survey_fields.extend(fields)
509
743
 
510
744
  except Exception:
511
745
  # Silently skip malformed attribute files
512
746
  pass
513
747
 
514
- # Analyze dependencies
748
+ return attributes, survey_fields
749
+
750
+
751
+ def _analyze_metadata_dependencies(cookbook_path: Path) -> list[str]:
752
+ """
753
+ Extract cookbook dependencies from metadata.
754
+
755
+ Args:
756
+ cookbook_path: Path to cookbook root
757
+
758
+ Returns:
759
+ List of dependency names
760
+
761
+ """
515
762
  metadata_file = _safe_join(cookbook_path, METADATA_FILENAME)
516
763
  if metadata_file.exists():
517
764
  try:
518
765
  with metadata_file.open("r") as f:
519
766
  content = f.read()
520
-
521
- dependencies = _extract_cookbook_dependencies(content)
522
- analysis["dependencies"] = dependencies
523
-
767
+ return _extract_cookbook_dependencies(content)
524
768
  except Exception:
525
- # Silently skip malformed metadata
526
769
  pass
770
+ return []
771
+
772
+
773
+ def _collect_static_files(cookbook_path: Path) -> tuple[list[str], list[str]]:
774
+ """
775
+ Collect templates and static files from cookbook.
776
+
777
+ Args:
778
+ cookbook_path: Path to cookbook root
779
+
780
+ Returns:
781
+ Tuple of (template names list, file names list)
782
+
783
+ """
784
+ templates = []
785
+ files = []
527
786
 
528
- # Count templates and files
529
787
  templates_dir = _safe_join(cookbook_path, "templates")
530
788
  if templates_dir.exists():
531
- analysis["templates"] = [
532
- f.name for f in templates_dir.rglob("*") if f.is_file()
533
- ]
789
+ templates = [f.name for f in templates_dir.rglob("*") if f.is_file()]
534
790
 
535
791
  files_dir = _safe_join(cookbook_path, "files")
536
792
  if files_dir.exists():
537
- analysis["files"] = [f.name for f in files_dir.rglob("*") if f.is_file()]
793
+ files = [f.name for f in files_dir.rglob("*") if f.is_file()]
538
794
 
539
- return analysis
795
+ return templates, files
796
+
797
+
798
+ def _analyze_cookbook_for_awx(cookbook_path: Path, cookbook_name: str) -> dict:
799
+ """
800
+ Analyze Chef cookbook structure for AWX job template generation.
801
+
802
+ Orchestrates multiple analysis helpers to build comprehensive cookbook metadata.
803
+
804
+ Args:
805
+ cookbook_path: Path to cookbook root
806
+ cookbook_name: Name of the cookbook
807
+
808
+ Returns:
809
+ Analysis dict with recipes, attributes, dependencies, templates, files, surveys
810
+
811
+ """
812
+ # Analyze each dimension independently
813
+ recipes = _analyze_recipes(cookbook_path)
814
+ attributes, survey_fields = _analyze_attributes_for_survey(cookbook_path)
815
+ dependencies = _analyze_metadata_dependencies(cookbook_path)
816
+ templates, files = _collect_static_files(cookbook_path)
817
+
818
+ # Assemble complete analysis
819
+ return {
820
+ "name": cookbook_name,
821
+ "recipes": recipes,
822
+ "attributes": attributes,
823
+ "dependencies": dependencies,
824
+ "templates": templates,
825
+ "files": files,
826
+ "survey_fields": survey_fields,
827
+ }
540
828
 
541
829
 
542
830
  def _generate_awx_job_template(
@@ -1552,16 +1840,19 @@ def _generate_deployment_migration_recommendations(
1552
1840
  return "\n".join(recommendations)
1553
1841
 
1554
1842
 
1555
- def _recommend_ansible_strategies(patterns: dict) -> str:
1556
- """Recommend appropriate Ansible strategies."""
1557
- strategies: list[str] = []
1558
-
1559
- # Handle both formats: list of dicts with 'type' key or list of strings
1560
- pattern_list = patterns.get("deployment_patterns", [])
1843
+ def _extract_detected_patterns(patterns: dict) -> list[str]:
1844
+ """Extract detected patterns from patterns dictionary."""
1845
+ pattern_list: list = patterns.get("deployment_patterns", [])
1561
1846
  if pattern_list and isinstance(pattern_list[0], dict):
1562
- detected_patterns = [p["type"] for p in pattern_list]
1563
- else:
1564
- detected_patterns = pattern_list
1847
+ return [p["type"] for p in pattern_list]
1848
+ return list(pattern_list)
1849
+
1850
+
1851
+ def _build_deployment_strategy_recommendations(
1852
+ detected_patterns: list[str],
1853
+ ) -> list[str]:
1854
+ """Build deployment strategy recommendations based on detected patterns."""
1855
+ strategies: list[str] = []
1565
1856
 
1566
1857
  if "blue_green" in detected_patterns:
1567
1858
  strategies.append(
@@ -1574,7 +1865,15 @@ def _recommend_ansible_strategies(patterns: dict) -> str:
1574
1865
  "• Rolling Update: Balanced approach with configurable parallelism"
1575
1866
  )
1576
1867
 
1577
- # Application-pattern specific strategies
1868
+ return strategies
1869
+
1870
+
1871
+ def _build_application_strategy_recommendations(
1872
+ detected_patterns: list[str],
1873
+ ) -> list[str]:
1874
+ """Build application-pattern specific strategy recommendations."""
1875
+ strategies: list[str] = []
1876
+
1578
1877
  if "package_management" in detected_patterns:
1579
1878
  strategies.append("• Package: Use `package` module for package installation")
1580
1879
  if "configuration_management" in detected_patterns:
@@ -1584,11 +1883,26 @@ def _recommend_ansible_strategies(patterns: dict) -> str:
1584
1883
  if "source_deployment" in detected_patterns:
1585
1884
  strategies.append("• Source: Use `git` module for source code deployment")
1586
1885
 
1886
+ return strategies
1887
+
1888
+
1889
+ def _get_default_strategy_recommendations() -> list[str]:
1890
+ """Get default strategy recommendations when no patterns detected."""
1891
+ return [
1892
+ "• Rolling Update: Recommended starting strategy",
1893
+ "• Blue/Green: For critical applications requiring zero downtime",
1894
+ "• Canary: For high-risk deployments requiring validation",
1895
+ ]
1896
+
1897
+
1898
+ def _recommend_ansible_strategies(patterns: dict) -> str:
1899
+ """Recommend appropriate Ansible strategies."""
1900
+ detected_patterns = _extract_detected_patterns(patterns)
1901
+
1902
+ strategies = _build_deployment_strategy_recommendations(detected_patterns)
1903
+ strategies.extend(_build_application_strategy_recommendations(detected_patterns))
1904
+
1587
1905
  if not strategies:
1588
- strategies = [
1589
- "• Rolling Update: Recommended starting strategy",
1590
- "• Blue/Green: For critical applications requiring zero downtime",
1591
- "• Canary: For high-risk deployments requiring validation",
1592
- ]
1906
+ strategies = _get_default_strategy_recommendations()
1593
1907
 
1594
1908
  return "\n".join(strategies)