mcp-souschef 2.5.3__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.
- {mcp_souschef-2.5.3.dist-info → mcp_souschef-2.8.0.dist-info}/METADATA +56 -21
- mcp_souschef-2.8.0.dist-info/RECORD +42 -0
- souschef/__init__.py +10 -2
- souschef/assessment.py +14 -14
- souschef/ci/github_actions.py +5 -5
- souschef/ci/gitlab_ci.py +4 -4
- souschef/ci/jenkins_pipeline.py +4 -4
- souschef/cli.py +12 -12
- 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 +853 -15
- souschef/converters/resource.py +103 -1
- souschef/core/constants.py +13 -0
- souschef/core/path_utils.py +12 -9
- souschef/deployment.py +24 -24
- souschef/parsers/attributes.py +397 -32
- souschef/parsers/recipe.py +48 -10
- souschef/server.py +35 -37
- souschef/ui/app.py +1413 -252
- souschef/ui/health_check.py +36 -0
- souschef/ui/pages/ai_settings.py +497 -0
- souschef/ui/pages/cookbook_analysis.py +1010 -75
- mcp_souschef-2.5.3.dist-info/RECORD +0 -38
- {mcp_souschef-2.5.3.dist-info → mcp_souschef-2.8.0.dist-info}/WHEEL +0 -0
- {mcp_souschef-2.5.3.dist-info → mcp_souschef-2.8.0.dist-info}/entry_points.txt +0 -0
- {mcp_souschef-2.5.3.dist-info → mcp_souschef-2.8.0.dist-info}/licenses/LICENSE +0 -0
souschef/converters/resource.py
CHANGED
|
@@ -2,9 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
4
|
import json
|
|
5
|
+
import re
|
|
5
6
|
from collections.abc import Callable
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
9
|
+
from souschef.converters.cookbook_specific import (
|
|
10
|
+
build_cookbook_resource_params,
|
|
11
|
+
get_cookbook_package_config,
|
|
12
|
+
)
|
|
8
13
|
from souschef.core.constants import ACTION_TO_STATE, RESOURCE_MAPPINGS
|
|
9
14
|
|
|
10
15
|
# Type alias for parameter builder functions
|
|
@@ -41,6 +46,51 @@ def _parse_properties(properties_str: str) -> dict[str, Any]:
|
|
|
41
46
|
return {}
|
|
42
47
|
|
|
43
48
|
|
|
49
|
+
def _normalize_template_value(value: Any) -> Any:
|
|
50
|
+
"""Normalize Ruby-style attribute references into Jinja templates."""
|
|
51
|
+
if isinstance(value, str):
|
|
52
|
+
# Convert Chef node attributes to Ansible variables dynamically
|
|
53
|
+
def _replace_node_attr(match):
|
|
54
|
+
cookbook = match.group(1)
|
|
55
|
+
attr = match.group(2)
|
|
56
|
+
|
|
57
|
+
# Convert cookbook name to readable format
|
|
58
|
+
def char_to_word(c: str) -> str:
|
|
59
|
+
number_words = {
|
|
60
|
+
"1": "one",
|
|
61
|
+
"2": "two",
|
|
62
|
+
"3": "three",
|
|
63
|
+
"4": "four",
|
|
64
|
+
"5": "five",
|
|
65
|
+
"6": "six",
|
|
66
|
+
"7": "seven",
|
|
67
|
+
"8": "eight",
|
|
68
|
+
"9": "nine",
|
|
69
|
+
"0": "zero",
|
|
70
|
+
}
|
|
71
|
+
return number_words.get(c, c)
|
|
72
|
+
|
|
73
|
+
# Replace non-alphanumeric characters with underscores
|
|
74
|
+
readable_cookbook = "".join(
|
|
75
|
+
char_to_word(c) if c.isdigit() else (c if c.isalnum() else "_")
|
|
76
|
+
for c in cookbook
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Ensure we don't have multiple consecutive underscores
|
|
80
|
+
readable_cookbook = re.sub(r"_+", "_", readable_cookbook)
|
|
81
|
+
# Remove leading/trailing underscores
|
|
82
|
+
readable_cookbook = readable_cookbook.strip("_")
|
|
83
|
+
|
|
84
|
+
return f"{{{{ {readable_cookbook}_{attr} }}}}"
|
|
85
|
+
|
|
86
|
+
value = re.sub(r"node\['(\w+)'\]\['(\w+)'\]", _replace_node_attr, value)
|
|
87
|
+
|
|
88
|
+
# Wrap in Jinja if it's a node reference
|
|
89
|
+
if "node[" in value:
|
|
90
|
+
return f"{{{{ {value} }}}}"
|
|
91
|
+
return value
|
|
92
|
+
|
|
93
|
+
|
|
44
94
|
def convert_resource_to_task(
|
|
45
95
|
resource_type: str, resource_name: str, action: str = "create", properties: str = ""
|
|
46
96
|
) -> str:
|
|
@@ -188,6 +238,22 @@ def _get_remote_file_params(
|
|
|
188
238
|
return params
|
|
189
239
|
|
|
190
240
|
|
|
241
|
+
def _get_include_recipe_params(
|
|
242
|
+
resource_name: str, action: str, props: dict[str, Any]
|
|
243
|
+
) -> dict[str, Any]:
|
|
244
|
+
"""
|
|
245
|
+
Build parameters for include_recipe resources.
|
|
246
|
+
|
|
247
|
+
Uses cookbook-specific configurations when available.
|
|
248
|
+
"""
|
|
249
|
+
cookbook_config = get_cookbook_package_config(resource_name)
|
|
250
|
+
if cookbook_config:
|
|
251
|
+
# Return a copy to prevent callers from mutating the shared mapping.
|
|
252
|
+
return dict(cookbook_config["params"])
|
|
253
|
+
# Default behavior for recipes without a specific mapping.
|
|
254
|
+
return {"name": resource_name, "state": "present"}
|
|
255
|
+
|
|
256
|
+
|
|
191
257
|
def _get_default_params(resource_name: str, action: str) -> dict[str, Any]:
|
|
192
258
|
"""Build default parameters for unknown resource types."""
|
|
193
259
|
params = {"name": resource_name}
|
|
@@ -209,6 +275,7 @@ RESOURCE_PARAM_BUILDERS: dict[str, ParamBuilder | str] = {
|
|
|
209
275
|
"user": _get_user_group_params,
|
|
210
276
|
"group": _get_user_group_params,
|
|
211
277
|
"remote_file": _get_remote_file_params,
|
|
278
|
+
"include_recipe": _get_include_recipe_params,
|
|
212
279
|
}
|
|
213
280
|
|
|
214
281
|
|
|
@@ -229,7 +296,23 @@ def _convert_chef_resource_to_ansible(
|
|
|
229
296
|
|
|
230
297
|
"""
|
|
231
298
|
# Get Ansible module name
|
|
232
|
-
ansible_module = RESOURCE_MAPPINGS.get(resource_type
|
|
299
|
+
ansible_module = RESOURCE_MAPPINGS.get(resource_type)
|
|
300
|
+
|
|
301
|
+
# Check for cookbook-specific include_recipe configurations
|
|
302
|
+
if resource_type == "include_recipe":
|
|
303
|
+
cookbook_config = get_cookbook_package_config(resource_name)
|
|
304
|
+
if cookbook_config:
|
|
305
|
+
ansible_module = cookbook_config["module"]
|
|
306
|
+
|
|
307
|
+
# Handle unknown resource types
|
|
308
|
+
if ansible_module is None:
|
|
309
|
+
# Return a task with just a comment for unknown resources
|
|
310
|
+
return {
|
|
311
|
+
"name": f"Create {resource_type} {resource_name}",
|
|
312
|
+
"# Unknown": f"{resource_type}:",
|
|
313
|
+
"resource_name": resource_name,
|
|
314
|
+
"state": "present",
|
|
315
|
+
}
|
|
233
316
|
|
|
234
317
|
# Start building the task
|
|
235
318
|
task: dict[str, Any] = {
|
|
@@ -242,6 +325,12 @@ def _convert_chef_resource_to_ansible(
|
|
|
242
325
|
# Build module parameters using appropriate builder
|
|
243
326
|
module_params = _build_module_params(resource_type, resource_name, action, props)
|
|
244
327
|
|
|
328
|
+
# Override with cookbook-specific params for include_recipe
|
|
329
|
+
if resource_type == "include_recipe":
|
|
330
|
+
cookbook_config = get_cookbook_package_config(resource_name)
|
|
331
|
+
if cookbook_config:
|
|
332
|
+
module_params = cookbook_config["params"].copy()
|
|
333
|
+
|
|
245
334
|
# Add special task-level flags for execute/bash resources
|
|
246
335
|
if resource_type in ["execute", "bash"]:
|
|
247
336
|
task["changed_when"] = "false"
|
|
@@ -266,6 +355,13 @@ def _build_module_params(
|
|
|
266
355
|
Dictionary of Ansible module parameters.
|
|
267
356
|
|
|
268
357
|
"""
|
|
358
|
+
# First check for cookbook-specific resource types
|
|
359
|
+
cookbook_params = build_cookbook_resource_params(
|
|
360
|
+
resource_type, resource_name, action, props
|
|
361
|
+
)
|
|
362
|
+
if cookbook_params is not None:
|
|
363
|
+
return cookbook_params
|
|
364
|
+
|
|
269
365
|
# Look up the parameter builder for this resource type
|
|
270
366
|
builder = RESOURCE_PARAM_BUILDERS.get(resource_type)
|
|
271
367
|
|
|
@@ -297,6 +393,8 @@ def _format_dict_value(key: str, value: dict[str, Any]) -> list[str]:
|
|
|
297
393
|
"""Format a dictionary value for YAML output."""
|
|
298
394
|
lines = [f" {key}:"]
|
|
299
395
|
for param_key, param_value in value.items():
|
|
396
|
+
# Indent nested params by four spaces so downstream formatting nests
|
|
397
|
+
# module parameters under the module key.
|
|
300
398
|
lines.append(f" {param_key}: {_format_yaml_value(param_value)}")
|
|
301
399
|
return lines
|
|
302
400
|
|
|
@@ -317,6 +415,10 @@ def _format_ansible_task(task: dict[str, Any]) -> str:
|
|
|
317
415
|
for key, value in task.items():
|
|
318
416
|
if key == "name":
|
|
319
417
|
continue
|
|
418
|
+
if key == "# Unknown":
|
|
419
|
+
# Handle unknown resources with a comment
|
|
420
|
+
result.append(f" # {value}")
|
|
421
|
+
continue
|
|
320
422
|
if isinstance(value, dict):
|
|
321
423
|
result.extend(_format_dict_value(key, value))
|
|
322
424
|
else:
|
souschef/core/constants.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"""Constants used throughout SousChef."""
|
|
2
2
|
|
|
3
3
|
__all__ = [
|
|
4
|
+
"VERSION",
|
|
4
5
|
"ANSIBLE_SERVICE_MODULE",
|
|
5
6
|
"METADATA_FILENAME",
|
|
6
7
|
"ERROR_PREFIX",
|
|
7
8
|
"NODE_PREFIX",
|
|
9
|
+
"ATTRIBUTE_PREFIX",
|
|
10
|
+
"VALUE_PREFIX",
|
|
8
11
|
"CHEF_RECIPE_PREFIX",
|
|
9
12
|
"CHEF_ROLE_PREFIX",
|
|
10
13
|
"REGEX_WHITESPACE_QUOTE",
|
|
@@ -39,6 +42,9 @@ __all__ = [
|
|
|
39
42
|
"ACTION_TO_STATE",
|
|
40
43
|
]
|
|
41
44
|
|
|
45
|
+
# Version
|
|
46
|
+
VERSION = "2.6.0"
|
|
47
|
+
|
|
42
48
|
# Ansible module names
|
|
43
49
|
ANSIBLE_SERVICE_MODULE = "ansible.builtin.service"
|
|
44
50
|
|
|
@@ -48,6 +54,8 @@ METADATA_FILENAME = "metadata.rb"
|
|
|
48
54
|
# Common prefixes
|
|
49
55
|
ERROR_PREFIX = "Error:"
|
|
50
56
|
NODE_PREFIX = "node["
|
|
57
|
+
ATTRIBUTE_PREFIX = "Attribute: "
|
|
58
|
+
VALUE_PREFIX = "Value: "
|
|
51
59
|
CHEF_RECIPE_PREFIX = "recipe["
|
|
52
60
|
CHEF_ROLE_PREFIX = "role["
|
|
53
61
|
|
|
@@ -126,6 +134,11 @@ RESOURCE_MAPPINGS = {
|
|
|
126
134
|
"mount": "ansible.builtin.mount",
|
|
127
135
|
"git": "ansible.builtin.git",
|
|
128
136
|
"remote_file": "ansible.builtin.get_url",
|
|
137
|
+
"nodejs_npm": "community.general.npm",
|
|
138
|
+
# Map include_recipe to a generic package install so well-known recipes
|
|
139
|
+
# (like nodejs) can be materialised into concrete tasks instead of
|
|
140
|
+
# producing stubbed import_role calls.
|
|
141
|
+
"include_recipe": "ansible.builtin.package",
|
|
129
142
|
}
|
|
130
143
|
|
|
131
144
|
# Chef action to Ansible state mappings
|
souschef/core/path_utils.py
CHANGED
|
@@ -7,10 +7,8 @@ def _normalize_path(path_str: str) -> Path:
|
|
|
7
7
|
"""
|
|
8
8
|
Normalize a file path for safe filesystem operations.
|
|
9
9
|
|
|
10
|
-
This function resolves relative paths and symlinks
|
|
11
|
-
preventing path traversal attacks (CWE-23).
|
|
12
|
-
intentionally allows full filesystem access as it runs in the user's
|
|
13
|
-
local environment with their permissions.
|
|
10
|
+
This function validates input and resolves relative paths and symlinks
|
|
11
|
+
to absolute paths, preventing path traversal attacks (CWE-23).
|
|
14
12
|
|
|
15
13
|
Args:
|
|
16
14
|
path_str: Path string to normalize.
|
|
@@ -19,17 +17,22 @@ def _normalize_path(path_str: str) -> Path:
|
|
|
19
17
|
Resolved absolute Path object.
|
|
20
18
|
|
|
21
19
|
Raises:
|
|
22
|
-
ValueError: If the path contains null bytes or is invalid.
|
|
20
|
+
ValueError: If the path contains null bytes, traversal attempts, or is invalid.
|
|
23
21
|
|
|
24
22
|
"""
|
|
23
|
+
if not isinstance(path_str, str):
|
|
24
|
+
raise ValueError(f"Path must be a string, got {type(path_str)}")
|
|
25
|
+
|
|
26
|
+
# Reject paths with null bytes
|
|
25
27
|
if "\x00" in path_str:
|
|
26
28
|
raise ValueError(f"Path contains null bytes: {path_str!r}")
|
|
27
29
|
|
|
30
|
+
# Reject paths with obvious directory traversal attempts
|
|
31
|
+
if ".." in path_str:
|
|
32
|
+
raise ValueError(f"Path contains directory traversal: {path_str!r}")
|
|
33
|
+
|
|
28
34
|
try:
|
|
29
|
-
# Resolve to absolute path, removing
|
|
30
|
-
# This is the path normalization function itself that validates input
|
|
31
|
-
# lgtm[py/path-injection]
|
|
32
|
-
# codeql[py/path-injection]
|
|
35
|
+
# Resolve to absolute path, removing ., and resolving symlinks
|
|
33
36
|
return Path(path_str).resolve()
|
|
34
37
|
except (OSError, RuntimeError) as e:
|
|
35
38
|
raise ValueError(f"Invalid path {path_str}: {e}") from e
|
souschef/deployment.py
CHANGED
|
@@ -51,7 +51,7 @@ def generate_awx_job_template_from_cookbook(
|
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
cookbook = validate_cookbook_structure(cookbook_path)
|
|
54
|
-
cookbook_analysis =
|
|
54
|
+
cookbook_analysis = _analyse_cookbook_for_awx(cookbook, cookbook_name)
|
|
55
55
|
job_template = _generate_awx_job_template(
|
|
56
56
|
cookbook_analysis, cookbook_name, target_environment, include_survey
|
|
57
57
|
)
|
|
@@ -184,7 +184,7 @@ def generate_awx_project_from_cookbooks(
|
|
|
184
184
|
)
|
|
185
185
|
|
|
186
186
|
# Analyze all cookbooks
|
|
187
|
-
cookbooks_analysis =
|
|
187
|
+
cookbooks_analysis = _analyse_cookbooks_directory(cookbooks_path)
|
|
188
188
|
|
|
189
189
|
# Generate project structure
|
|
190
190
|
project_config = _generate_awx_project_config(project_name, scm_type, scm_url)
|
|
@@ -329,7 +329,7 @@ def convert_chef_deployment_to_ansible_strategy(
|
|
|
329
329
|
)
|
|
330
330
|
|
|
331
331
|
# Analyze Chef deployment pattern
|
|
332
|
-
pattern_analysis =
|
|
332
|
+
pattern_analysis = _analyse_chef_deployment_pattern(cookbook)
|
|
333
333
|
|
|
334
334
|
# Determine best strategy if auto-detect
|
|
335
335
|
if deployment_pattern == "auto":
|
|
@@ -626,11 +626,11 @@ def generate_canary_deployment_strategy(
|
|
|
626
626
|
)
|
|
627
627
|
|
|
628
628
|
|
|
629
|
-
def
|
|
629
|
+
def analyse_chef_application_patterns(
|
|
630
630
|
cookbook_path: str, application_type: str = "web_application"
|
|
631
631
|
) -> str:
|
|
632
632
|
"""
|
|
633
|
-
|
|
633
|
+
Analyse cookbook deployment patterns and recommend Ansible strategies.
|
|
634
634
|
|
|
635
635
|
Detects blue/green, canary, rolling, or custom deployment approaches.
|
|
636
636
|
Application type helps tune recommendations for web/database/service workloads.
|
|
@@ -647,7 +647,7 @@ def analyze_chef_application_patterns(
|
|
|
647
647
|
)
|
|
648
648
|
|
|
649
649
|
# Analyze cookbook for application patterns
|
|
650
|
-
analysis =
|
|
650
|
+
analysis = _analyse_application_cookbook(cookbook, application_type)
|
|
651
651
|
|
|
652
652
|
return f"""# Chef Application Patterns Analysis
|
|
653
653
|
# Cookbook: {cookbook.name}
|
|
@@ -685,9 +685,9 @@ def analyze_chef_application_patterns(
|
|
|
685
685
|
# AWX Helper Functions
|
|
686
686
|
|
|
687
687
|
|
|
688
|
-
def
|
|
688
|
+
def _analyse_recipes(cookbook_path: Path) -> list[dict[str, Any]]:
|
|
689
689
|
"""
|
|
690
|
-
|
|
690
|
+
Analyse recipes directory for AWX job steps.
|
|
691
691
|
|
|
692
692
|
Args:
|
|
693
693
|
cookbook_path: Path to cookbook root
|
|
@@ -710,11 +710,11 @@ def _analyze_recipes(cookbook_path: Path) -> list[dict[str, Any]]:
|
|
|
710
710
|
return recipes
|
|
711
711
|
|
|
712
712
|
|
|
713
|
-
def
|
|
713
|
+
def _analyse_attributes_for_survey(
|
|
714
714
|
cookbook_path: Path,
|
|
715
715
|
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
716
716
|
"""
|
|
717
|
-
|
|
717
|
+
Analyse attributes directory for survey field generation.
|
|
718
718
|
|
|
719
719
|
Args:
|
|
720
720
|
cookbook_path: Path to cookbook root
|
|
@@ -748,7 +748,7 @@ def _analyze_attributes_for_survey(
|
|
|
748
748
|
return attributes, survey_fields
|
|
749
749
|
|
|
750
750
|
|
|
751
|
-
def
|
|
751
|
+
def _analyse_metadata_dependencies(cookbook_path: Path) -> list[str]:
|
|
752
752
|
"""
|
|
753
753
|
Extract cookbook dependencies from metadata.
|
|
754
754
|
|
|
@@ -795,9 +795,9 @@ def _collect_static_files(cookbook_path: Path) -> tuple[list[str], list[str]]:
|
|
|
795
795
|
return templates, files
|
|
796
796
|
|
|
797
797
|
|
|
798
|
-
def
|
|
798
|
+
def _analyse_cookbook_for_awx(cookbook_path: Path, cookbook_name: str) -> dict:
|
|
799
799
|
"""
|
|
800
|
-
|
|
800
|
+
Analyse Chef cookbook structure for AWX job template generation.
|
|
801
801
|
|
|
802
802
|
Orchestrates multiple analysis helpers to build comprehensive cookbook metadata.
|
|
803
803
|
|
|
@@ -810,9 +810,9 @@ def _analyze_cookbook_for_awx(cookbook_path: Path, cookbook_name: str) -> dict:
|
|
|
810
810
|
|
|
811
811
|
"""
|
|
812
812
|
# Analyze each dimension independently
|
|
813
|
-
recipes =
|
|
814
|
-
attributes, survey_fields =
|
|
815
|
-
dependencies =
|
|
813
|
+
recipes = _analyse_recipes(cookbook_path)
|
|
814
|
+
attributes, survey_fields = _analyse_attributes_for_survey(cookbook_path)
|
|
815
|
+
dependencies = _analyse_metadata_dependencies(cookbook_path)
|
|
816
816
|
templates, files = _collect_static_files(cookbook_path)
|
|
817
817
|
|
|
818
818
|
# Assemble complete analysis
|
|
@@ -1155,8 +1155,8 @@ def _generate_survey_fields_from_attributes(attributes: dict) -> list:
|
|
|
1155
1155
|
return survey_fields
|
|
1156
1156
|
|
|
1157
1157
|
|
|
1158
|
-
def
|
|
1159
|
-
"""
|
|
1158
|
+
def _analyse_cookbooks_directory(cookbooks_path: Path) -> dict:
|
|
1159
|
+
"""Analyse entire cookbooks directory structure."""
|
|
1160
1160
|
analysis: dict[str, Any] = {
|
|
1161
1161
|
"total_cookbooks": 0,
|
|
1162
1162
|
"cookbooks": {},
|
|
@@ -1172,7 +1172,7 @@ def _analyze_cookbooks_directory(cookbooks_path: Path) -> dict:
|
|
|
1172
1172
|
cookbook_name = cookbook_dir.name
|
|
1173
1173
|
analysis["total_cookbooks"] += 1
|
|
1174
1174
|
|
|
1175
|
-
cookbook_analysis =
|
|
1175
|
+
cookbook_analysis = _analyse_cookbook_for_awx(cookbook_dir, cookbook_name)
|
|
1176
1176
|
analysis["cookbooks"][cookbook_name] = cookbook_analysis
|
|
1177
1177
|
|
|
1178
1178
|
# Aggregate stats
|
|
@@ -1186,8 +1186,8 @@ def _analyze_cookbooks_directory(cookbooks_path: Path) -> dict:
|
|
|
1186
1186
|
# Deployment Strategy Helper Functions
|
|
1187
1187
|
|
|
1188
1188
|
|
|
1189
|
-
def
|
|
1190
|
-
"""
|
|
1189
|
+
def _analyse_chef_deployment_pattern(cookbook_path: Path) -> dict:
|
|
1190
|
+
"""Analyse Chef cookbook for deployment patterns."""
|
|
1191
1191
|
analysis: dict[str, Any] = {
|
|
1192
1192
|
"deployment_steps": [],
|
|
1193
1193
|
"health_checks": [],
|
|
@@ -1505,8 +1505,8 @@ def _assess_complexity_from_resource_count(resource_count: int) -> tuple[str, st
|
|
|
1505
1505
|
return "medium", "2-3 weeks", "medium"
|
|
1506
1506
|
|
|
1507
1507
|
|
|
1508
|
-
def
|
|
1509
|
-
"""
|
|
1508
|
+
def _analyse_application_cookbook(cookbook_path: Path, app_type: str) -> dict:
|
|
1509
|
+
"""Analyse Chef cookbook for application deployment patterns."""
|
|
1510
1510
|
analysis: dict[str, Any] = {
|
|
1511
1511
|
"application_type": app_type,
|
|
1512
1512
|
"deployment_patterns": [],
|
|
@@ -1650,7 +1650,7 @@ def _format_deployment_patterns(analysis: dict) -> str:
|
|
|
1650
1650
|
|
|
1651
1651
|
def _format_chef_resources_analysis(analysis: dict) -> str:
|
|
1652
1652
|
"""Format Chef resources analysis."""
|
|
1653
|
-
# Check for new format first (from
|
|
1653
|
+
# Check for new format first (from _analyse_application_cookbook)
|
|
1654
1654
|
resources = analysis.get("resources", [])
|
|
1655
1655
|
if resources:
|
|
1656
1656
|
# Count resource types
|