mcp-souschef 2.8.0__py3-none-any.whl → 3.2.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.2.0.dist-info}/METADATA +159 -384
- mcp_souschef-3.2.0.dist-info/RECORD +47 -0
- {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.2.0.dist-info}/WHEEL +1 -1
- souschef/__init__.py +31 -7
- souschef/assessment.py +1451 -105
- 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 +149 -16
- souschef/converters/playbook.py +378 -138
- souschef/converters/resource.py +12 -11
- souschef/converters/template.py +177 -0
- souschef/core/__init__.py +6 -1
- souschef/core/metrics.py +313 -0
- souschef/core/path_utils.py +233 -19
- souschef/core/validation.py +53 -0
- souschef/deployment.py +71 -12
- souschef/generators/__init__.py +13 -0
- souschef/generators/repo.py +695 -0
- souschef/parsers/attributes.py +1 -1
- souschef/parsers/habitat.py +1 -1
- souschef/parsers/inspec.py +25 -2
- souschef/parsers/metadata.py +5 -3
- souschef/parsers/recipe.py +1 -1
- souschef/parsers/resource.py +1 -1
- souschef/parsers/template.py +1 -1
- souschef/server.py +1039 -121
- souschef/ui/app.py +486 -374
- souschef/ui/pages/ai_settings.py +74 -8
- souschef/ui/pages/cookbook_analysis.py +3216 -373
- souschef/ui/pages/validation_reports.py +274 -0
- mcp_souschef-2.8.0.dist-info/RECORD +0 -42
- souschef/converters/cookbook_specific.py.backup +0 -109
- {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.2.0.dist-info}/entry_points.txt +0 -0
- {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.2.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: # nosonar
|
|
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,47 @@ 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
|
+
|
|
57
|
+
def _safe_write_file(content: str, output: str | None, default_path: Path) -> Path:
|
|
58
|
+
"""
|
|
59
|
+
Safely write content to a validated file path.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
content: Content to write to file.
|
|
63
|
+
output: Optional user-specified output path.
|
|
64
|
+
default_path: Default path if output not specified.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The path where content was written.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
click.Abort: If path validation or write fails.
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
validated_path = _resolve_output_path(output, default_path)
|
|
74
|
+
try:
|
|
75
|
+
# Separate validation from write to satisfy SonarQube path construction rules
|
|
76
|
+
with validated_path.open("w", encoding="utf-8") as f:
|
|
77
|
+
f.write(content)
|
|
78
|
+
except OSError as e:
|
|
79
|
+
click.echo(f"Error writing file: {e}", err=True)
|
|
80
|
+
raise click.Abort() from e
|
|
81
|
+
return validated_path
|
|
82
|
+
|
|
83
|
+
|
|
43
84
|
@click.group()
|
|
44
|
-
@click.version_option(version=
|
|
85
|
+
@click.version_option(version=__version__, prog_name="souschef")
|
|
45
86
|
def cli() -> None:
|
|
46
87
|
"""
|
|
47
88
|
SousChef - Chef to Ansible conversion toolkit.
|
|
@@ -433,18 +474,21 @@ def generate_jenkinsfile(
|
|
|
433
474
|
|
|
434
475
|
"""
|
|
435
476
|
try:
|
|
477
|
+
safe_cookbook_path = str(_normalize_path(cookbook_path))
|
|
436
478
|
result = generate_jenkinsfile_from_chef(
|
|
437
|
-
cookbook_path=
|
|
479
|
+
cookbook_path=safe_cookbook_path,
|
|
438
480
|
pipeline_type=pipeline_type,
|
|
439
481
|
enable_parallel="yes" if parallel else "no",
|
|
440
482
|
)
|
|
441
483
|
|
|
442
484
|
# Determine output path
|
|
443
|
-
|
|
485
|
+
_resolve_output_path(output, default_path=Path.cwd() / "Jenkinsfile")
|
|
444
486
|
|
|
445
|
-
# Write Jenkinsfile
|
|
446
|
-
|
|
447
|
-
|
|
487
|
+
# Write Jenkinsfile using safe write helper
|
|
488
|
+
written_path = _safe_write_file(
|
|
489
|
+
result, output, default_path=Path.cwd() / "Jenkinsfile"
|
|
490
|
+
)
|
|
491
|
+
click.echo(f"✓ Generated {pipeline_type} Jenkinsfile: {written_path}")
|
|
448
492
|
|
|
449
493
|
# Show summary
|
|
450
494
|
click.echo("\nGenerated Pipeline Stages:")
|
|
@@ -510,18 +554,18 @@ def generate_gitlab_ci(
|
|
|
510
554
|
|
|
511
555
|
"""
|
|
512
556
|
try:
|
|
557
|
+
safe_cookbook_path = str(_normalize_path(cookbook_path))
|
|
513
558
|
result = generate_gitlab_ci_from_chef(
|
|
514
|
-
cookbook_path=
|
|
559
|
+
cookbook_path=safe_cookbook_path,
|
|
515
560
|
enable_cache="yes" if cache else "no",
|
|
516
561
|
enable_artifacts="yes" if artifacts else "no",
|
|
517
562
|
)
|
|
518
563
|
|
|
519
|
-
#
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
click.echo(f"✓ Generated GitLab CI configuration: {output_path}")
|
|
564
|
+
# Write GitLab CI config using safe write helper
|
|
565
|
+
written_path = _safe_write_file(
|
|
566
|
+
result, output, default_path=Path.cwd() / ".gitlab-ci.yml"
|
|
567
|
+
)
|
|
568
|
+
click.echo(f"✓ Generated GitLab CI configuration: {written_path}")
|
|
525
569
|
|
|
526
570
|
# Show summary
|
|
527
571
|
click.echo("\nGenerated CI Jobs:")
|
|
@@ -590,8 +634,9 @@ def generate_github_workflow(
|
|
|
590
634
|
|
|
591
635
|
"""
|
|
592
636
|
try:
|
|
637
|
+
safe_cookbook_path = str(_normalize_path(cookbook_path))
|
|
593
638
|
result = generate_github_workflow_from_chef(
|
|
594
|
-
cookbook_path=
|
|
639
|
+
cookbook_path=safe_cookbook_path,
|
|
595
640
|
workflow_name=workflow_name,
|
|
596
641
|
enable_cache="yes" if cache else "no",
|
|
597
642
|
enable_artifacts="yes" if artifacts else "no",
|
|
@@ -599,11 +644,11 @@ def generate_github_workflow(
|
|
|
599
644
|
|
|
600
645
|
# Determine output path
|
|
601
646
|
if output:
|
|
602
|
-
output_path = Path(
|
|
647
|
+
output_path = _resolve_output_path(output, Path.cwd() / "ci.yml")
|
|
603
648
|
else:
|
|
604
649
|
workflows_dir = Path.cwd() / ".github" / "workflows"
|
|
605
650
|
workflows_dir.mkdir(parents=True, exist_ok=True)
|
|
606
|
-
output_path = workflows_dir / "ci.yml"
|
|
651
|
+
output_path = _resolve_output_path(None, workflows_dir / "ci.yml")
|
|
607
652
|
|
|
608
653
|
# Write workflow file
|
|
609
654
|
output_path.write_text(result)
|
|
@@ -1077,6 +1122,94 @@ def convert_inspec(profile_path: str, output_path: str, output_format: str) -> N
|
|
|
1077
1122
|
sys.exit(1)
|
|
1078
1123
|
|
|
1079
1124
|
|
|
1125
|
+
@cli.command("convert-cookbook")
|
|
1126
|
+
@click.option(
|
|
1127
|
+
"--cookbook-path",
|
|
1128
|
+
required=True,
|
|
1129
|
+
type=click.Path(exists=True),
|
|
1130
|
+
help="Path to the Chef cookbook directory",
|
|
1131
|
+
)
|
|
1132
|
+
@click.option(
|
|
1133
|
+
"--output-path",
|
|
1134
|
+
required=True,
|
|
1135
|
+
type=click.Path(),
|
|
1136
|
+
help="Directory where the Ansible role will be created",
|
|
1137
|
+
)
|
|
1138
|
+
@click.option(
|
|
1139
|
+
"--assessment-file",
|
|
1140
|
+
type=click.Path(exists=True),
|
|
1141
|
+
help="Path to JSON file with assessment results for optimization",
|
|
1142
|
+
)
|
|
1143
|
+
@click.option(
|
|
1144
|
+
"--role-name",
|
|
1145
|
+
help="Name for the Ansible role (defaults to cookbook name)",
|
|
1146
|
+
)
|
|
1147
|
+
@click.option(
|
|
1148
|
+
"--skip-templates",
|
|
1149
|
+
is_flag=True,
|
|
1150
|
+
help="Skip conversion of ERB templates to Jinja2",
|
|
1151
|
+
)
|
|
1152
|
+
@click.option(
|
|
1153
|
+
"--skip-attributes",
|
|
1154
|
+
is_flag=True,
|
|
1155
|
+
help="Skip conversion of attributes to Ansible variables",
|
|
1156
|
+
)
|
|
1157
|
+
@click.option(
|
|
1158
|
+
"--skip-recipes",
|
|
1159
|
+
is_flag=True,
|
|
1160
|
+
help="Skip conversion of recipes to Ansible tasks",
|
|
1161
|
+
)
|
|
1162
|
+
def convert_cookbook(
|
|
1163
|
+
cookbook_path: str,
|
|
1164
|
+
output_path: str,
|
|
1165
|
+
assessment_file: str | None = None,
|
|
1166
|
+
role_name: str | None = None,
|
|
1167
|
+
skip_templates: bool = False,
|
|
1168
|
+
skip_attributes: bool = False,
|
|
1169
|
+
skip_recipes: bool = False,
|
|
1170
|
+
) -> None:
|
|
1171
|
+
r"""
|
|
1172
|
+
Convert an entire Chef cookbook to a complete Ansible role.
|
|
1173
|
+
|
|
1174
|
+
Performs comprehensive conversion including recipes, templates, attributes,
|
|
1175
|
+
and proper Ansible role structure. Can use assessment data for optimization.
|
|
1176
|
+
|
|
1177
|
+
Example:
|
|
1178
|
+
souschef convert-cookbook --cookbook-path /chef/cookbooks/nginx \\
|
|
1179
|
+
--output-path /ansible/roles \\
|
|
1180
|
+
--assessment-file assessment.json
|
|
1181
|
+
|
|
1182
|
+
"""
|
|
1183
|
+
try:
|
|
1184
|
+
# Load assessment data if provided
|
|
1185
|
+
assessment_data = ""
|
|
1186
|
+
if assessment_file:
|
|
1187
|
+
assessment_path = Path(assessment_file)
|
|
1188
|
+
if assessment_path.exists():
|
|
1189
|
+
assessment_data = assessment_path.read_text()
|
|
1190
|
+
else:
|
|
1191
|
+
click.echo(f"Warning: Assessment file not found: {assessment_file}")
|
|
1192
|
+
|
|
1193
|
+
# Call server function
|
|
1194
|
+
from souschef.server import convert_cookbook_comprehensive
|
|
1195
|
+
|
|
1196
|
+
result = convert_cookbook_comprehensive(
|
|
1197
|
+
cookbook_path=cookbook_path,
|
|
1198
|
+
output_path=output_path,
|
|
1199
|
+
assessment_data=assessment_data,
|
|
1200
|
+
include_templates=not skip_templates,
|
|
1201
|
+
include_attributes=not skip_attributes,
|
|
1202
|
+
include_recipes=not skip_recipes,
|
|
1203
|
+
role_name=role_name or "",
|
|
1204
|
+
)
|
|
1205
|
+
|
|
1206
|
+
click.echo(result)
|
|
1207
|
+
|
|
1208
|
+
except Exception as e:
|
|
1209
|
+
click.echo(f"Error converting cookbook: {e}", err=True)
|
|
1210
|
+
sys.exit(1)
|
|
1211
|
+
|
|
1212
|
+
|
|
1080
1213
|
@cli.command()
|
|
1081
1214
|
@click.option("--port", default=8501, help="Port to run the Streamlit app on")
|
|
1082
1215
|
def ui(port: int) -> None:
|