mcp-souschef 2.8.0__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.
- {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.0.0.dist-info}/METADATA +82 -10
- {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.0.0.dist-info}/RECORD +23 -19
- {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.0.0.dist-info}/WHEEL +1 -1
- souschef/__init__.py +37 -5
- souschef/assessment.py +1248 -57
- souschef/ci/common.py +126 -0
- souschef/ci/github_actions.py +3 -92
- souschef/ci/gitlab_ci.py +2 -52
- souschef/ci/jenkins_pipeline.py +2 -59
- souschef/cli.py +117 -8
- souschef/converters/playbook.py +259 -90
- souschef/converters/resource.py +12 -11
- souschef/converters/template.py +177 -0
- souschef/core/metrics.py +313 -0
- souschef/core/validation.py +53 -0
- souschef/deployment.py +61 -9
- souschef/server.py +680 -0
- souschef/ui/app.py +469 -351
- souschef/ui/pages/ai_settings.py +74 -8
- souschef/ui/pages/cookbook_analysis.py +2467 -298
- souschef/ui/pages/validation_reports.py +274 -0
- {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.0.0.dist-info}/entry_points.txt +0 -0
- {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.0.0.dist-info}/licenses/LICENSE +0 -0
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
|
souschef/ci/github_actions.py
CHANGED
|
@@ -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"
|
|
@@ -53,7 +55,7 @@ def generate_github_workflow_from_chef_ci(
|
|
|
53
55
|
raise FileNotFoundError(f"Cookbook directory not found: {cookbook_path}")
|
|
54
56
|
|
|
55
57
|
# Analyse Chef CI patterns
|
|
56
|
-
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 _analyse_chef_ci_patterns(cookbook_dir: Path) -> dict[str, Any]:
|
|
67
|
-
"""
|
|
68
|
-
Analyse 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 analyse.
|
|
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
|
|
5
|
+
from souschef.ci.common import analyse_chef_ci_patterns
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
def generate_gitlab_ci_from_chef_ci(
|
|
@@ -28,7 +27,7 @@ def generate_gitlab_ci_from_chef_ci(
|
|
|
28
27
|
|
|
29
28
|
"""
|
|
30
29
|
# Analyse Chef CI patterns
|
|
31
|
-
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 _analyse_chef_ci_patterns(cookbook_path: str) -> dict[str, Any]:
|
|
40
|
-
"""
|
|
41
|
-
Analyse 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 = []
|
souschef/ci/jenkins_pipeline.py
CHANGED
|
@@ -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
|
|
5
|
+
from souschef.ci.common import analyse_chef_ci_patterns
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
def generate_jenkinsfile_from_chef_ci(
|
|
@@ -29,7 +28,7 @@ def generate_jenkinsfile_from_chef_ci(
|
|
|
29
28
|
|
|
30
29
|
"""
|
|
31
30
|
# Analyse Chef CI patterns
|
|
32
|
-
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 _analyse_chef_ci_patterns(cookbook_path: str) -> dict[str, Any]:
|
|
43
|
-
"""
|
|
44
|
-
Analyse 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=
|
|
58
|
+
@click.version_option(version=__version__, prog_name="souschef")
|
|
45
59
|
def cli() -> None:
|
|
46
60
|
"""
|
|
47
61
|
SousChef - Chef to Ansible conversion toolkit.
|
|
@@ -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=
|
|
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 =
|
|
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)
|
|
@@ -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=
|
|
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 =
|
|
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)
|
|
@@ -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=
|
|
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(
|
|
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)
|
|
@@ -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:
|