mcp-souschef 2.2.0__py3-none-any.whl → 2.8.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.
@@ -0,0 +1,299 @@
1
+ """GitLab CI 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_gitlab_ci_from_chef_ci(
10
+ cookbook_path: str,
11
+ project_name: str,
12
+ enable_cache: bool = True,
13
+ enable_artifacts: bool = True,
14
+ ) -> str:
15
+ """
16
+ Generate .gitlab-ci.yml from Chef cookbook CI/CD patterns.
17
+
18
+ Analyzes Chef testing tools and generates equivalent GitLab CI stages.
19
+
20
+ Args:
21
+ cookbook_path: Path to Chef cookbook.
22
+ project_name: GitLab project name.
23
+ enable_cache: Enable caching for dependencies.
24
+ enable_artifacts: Enable artifacts for test results.
25
+
26
+ Returns:
27
+ GitLab CI YAML content.
28
+
29
+ """
30
+ # Analyse Chef CI patterns
31
+ ci_patterns = _analyse_chef_ci_patterns(cookbook_path)
32
+
33
+ # Generate CI configuration
34
+ return _generate_gitlab_ci_yaml(
35
+ project_name, ci_patterns, enable_cache, enable_artifacts
36
+ )
37
+
38
+
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
+ def _build_lint_jobs(ci_patterns: dict[str, Any], enable_artifacts: bool) -> list[str]:
89
+ """Build lint job configurations."""
90
+ jobs = []
91
+ if ci_patterns.get("lint_tools"):
92
+ lint_commands = []
93
+ for tool in ci_patterns["lint_tools"]:
94
+ if tool == "cookstyle":
95
+ lint_commands.append(" - ansible-lint playbooks/")
96
+ elif tool == "foodcritic":
97
+ lint_commands.append(" - yamllint -c .yamllint .")
98
+
99
+ if lint_commands:
100
+ jobs.append(
101
+ _create_gitlab_job(
102
+ "lint:ansible",
103
+ "lint",
104
+ lint_commands,
105
+ allow_failure=False,
106
+ artifacts=enable_artifacts,
107
+ )
108
+ )
109
+ return jobs
110
+
111
+
112
+ def _build_test_jobs(ci_patterns: dict[str, Any], enable_artifacts: bool) -> list[str]:
113
+ """Build test job configurations."""
114
+ jobs = []
115
+
116
+ # Unit test job (ChefSpec → Molecule)
117
+ if ci_patterns.get("has_chefspec"):
118
+ jobs.append(
119
+ _create_gitlab_job(
120
+ "test:unit",
121
+ "test",
122
+ [" - molecule test --scenario-name default"],
123
+ artifacts=enable_artifacts,
124
+ )
125
+ )
126
+
127
+ # Integration test jobs (Kitchen → Molecule)
128
+ if ci_patterns.get("has_kitchen") or ci_patterns.get("has_inspec"):
129
+ if ci_patterns.get("test_suites"):
130
+ for suite in ci_patterns["test_suites"]:
131
+ jobs.append(
132
+ _create_gitlab_job(
133
+ f"test:integration:{suite}",
134
+ "test",
135
+ [f" - molecule test --scenario-name {suite}"],
136
+ artifacts=enable_artifacts,
137
+ )
138
+ )
139
+ else:
140
+ jobs.append(
141
+ _create_gitlab_job(
142
+ "test:integration",
143
+ "test",
144
+ [" - molecule test"],
145
+ artifacts=enable_artifacts,
146
+ )
147
+ )
148
+
149
+ return jobs
150
+
151
+
152
+ def _generate_gitlab_ci_yaml(
153
+ project_name: str,
154
+ ci_patterns: dict[str, Any],
155
+ enable_cache: bool,
156
+ enable_artifacts: bool,
157
+ ) -> str:
158
+ """
159
+ Generate GitLab CI YAML configuration.
160
+
161
+ Args:
162
+ project_name: Project name.
163
+ ci_patterns: Detected CI patterns.
164
+ enable_cache: Enable caching.
165
+ enable_artifacts: Enable artifacts.
166
+
167
+ Returns:
168
+ GitLab CI YAML content.
169
+
170
+ """
171
+ stages = ["lint", "test", "deploy"]
172
+ jobs = []
173
+
174
+ # Global configuration
175
+ config_lines = [
176
+ f"# .gitlab-ci.yml: {project_name}",
177
+ "# Generated from Chef cookbook CI/CD patterns",
178
+ "",
179
+ "image: python:3.11",
180
+ "",
181
+ "stages:",
182
+ ]
183
+ for stage in stages:
184
+ config_lines.append(f" - {stage}")
185
+ config_lines.append("")
186
+
187
+ # Cache configuration
188
+ if enable_cache:
189
+ config_lines.extend(
190
+ [
191
+ "cache:",
192
+ " paths:",
193
+ " - .cache/pip",
194
+ " - venv/",
195
+ "",
196
+ ]
197
+ )
198
+
199
+ # Variables
200
+ config_lines.extend(
201
+ [
202
+ "variables:",
203
+ " PIP_CACHE_DIR: $CI_PROJECT_DIR/.cache/pip",
204
+ " ANSIBLE_FORCE_COLOR: 'true'",
205
+ "",
206
+ ]
207
+ )
208
+
209
+ # Before script
210
+ config_lines.extend(
211
+ [
212
+ "before_script:",
213
+ " - python -m venv venv",
214
+ " - source venv/bin/activate",
215
+ " - pip install --upgrade pip",
216
+ " - pip install ansible ansible-lint molecule molecule-docker",
217
+ "",
218
+ ]
219
+ )
220
+
221
+ # Build jobs
222
+ jobs.extend(_build_lint_jobs(ci_patterns, enable_artifacts))
223
+ jobs.extend(_build_test_jobs(ci_patterns, enable_artifacts))
224
+
225
+ # Deploy job
226
+ jobs.append(
227
+ _create_gitlab_job(
228
+ "deploy:production",
229
+ "deploy",
230
+ [
231
+ (
232
+ " - ansible-playbook -i inventory/production "
233
+ "playbooks/site.yml --check"
234
+ ),
235
+ " - ansible-playbook -i inventory/production playbooks/site.yml",
236
+ ],
237
+ when="manual",
238
+ only_branches=["main", "master"],
239
+ )
240
+ )
241
+
242
+ # Combine configuration and jobs
243
+ config = "\n".join(config_lines) + "\n" + "\n".join(jobs)
244
+ return config
245
+
246
+
247
+ def _create_gitlab_job(
248
+ name: str,
249
+ stage: str,
250
+ script: list[str],
251
+ allow_failure: bool = False,
252
+ artifacts: bool = False,
253
+ when: str | None = None,
254
+ only_branches: list[str] | None = None,
255
+ ) -> str:
256
+ """
257
+ Create a GitLab CI job.
258
+
259
+ Args:
260
+ name: Job name.
261
+ stage: Stage name.
262
+ script: List of script commands.
263
+ allow_failure: Allow job failure.
264
+ artifacts: Enable artifacts.
265
+ when: When to run (manual, on_success, etc).
266
+ only_branches: Only run on specific branches.
267
+
268
+ Returns:
269
+ GitLab CI job YAML block.
270
+
271
+ """
272
+ job_lines = [f"{name}:", f" stage: {stage}", " script:"]
273
+ job_lines.extend(script)
274
+
275
+ if allow_failure:
276
+ job_lines.append(" allow_failure: true")
277
+
278
+ if artifacts:
279
+ job_lines.extend(
280
+ [
281
+ " artifacts:",
282
+ " reports:",
283
+ " junit: test-results/*.xml",
284
+ " paths:",
285
+ " - test-results/",
286
+ " expire_in: 1 week",
287
+ ]
288
+ )
289
+
290
+ if when:
291
+ job_lines.append(f" when: {when}")
292
+
293
+ if only_branches:
294
+ job_lines.append(" only:")
295
+ for branch in only_branches:
296
+ job_lines.append(f" - {branch}")
297
+
298
+ job_lines.append("")
299
+ return "\n".join(job_lines)
@@ -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
+ # Analyse Chef CI patterns
32
+ ci_patterns = _analyse_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 _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
+ 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
+ )