mcp-souschef 2.5.3__py3-none-any.whl → 3.0.0__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/ci/common.py ADDED
@@ -0,0 +1,126 @@
1
+ """Common CI/CD analysis utilities for Chef cookbooks."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import yaml
7
+
8
+ from souschef.core.path_utils import _normalize_path
9
+
10
+
11
+ def _normalize_cookbook_base(cookbook_path: str | Path) -> Path:
12
+ """Normalise and resolve a cookbook path to prevent traversal."""
13
+ return (
14
+ _normalize_path(cookbook_path)
15
+ if isinstance(cookbook_path, str)
16
+ else cookbook_path.resolve()
17
+ )
18
+
19
+
20
+ def _initialise_patterns(base_path: Path) -> dict[str, Any]:
21
+ """Build initial pattern flags before deeper inspection."""
22
+ return {
23
+ "has_kitchen": (base_path / ".kitchen.yml").exists(),
24
+ "has_chefspec": (base_path / "spec").exists(),
25
+ "has_inspec": (base_path / "test" / "integration").exists(),
26
+ "has_berksfile": (base_path / "Berksfile").exists(),
27
+ "has_cookstyle": (base_path / ".cookstyle.yml").exists(),
28
+ "has_foodcritic": (base_path / ".foodcritic").exists(),
29
+ "lint_tools": [],
30
+ "kitchen_suites": [],
31
+ "kitchen_platforms": [],
32
+ }
33
+
34
+
35
+ def _detect_lint_tools(base_path: Path) -> list[str]:
36
+ """Detect linting tools configured in the cookbook directory."""
37
+ lint_tools: list[str] = []
38
+ if (base_path / ".foodcritic").exists():
39
+ lint_tools.append("foodcritic")
40
+ if (base_path / ".cookstyle.yml").exists():
41
+ lint_tools.append("cookstyle")
42
+ return lint_tools
43
+
44
+
45
+ def _has_chefspec_tests(base_path: Path) -> bool:
46
+ """Return True when ChefSpec tests are present."""
47
+ spec_dir = base_path / "spec"
48
+ return spec_dir.exists() and any(spec_dir.glob("**/*_spec.rb"))
49
+
50
+
51
+ def _parse_kitchen_configuration(kitchen_file: Path) -> tuple[list[str], list[str]]:
52
+ """Extract Test Kitchen suites and platforms from configuration."""
53
+ kitchen_suites: list[str] = []
54
+ kitchen_platforms: list[str] = []
55
+
56
+ try:
57
+ with kitchen_file.open() as file_handle:
58
+ kitchen_config = yaml.safe_load(file_handle)
59
+ if not kitchen_config:
60
+ return kitchen_suites, kitchen_platforms
61
+
62
+ suites = kitchen_config.get("suites", [])
63
+ if suites:
64
+ kitchen_suites.extend(suite.get("name", "default") for suite in suites)
65
+
66
+ platforms = kitchen_config.get("platforms", [])
67
+ if platforms:
68
+ kitchen_platforms.extend(
69
+ platform.get("name", "unknown") for platform in platforms
70
+ )
71
+ except (yaml.YAMLError, OSError, KeyError, TypeError, AttributeError):
72
+ # Gracefully handle malformed .kitchen.yml - continue with empty config
73
+ # Catches: YAML syntax errors, file I/O errors, missing config keys,
74
+ # type mismatches in config structure, and missing dict attributes
75
+ return kitchen_suites, kitchen_platforms
76
+
77
+ return kitchen_suites, kitchen_platforms
78
+
79
+
80
+ def analyse_chef_ci_patterns(cookbook_path: str | Path) -> dict[str, Any]:
81
+ """
82
+ Analyse Chef cookbook for CI/CD patterns and testing configurations.
83
+
84
+ This function examines a Chef cookbook directory to detect various
85
+ testing and linting tools, as well as Test Kitchen configurations
86
+ including suites and platforms.
87
+
88
+ Args:
89
+ cookbook_path: Path to the Chef cookbook directory to analyse.
90
+
91
+ Returns:
92
+ Dictionary containing detected patterns with the following keys:
93
+ - has_kitchen (bool): Whether Test Kitchen is configured
94
+ (.kitchen.yml exists)
95
+ - has_chefspec (bool): Whether ChefSpec tests are present
96
+ (spec/**/*_spec.rb files)
97
+ - has_inspec (bool): Whether InSpec tests are present
98
+ (test/integration/ exists)
99
+ - has_berksfile (bool): Whether Berksfile exists
100
+ - lint_tools (list[str]): List of detected linting tools
101
+ ('foodcritic', 'cookstyle')
102
+ - kitchen_suites (list[str]): Names of Test Kitchen suites
103
+ found in .kitchen.yml
104
+
105
+ Note:
106
+ If .kitchen.yml is malformed or cannot be parsed, the function
107
+ continues with empty suite and platform lists rather than
108
+ raising an exception.
109
+
110
+ """
111
+ base_path = _normalize_cookbook_base(cookbook_path)
112
+
113
+ patterns: dict[str, Any] = _initialise_patterns(base_path)
114
+ patterns["lint_tools"] = _detect_lint_tools(base_path)
115
+ patterns["has_chefspec"] = _has_chefspec_tests(base_path)
116
+
117
+ kitchen_file = base_path / ".kitchen.yml"
118
+ if kitchen_file.exists():
119
+ suites, platforms = _parse_kitchen_configuration(kitchen_file)
120
+ patterns["kitchen_suites"] = suites
121
+ patterns["kitchen_platforms"] = platforms
122
+
123
+ # Add backward compatibility alias
124
+ patterns["test_suites"] = patterns["kitchen_suites"]
125
+
126
+ return patterns
@@ -11,6 +11,8 @@ from typing import Any
11
11
 
12
12
  import yaml
13
13
 
14
+ from souschef.ci.common import analyse_chef_ci_patterns
15
+
14
16
  # GitHub Actions constants
15
17
  ACTION_CHECKOUT = "actions/checkout@v4"
16
18
  ACTION_SETUP_RUBY = "ruby/setup-ruby@v1"
@@ -52,8 +54,8 @@ def generate_github_workflow_from_chef_ci(
52
54
  if not cookbook_dir.exists():
53
55
  raise FileNotFoundError(f"Cookbook directory not found: {cookbook_path}")
54
56
 
55
- # Analyze Chef CI patterns
56
- patterns = _analyze_chef_ci_patterns(cookbook_dir)
57
+ # Analyse Chef CI patterns
58
+ patterns = analyse_chef_ci_patterns(cookbook_dir)
57
59
 
58
60
  # Build workflow structure
59
61
  workflow = _build_workflow_structure(
@@ -63,97 +65,6 @@ def generate_github_workflow_from_chef_ci(
63
65
  return yaml.dump(workflow, default_flow_style=False, sort_keys=False)
64
66
 
65
67
 
66
- def _analyze_chef_ci_patterns(cookbook_dir: Path) -> dict[str, Any]:
67
- """
68
- Analyze Chef cookbook for CI/CD patterns and testing configurations.
69
-
70
- This function examines a Chef cookbook directory to detect various
71
- testing and linting tools, as well as Test Kitchen configurations
72
- including suites and platforms.
73
-
74
- Args:
75
- cookbook_dir: Path to the Chef cookbook directory to analyze.
76
-
77
- Returns:
78
- Dictionary containing detected patterns with the following keys:
79
- - has_kitchen (bool): Whether Test Kitchen is configured
80
- (.kitchen.yml exists)
81
- - has_chefspec (bool): Whether ChefSpec tests are present
82
- (spec/**/*_spec.rb files)
83
- - has_cookstyle (bool): Whether Cookstyle is configured
84
- (.cookstyle.yml exists)
85
- - has_foodcritic (bool): Whether Foodcritic (legacy) is
86
- configured (.foodcritic exists)
87
- - kitchen_suites (list[str]): Names of Test Kitchen suites
88
- found in .kitchen.yml
89
- - kitchen_platforms (list[str]): Names of Test Kitchen
90
- platforms found in .kitchen.yml
91
-
92
- Note:
93
- If .kitchen.yml is malformed or cannot be parsed, the function
94
- continues with empty suite and platform lists rather than
95
- raising an exception.
96
-
97
- Example:
98
- >>> patterns = _analyze_chef_ci_patterns(Path("/path/to/cookbook"))
99
- >>> patterns["has_kitchen"]
100
- True
101
- >>> patterns["kitchen_suites"]
102
- ['default', 'integration']
103
-
104
- """
105
- patterns: dict[str, Any] = {
106
- "has_kitchen": False,
107
- "has_chefspec": False,
108
- "has_cookstyle": False,
109
- "has_foodcritic": False,
110
- "kitchen_suites": [],
111
- "kitchen_platforms": [],
112
- }
113
-
114
- # Check for Test Kitchen
115
- kitchen_yml = cookbook_dir / ".kitchen.yml"
116
- if kitchen_yml.exists():
117
- patterns["has_kitchen"] = True
118
- try:
119
- with kitchen_yml.open() as f:
120
- kitchen_config = yaml.safe_load(f)
121
- if kitchen_config:
122
- # Extract suites
123
- suites = kitchen_config.get("suites", [])
124
- if suites:
125
- patterns["kitchen_suites"] = [
126
- s.get("name", "default") for s in suites
127
- ]
128
- # Extract platforms
129
- platforms = kitchen_config.get("platforms", [])
130
- if platforms:
131
- patterns["kitchen_platforms"] = [
132
- p.get("name", "unknown") for p in platforms
133
- ]
134
- except (yaml.YAMLError, OSError, KeyError, TypeError, AttributeError):
135
- # Gracefully handle malformed .kitchen.yml - continue with empty config
136
- # Catches: YAML syntax errors, file I/O errors, missing config keys,
137
- # type mismatches in config structure, and missing dict attributes
138
- pass
139
-
140
- # Check for ChefSpec
141
- spec_dir = cookbook_dir / "spec"
142
- if spec_dir.exists() and any(spec_dir.glob("**/*_spec.rb")):
143
- patterns["has_chefspec"] = True
144
-
145
- # Check for Cookstyle
146
- cookstyle_yml = cookbook_dir / ".cookstyle.yml"
147
- if cookstyle_yml.exists():
148
- patterns["has_cookstyle"] = True
149
-
150
- # Check for Foodcritic (legacy)
151
- if (cookbook_dir / ".foodcritic").exists():
152
- patterns["has_foodcritic"] = True
153
-
154
- return patterns
155
-
156
-
157
68
  def _build_workflow_structure(
158
69
  workflow_name: str,
159
70
  patterns: dict[str, Any],
souschef/ci/gitlab_ci.py CHANGED
@@ -1,9 +1,8 @@
1
1
  """GitLab CI generation from Chef CI/CD patterns."""
2
2
 
3
- from pathlib import Path
4
3
  from typing import Any
5
4
 
6
- import yaml
5
+ from souschef.ci.common import analyse_chef_ci_patterns
7
6
 
8
7
 
9
8
  def generate_gitlab_ci_from_chef_ci(
@@ -27,8 +26,8 @@ def generate_gitlab_ci_from_chef_ci(
27
26
  GitLab CI YAML content.
28
27
 
29
28
  """
30
- # Analyze Chef CI patterns
31
- ci_patterns = _analyze_chef_ci_patterns(cookbook_path)
29
+ # Analyse Chef CI patterns
30
+ ci_patterns = analyse_chef_ci_patterns(cookbook_path)
32
31
 
33
32
  # Generate CI configuration
34
33
  return _generate_gitlab_ci_yaml(
@@ -36,55 +35,6 @@ def generate_gitlab_ci_from_chef_ci(
36
35
  )
37
36
 
38
37
 
39
- def _analyze_chef_ci_patterns(cookbook_path: str) -> dict[str, Any]:
40
- """
41
- Analyze Chef cookbook for CI/CD patterns.
42
-
43
- Args:
44
- cookbook_path: Path to Chef cookbook.
45
-
46
- Returns:
47
- Dictionary of detected CI patterns.
48
-
49
- """
50
- base_path = Path(cookbook_path)
51
-
52
- patterns: dict[str, Any] = {
53
- "has_kitchen": (base_path / ".kitchen.yml").exists(),
54
- "has_chefspec": (base_path / "spec").exists(),
55
- "has_inspec": (base_path / "test" / "integration").exists(),
56
- "has_berksfile": (base_path / "Berksfile").exists(),
57
- "lint_tools": [],
58
- "test_suites": [],
59
- }
60
-
61
- # Detect linting tools
62
- lint_tools: list[str] = patterns["lint_tools"]
63
- if (base_path / ".foodcritic").exists():
64
- lint_tools.append("foodcritic")
65
- if (base_path / ".cookstyle.yml").exists():
66
- lint_tools.append("cookstyle")
67
-
68
- # Parse kitchen.yml for test suites
69
- kitchen_file = base_path / ".kitchen.yml"
70
- if kitchen_file.exists():
71
- try:
72
- test_suites: list[str] = patterns["test_suites"]
73
- with kitchen_file.open() as f:
74
- kitchen_config = yaml.safe_load(f)
75
- if kitchen_config and "suites" in kitchen_config:
76
- test_suites.extend(
77
- suite["name"] for suite in kitchen_config["suites"]
78
- )
79
- except (yaml.YAMLError, OSError, KeyError, TypeError, AttributeError):
80
- # Gracefully handle malformed .kitchen.yml - continue with empty
81
- # test suites. Catches: YAML syntax errors, file I/O errors,
82
- # missing config keys, type mismatches
83
- pass
84
-
85
- return patterns
86
-
87
-
88
38
  def _build_lint_jobs(ci_patterns: dict[str, Any], enable_artifacts: bool) -> list[str]:
89
39
  """Build lint job configurations."""
90
40
  jobs = []
@@ -1,9 +1,8 @@
1
1
  """Jenkins pipeline generation from Chef CI/CD patterns."""
2
2
 
3
- from pathlib import Path
4
3
  from typing import Any
5
4
 
6
- import yaml
5
+ from souschef.ci.common import analyse_chef_ci_patterns
7
6
 
8
7
 
9
8
  def generate_jenkinsfile_from_chef_ci(
@@ -28,8 +27,8 @@ def generate_jenkinsfile_from_chef_ci(
28
27
  Jenkinsfile content (Groovy DSL).
29
28
 
30
29
  """
31
- # Analyze Chef CI patterns
32
- ci_patterns = _analyze_chef_ci_patterns(cookbook_path)
30
+ # Analyse Chef CI patterns
31
+ ci_patterns = analyse_chef_ci_patterns(cookbook_path)
33
32
 
34
33
  if pipeline_type == "declarative":
35
34
  return _generate_declarative_pipeline(
@@ -39,62 +38,6 @@ def generate_jenkinsfile_from_chef_ci(
39
38
  return _generate_scripted_pipeline(pipeline_name, enable_parallel)
40
39
 
41
40
 
42
- def _analyze_chef_ci_patterns(cookbook_path: str) -> dict[str, Any]:
43
- """
44
- Analyze Chef cookbook for CI/CD patterns.
45
-
46
- Detects:
47
- - Test Kitchen configuration (.kitchen.yml)
48
- - ChefSpec tests (spec/)
49
- - InSpec tests (test/integration/)
50
- - Foodcritic/Cookstyle linting
51
- - Berksfile dependencies
52
-
53
- Args:
54
- cookbook_path: Path to Chef cookbook.
55
-
56
- Returns:
57
- Dictionary of detected CI patterns.
58
-
59
- """
60
- base_path = Path(cookbook_path)
61
-
62
- patterns: dict[str, Any] = {
63
- "has_kitchen": (base_path / ".kitchen.yml").exists(),
64
- "has_chefspec": (base_path / "spec").exists(),
65
- "has_inspec": (base_path / "test" / "integration").exists(),
66
- "has_berksfile": (base_path / "Berksfile").exists(),
67
- "lint_tools": [],
68
- "test_suites": [],
69
- }
70
-
71
- # Detect linting tools
72
- lint_tools: list[str] = patterns["lint_tools"]
73
- if (base_path / ".foodcritic").exists():
74
- lint_tools.append("foodcritic")
75
- if (base_path / ".cookstyle.yml").exists():
76
- lint_tools.append("cookstyle")
77
-
78
- # Parse kitchen.yml for test suites
79
- kitchen_file = base_path / ".kitchen.yml"
80
- if kitchen_file.exists():
81
- try:
82
- test_suites: list[str] = patterns["test_suites"]
83
- with kitchen_file.open() as f:
84
- kitchen_config = yaml.safe_load(f)
85
- if kitchen_config and "suites" in kitchen_config:
86
- test_suites.extend(
87
- suite["name"] for suite in kitchen_config["suites"]
88
- )
89
- except (yaml.YAMLError, OSError, KeyError, TypeError, AttributeError):
90
- # Gracefully handle malformed .kitchen.yml - continue with empty config
91
- # Catches: YAML syntax errors, file I/O errors, missing config keys,
92
- # type mismatches in config structure, and missing dict attributes
93
- pass
94
-
95
- return patterns
96
-
97
-
98
41
  def _create_lint_stage(ci_patterns: dict[str, Any]) -> str | None:
99
42
  """Create lint stage if lint tools are detected."""
100
43
  if not ci_patterns.get("lint_tools"):
souschef/cli.py CHANGED
@@ -11,7 +11,9 @@ from typing import NoReturn
11
11
 
12
12
  import click
13
13
 
14
+ from souschef import __version__
14
15
  from souschef.converters.playbook import generate_playbook_from_recipe
16
+ from souschef.core.path_utils import _normalize_path
15
17
  from souschef.profiling import (
16
18
  generate_cookbook_performance_report,
17
19
  profile_function,
@@ -40,8 +42,20 @@ CI_JOB_UNIT_TESTS = " • Unit Tests (ChefSpec)"
40
42
  CI_JOB_INTEGRATION_TESTS = " • Integration Tests (Test Kitchen)"
41
43
 
42
44
 
45
+ def _resolve_output_path(output: str | None, default_path: Path) -> Path:
46
+ """Normalise and validate output paths for generated files."""
47
+ try:
48
+ resolved_path = _normalize_path(output) if output else default_path.resolve()
49
+ except ValueError as exc: # noqa: TRY003
50
+ click.echo(f"Invalid output path: {exc}", err=True)
51
+ raise click.Abort() from exc
52
+
53
+ resolved_path.parent.mkdir(parents=True, exist_ok=True)
54
+ return resolved_path
55
+
56
+
43
57
  @click.group()
44
- @click.version_option(version="0.1.0", prog_name="souschef")
58
+ @click.version_option(version=__version__, prog_name="souschef")
45
59
  def cli() -> None:
46
60
  """
47
61
  SousChef - Chef to Ansible conversion toolkit.
@@ -281,16 +295,16 @@ def _display_template_summary(template_file: Path) -> None:
281
295
  @click.option("--dry-run", is_flag=True, help="Show what would be done")
282
296
  def cookbook(cookbook_path: str, output: str | None, dry_run: bool) -> None:
283
297
  """
284
- Analyze an entire Chef cookbook.
298
+ Analyse an entire Chef cookbook.
285
299
 
286
300
  COOKBOOK_PATH: Path to the cookbook root directory
287
301
 
288
- This command analyzes the cookbook structure, metadata, recipes,
302
+ This command analyses the cookbook structure, metadata, recipes,
289
303
  attributes, templates, and custom resources.
290
304
  """
291
305
  cookbook_dir = Path(cookbook_path)
292
306
 
293
- click.echo(f"Analyzing cookbook: {cookbook_dir.name}")
307
+ click.echo(f"Analysing cookbook: {cookbook_dir.name}")
294
308
  click.echo("=" * 50)
295
309
 
296
310
  # Parse metadata
@@ -418,7 +432,7 @@ def generate_jenkinsfile(
418
432
 
419
433
  COOKBOOK_PATH: Path to the Chef cookbook root directory
420
434
 
421
- This command analyzes the cookbook for CI patterns (Test Kitchen,
435
+ This command analyses the cookbook for CI patterns (Test Kitchen,
422
436
  lint tools, test suites) and generates an appropriate Jenkinsfile
423
437
  with stages for linting, testing, and convergence.
424
438
 
@@ -433,14 +447,17 @@ def generate_jenkinsfile(
433
447
 
434
448
  """
435
449
  try:
450
+ safe_cookbook_path = str(_normalize_path(cookbook_path))
436
451
  result = generate_jenkinsfile_from_chef(
437
- cookbook_path=cookbook_path,
452
+ cookbook_path=safe_cookbook_path,
438
453
  pipeline_type=pipeline_type,
439
454
  enable_parallel="yes" if parallel else "no",
440
455
  )
441
456
 
442
457
  # Determine output path
443
- output_path = Path(output) if output else Path.cwd() / "Jenkinsfile"
458
+ output_path = _resolve_output_path(
459
+ output, default_path=Path.cwd() / "Jenkinsfile"
460
+ )
444
461
 
445
462
  # Write Jenkinsfile
446
463
  output_path.write_text(result)
@@ -495,7 +512,7 @@ def generate_gitlab_ci(
495
512
 
496
513
  COOKBOOK_PATH: Path to the Chef cookbook root directory
497
514
 
498
- This command analyzes the cookbook for CI patterns (Test Kitchen,
515
+ This command analyses the cookbook for CI patterns (Test Kitchen,
499
516
  lint tools, test suites) and generates an appropriate GitLab CI
500
517
  configuration with jobs for linting, testing, and convergence.
501
518
 
@@ -510,14 +527,17 @@ def generate_gitlab_ci(
510
527
 
511
528
  """
512
529
  try:
530
+ safe_cookbook_path = str(_normalize_path(cookbook_path))
513
531
  result = generate_gitlab_ci_from_chef(
514
- cookbook_path=cookbook_path,
532
+ cookbook_path=safe_cookbook_path,
515
533
  enable_cache="yes" if cache else "no",
516
534
  enable_artifacts="yes" if artifacts else "no",
517
535
  )
518
536
 
519
537
  # Determine output path
520
- output_path = Path(output) if output else Path.cwd() / ".gitlab-ci.yml"
538
+ output_path = _resolve_output_path(
539
+ output, default_path=Path.cwd() / ".gitlab-ci.yml"
540
+ )
521
541
 
522
542
  # Write GitLab CI config
523
543
  output_path.write_text(result)
@@ -575,7 +595,7 @@ def generate_github_workflow(
575
595
 
576
596
  COOKBOOK_PATH: Path to the Chef cookbook root directory
577
597
 
578
- This command analyzes the cookbook for CI patterns (Test Kitchen,
598
+ This command analyses the cookbook for CI patterns (Test Kitchen,
579
599
  lint tools, test suites) and generates an appropriate GitHub Actions
580
600
  workflow with jobs for linting, testing, and convergence.
581
601
 
@@ -590,8 +610,9 @@ def generate_github_workflow(
590
610
 
591
611
  """
592
612
  try:
613
+ safe_cookbook_path = str(_normalize_path(cookbook_path))
593
614
  result = generate_github_workflow_from_chef(
594
- cookbook_path=cookbook_path,
615
+ cookbook_path=safe_cookbook_path,
595
616
  workflow_name=workflow_name,
596
617
  enable_cache="yes" if cache else "no",
597
618
  enable_artifacts="yes" if artifacts else "no",
@@ -599,11 +620,11 @@ def generate_github_workflow(
599
620
 
600
621
  # Determine output path
601
622
  if output:
602
- output_path = Path(output)
623
+ output_path = _resolve_output_path(output, Path.cwd() / "ci.yml")
603
624
  else:
604
625
  workflows_dir = Path.cwd() / ".github" / "workflows"
605
626
  workflows_dir.mkdir(parents=True, exist_ok=True)
606
- output_path = workflows_dir / "ci.yml"
627
+ output_path = _resolve_output_path(None, workflows_dir / "ci.yml")
607
628
 
608
629
  # Write workflow file
609
630
  output_path.write_text(result)
@@ -687,7 +708,7 @@ def profile(cookbook_path: str, output: str | None) -> None:
687
708
 
688
709
  COOKBOOK_PATH: Path to the Chef cookbook to profile
689
710
 
690
- This command analyzes the performance of parsing all cookbook components
711
+ This command analyses the performance of parsing all cookbook components
691
712
  (recipes, attributes, resources, templates) and provides recommendations
692
713
  for optimization.
693
714
  """
@@ -850,7 +871,7 @@ def assess_cookbook(cookbook_path: str, output_format: str) -> None:
850
871
  """
851
872
  Assess a Chef cookbook for migration complexity.
852
873
 
853
- Analyzes the cookbook and provides complexity level, recipe/resource counts,
874
+ Analyses the cookbook and provides complexity level, recipe/resource counts,
854
875
  estimated migration effort, and recommendations. Used by Terraform provider.
855
876
 
856
877
  Example:
@@ -870,8 +891,8 @@ def assess_cookbook(cookbook_path: str, output_format: str) -> None:
870
891
  click.echo(f"Error: {cookbook_path} is not a directory", err=True)
871
892
  sys.exit(1)
872
893
 
873
- # Analyze cookbook
874
- analysis = _analyze_cookbook_for_assessment(cookbook_dir)
894
+ # Analyse cookbook
895
+ analysis = _analyse_cookbook_for_assessment(cookbook_dir)
875
896
 
876
897
  if output_format == "json":
877
898
  click.echo(json.dumps(analysis))
@@ -883,8 +904,8 @@ def assess_cookbook(cookbook_path: str, output_format: str) -> None:
883
904
  sys.exit(1)
884
905
 
885
906
 
886
- def _analyze_cookbook_for_assessment(cookbook_dir: Path) -> dict:
887
- """Analyze cookbook and return assessment data."""
907
+ def _analyse_cookbook_for_assessment(cookbook_dir: Path) -> dict:
908
+ """Analyse cookbook and return assessment data."""
888
909
  recipe_count = 0
889
910
  resource_count = 0
890
911
  recipes_dir = cookbook_dir / "recipes"
@@ -1077,6 +1098,94 @@ def convert_inspec(profile_path: str, output_path: str, output_format: str) -> N
1077
1098
  sys.exit(1)
1078
1099
 
1079
1100
 
1101
+ @cli.command("convert-cookbook")
1102
+ @click.option(
1103
+ "--cookbook-path",
1104
+ required=True,
1105
+ type=click.Path(exists=True),
1106
+ help="Path to the Chef cookbook directory",
1107
+ )
1108
+ @click.option(
1109
+ "--output-path",
1110
+ required=True,
1111
+ type=click.Path(),
1112
+ help="Directory where the Ansible role will be created",
1113
+ )
1114
+ @click.option(
1115
+ "--assessment-file",
1116
+ type=click.Path(exists=True),
1117
+ help="Path to JSON file with assessment results for optimization",
1118
+ )
1119
+ @click.option(
1120
+ "--role-name",
1121
+ help="Name for the Ansible role (defaults to cookbook name)",
1122
+ )
1123
+ @click.option(
1124
+ "--skip-templates",
1125
+ is_flag=True,
1126
+ help="Skip conversion of ERB templates to Jinja2",
1127
+ )
1128
+ @click.option(
1129
+ "--skip-attributes",
1130
+ is_flag=True,
1131
+ help="Skip conversion of attributes to Ansible variables",
1132
+ )
1133
+ @click.option(
1134
+ "--skip-recipes",
1135
+ is_flag=True,
1136
+ help="Skip conversion of recipes to Ansible tasks",
1137
+ )
1138
+ def convert_cookbook(
1139
+ cookbook_path: str,
1140
+ output_path: str,
1141
+ assessment_file: str | None = None,
1142
+ role_name: str | None = None,
1143
+ skip_templates: bool = False,
1144
+ skip_attributes: bool = False,
1145
+ skip_recipes: bool = False,
1146
+ ) -> None:
1147
+ r"""
1148
+ Convert an entire Chef cookbook to a complete Ansible role.
1149
+
1150
+ Performs comprehensive conversion including recipes, templates, attributes,
1151
+ and proper Ansible role structure. Can use assessment data for optimization.
1152
+
1153
+ Example:
1154
+ souschef convert-cookbook --cookbook-path /chef/cookbooks/nginx \\
1155
+ --output-path /ansible/roles \\
1156
+ --assessment-file assessment.json
1157
+
1158
+ """
1159
+ try:
1160
+ # Load assessment data if provided
1161
+ assessment_data = ""
1162
+ if assessment_file:
1163
+ assessment_path = Path(assessment_file)
1164
+ if assessment_path.exists():
1165
+ assessment_data = assessment_path.read_text()
1166
+ else:
1167
+ click.echo(f"Warning: Assessment file not found: {assessment_file}")
1168
+
1169
+ # Call server function
1170
+ from souschef.server import convert_cookbook_comprehensive
1171
+
1172
+ result = convert_cookbook_comprehensive(
1173
+ cookbook_path=cookbook_path,
1174
+ output_path=output_path,
1175
+ assessment_data=assessment_data,
1176
+ include_templates=not skip_templates,
1177
+ include_attributes=not skip_attributes,
1178
+ include_recipes=not skip_recipes,
1179
+ role_name=role_name or "",
1180
+ )
1181
+
1182
+ click.echo(result)
1183
+
1184
+ except Exception as e:
1185
+ click.echo(f"Error converting cookbook: {e}", err=True)
1186
+ sys.exit(1)
1187
+
1188
+
1080
1189
  @cli.command()
1081
1190
  @click.option("--port", default=8501, help="Port to run the Streamlit app on")
1082
1191
  def ui(port: int) -> None: