mcp-souschef 2.1.2__py3-none-any.whl → 2.5.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,343 @@
1
+ """Jenkins pipeline generation from Chef CI/CD patterns."""
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import yaml
7
+
8
+
9
+ def generate_jenkinsfile_from_chef_ci(
10
+ cookbook_path: str,
11
+ pipeline_name: str,
12
+ pipeline_type: str = "declarative",
13
+ enable_parallel: bool = True,
14
+ ) -> str:
15
+ """
16
+ Generate Jenkinsfile from Chef cookbook CI/CD patterns.
17
+
18
+ Analyzes Chef testing tools (kitchen-ci, foodcritic, cookstyle, chefspec)
19
+ and generates equivalent Jenkins pipeline stages.
20
+
21
+ Args:
22
+ cookbook_path: Path to Chef cookbook.
23
+ pipeline_name: Name for the Jenkins pipeline.
24
+ pipeline_type: 'declarative' or 'scripted'.
25
+ enable_parallel: Enable parallel execution of test stages.
26
+
27
+ Returns:
28
+ Jenkinsfile content (Groovy DSL).
29
+
30
+ """
31
+ # Analyze Chef CI patterns
32
+ ci_patterns = _analyze_chef_ci_patterns(cookbook_path)
33
+
34
+ if pipeline_type == "declarative":
35
+ return _generate_declarative_pipeline(
36
+ pipeline_name, ci_patterns, enable_parallel
37
+ )
38
+ else:
39
+ return _generate_scripted_pipeline(pipeline_name, enable_parallel)
40
+
41
+
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
+ def _create_lint_stage(ci_patterns: dict[str, Any]) -> str | None:
99
+ """Create lint stage if lint tools are detected."""
100
+ if not ci_patterns.get("lint_tools"):
101
+ return None
102
+
103
+ lint_steps: list[str] = []
104
+ for tool in ci_patterns["lint_tools"]:
105
+ if tool == "cookstyle":
106
+ lint_steps.append("sh 'ansible-lint playbooks/'")
107
+ elif tool == "foodcritic":
108
+ lint_steps.append("sh 'yamllint -c .yamllint .'")
109
+
110
+ if not lint_steps:
111
+ return None
112
+
113
+ return _create_stage("Lint", lint_steps)
114
+
115
+
116
+ def _create_unit_test_stage(ci_patterns: dict[str, Any]) -> str | None:
117
+ """Create unit test stage if ChefSpec is detected."""
118
+ if not ci_patterns.get("has_chefspec"):
119
+ return None
120
+
121
+ return _create_stage(
122
+ "Unit Tests",
123
+ ["sh 'molecule test --scenario-name default'"],
124
+ )
125
+
126
+
127
+ def _create_integration_test_stage(ci_patterns: dict[str, Any]) -> str | None:
128
+ """Create integration test stage if Test Kitchen or InSpec is detected."""
129
+ if not (ci_patterns.get("has_kitchen") or ci_patterns.get("has_inspec")):
130
+ return None
131
+
132
+ test_steps = []
133
+ if ci_patterns.get("test_suites"):
134
+ for suite in ci_patterns["test_suites"]:
135
+ test_steps.append(f"sh 'molecule test --scenario-name {suite}'")
136
+ else:
137
+ test_steps.append("sh 'molecule test'")
138
+
139
+ return _create_stage("Integration Tests", test_steps)
140
+
141
+
142
+ def _create_deploy_stage() -> str:
143
+ """Create deploy stage."""
144
+ return _create_stage(
145
+ "Deploy",
146
+ [
147
+ (
148
+ "sh 'ansible-playbook -i inventory/production "
149
+ "playbooks/site.yml --check'"
150
+ ),
151
+ "input message: 'Deploy to production?', ok: 'Deploy'",
152
+ "sh 'ansible-playbook -i inventory/production playbooks/site.yml'",
153
+ ],
154
+ )
155
+
156
+
157
+ def _generate_declarative_pipeline(
158
+ pipeline_name: str, ci_patterns: dict[str, Any], enable_parallel: bool = True
159
+ ) -> str:
160
+ """
161
+ Generate Jenkins Declarative Pipeline.
162
+
163
+ Args:
164
+ pipeline_name: Pipeline name.
165
+ ci_patterns: Detected CI patterns.
166
+ enable_parallel: Enable parallel execution of test stages.
167
+
168
+ Returns:
169
+ Jenkinsfile with Declarative Pipeline syntax.
170
+
171
+ """
172
+ stages = []
173
+
174
+ # Collect test stages for potential parallel execution
175
+ test_stages = []
176
+
177
+ lint_stage = _create_lint_stage(ci_patterns)
178
+ if lint_stage:
179
+ test_stages.append(lint_stage)
180
+
181
+ unit_stage = _create_unit_test_stage(ci_patterns)
182
+ if unit_stage:
183
+ test_stages.append(unit_stage)
184
+
185
+ integration_stage = _create_integration_test_stage(ci_patterns)
186
+ if integration_stage:
187
+ test_stages.append(integration_stage)
188
+
189
+ # Add test stages (parallel or sequential based on enable_parallel)
190
+ if enable_parallel and len(test_stages) > 1:
191
+ # Wrap multiple test stages in parallel block
192
+ parallel_content = "\n".join(test_stages)
193
+ parallel_stage = f"""stage('Test') {{
194
+ parallel {{
195
+ {_indent_content(parallel_content, 16)}
196
+ }}
197
+ }}"""
198
+ stages.append(parallel_stage)
199
+ else:
200
+ # Execute stages sequentially
201
+ stages.extend(test_stages)
202
+
203
+ # Always add deploy stage (never parallelized)
204
+ stages.append(_create_deploy_stage())
205
+
206
+ # Build pipeline
207
+ stages_groovy = "\n\n".join(stages)
208
+
209
+ return f"""// Jenkinsfile: {pipeline_name}
210
+ // Generated from Chef cookbook CI/CD patterns
211
+ // Pipeline Type: Declarative
212
+
213
+ pipeline {{
214
+ agent any
215
+
216
+ options {{
217
+ timestamps()
218
+ ansiColor('xterm')
219
+ buildDiscarder(logRotator(numToKeepStr: '10'))
220
+ }}
221
+
222
+ environment {{
223
+ ANSIBLE_FORCE_COLOR = 'true'
224
+ ANSIBLE_HOST_KEY_CHECKING = 'false'
225
+ }}
226
+
227
+ stages {{
228
+ {_indent_content(stages_groovy, 8)}
229
+ }}
230
+
231
+ post {{
232
+ always {{
233
+ cleanWs()
234
+ }}
235
+ success {{
236
+ echo 'Pipeline succeeded!'
237
+ }}
238
+ failure {{
239
+ echo 'Pipeline failed!'
240
+ }}
241
+ }}
242
+ }}
243
+ """
244
+
245
+
246
+ def _generate_scripted_pipeline(
247
+ pipeline_name: str, enable_parallel: bool = True
248
+ ) -> str:
249
+ """
250
+ Generate Jenkins Scripted Pipeline.
251
+
252
+ Args:
253
+ pipeline_name: Pipeline name.
254
+ enable_parallel: Enable parallel execution of test stages.
255
+
256
+ Returns:
257
+ Jenkinsfile with Scripted Pipeline syntax.
258
+
259
+ """
260
+ if enable_parallel:
261
+ test_block = """ parallel(
262
+ lint: {{
263
+ stage('Lint') {{
264
+ sh 'ansible-lint playbooks/'
265
+ }}
266
+ }},
267
+ test: {{
268
+ stage('Test') {{
269
+ sh 'molecule test'
270
+ }}
271
+ }}
272
+ )"""
273
+ else:
274
+ test_block = """ stage('Lint') {{
275
+ sh 'ansible-lint playbooks/'
276
+ }}
277
+
278
+ stage('Test') {{
279
+ sh 'molecule test'
280
+ }}"""
281
+
282
+ return f"""// Jenkinsfile: {pipeline_name}
283
+ // Generated from Chef cookbook CI/CD patterns
284
+ // Pipeline Type: Scripted
285
+
286
+ node {{
287
+ try {{
288
+ stage('Checkout') {{
289
+ checkout scm
290
+ }}
291
+
292
+ {test_block}
293
+
294
+ stage('Deploy') {{
295
+ input message: 'Deploy to production?', ok: 'Deploy'
296
+ sh 'ansible-playbook -i inventory/production playbooks/site.yml'
297
+ }}
298
+ }} catch (Exception e) {{
299
+ currentBuild.result = 'FAILURE'
300
+ throw e
301
+ }} finally {{
302
+ cleanWs()
303
+ }}
304
+ }}
305
+ """
306
+
307
+
308
+ def _create_stage(name: str, steps: list[str]) -> str:
309
+ """
310
+ Create a Jenkins Declarative Pipeline stage.
311
+
312
+ Args:
313
+ name: Stage name.
314
+ steps: List of steps (shell commands or Jenkins DSL).
315
+
316
+ Returns:
317
+ Groovy stage block.
318
+
319
+ """
320
+ steps_formatted = "\n".join(f" {step}" for step in steps)
321
+ return f"""stage('{name}') {{
322
+ steps {{
323
+ {steps_formatted}
324
+ }}
325
+ }}"""
326
+
327
+
328
+ def _indent_content(content: str, spaces: int) -> str:
329
+ """
330
+ Indent multi-line content.
331
+
332
+ Args:
333
+ content: Content to indent.
334
+ spaces: Number of spaces to indent.
335
+
336
+ Returns:
337
+ Indented content.
338
+
339
+ """
340
+ indent = " " * spaces
341
+ return "\n".join(
342
+ indent + line if line.strip() else line for line in content.split("\n")
343
+ )