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.
- {mcp_souschef-2.5.3.dist-info → mcp_souschef-3.0.0.dist-info}/METADATA +135 -28
- mcp_souschef-3.0.0.dist-info/RECORD +46 -0
- {mcp_souschef-2.5.3.dist-info → mcp_souschef-3.0.0.dist-info}/WHEEL +1 -1
- souschef/__init__.py +43 -3
- souschef/assessment.py +1260 -69
- souschef/ci/common.py +126 -0
- souschef/ci/github_actions.py +4 -93
- souschef/ci/gitlab_ci.py +3 -53
- souschef/ci/jenkins_pipeline.py +3 -60
- souschef/cli.py +129 -20
- souschef/converters/__init__.py +2 -2
- souschef/converters/cookbook_specific.py +125 -0
- souschef/converters/cookbook_specific.py.backup +109 -0
- souschef/converters/playbook.py +1022 -15
- souschef/converters/resource.py +113 -10
- souschef/converters/template.py +177 -0
- souschef/core/constants.py +13 -0
- souschef/core/metrics.py +313 -0
- souschef/core/path_utils.py +12 -9
- souschef/core/validation.py +53 -0
- souschef/deployment.py +85 -33
- souschef/parsers/attributes.py +397 -32
- souschef/parsers/recipe.py +48 -10
- souschef/server.py +715 -37
- souschef/ui/app.py +1658 -379
- souschef/ui/health_check.py +36 -0
- souschef/ui/pages/ai_settings.py +563 -0
- souschef/ui/pages/cookbook_analysis.py +3270 -166
- souschef/ui/pages/validation_reports.py +274 -0
- mcp_souschef-2.5.3.dist-info/RECORD +0 -38
- {mcp_souschef-2.5.3.dist-info → mcp_souschef-3.0.0.dist-info}/entry_points.txt +0 -0
- {mcp_souschef-2.5.3.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"
|
|
@@ -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
|
-
#
|
|
56
|
-
patterns =
|
|
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
|
|
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
|
-
#
|
|
31
|
-
ci_patterns =
|
|
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 = []
|
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(
|
|
@@ -28,8 +27,8 @@ def generate_jenkinsfile_from_chef_ci(
|
|
|
28
27
|
Jenkinsfile content (Groovy DSL).
|
|
29
28
|
|
|
30
29
|
"""
|
|
31
|
-
#
|
|
32
|
-
ci_patterns =
|
|
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=
|
|
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
|
-
|
|
298
|
+
Analyse an entire Chef cookbook.
|
|
285
299
|
|
|
286
300
|
COOKBOOK_PATH: Path to the cookbook root directory
|
|
287
301
|
|
|
288
|
-
This command
|
|
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"
|
|
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
|
|
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=
|
|
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)
|
|
@@ -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
|
|
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=
|
|
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)
|
|
@@ -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
|
|
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=
|
|
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)
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
#
|
|
874
|
-
analysis =
|
|
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
|
|
887
|
-
"""
|
|
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:
|