awslabs.terraform-mcp-server 1.0.14__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.
- awslabs/__init__.py +17 -0
- awslabs/terraform_mcp_server/__init__.py +17 -0
- awslabs/terraform_mcp_server/impl/resources/__init__.py +25 -0
- awslabs/terraform_mcp_server/impl/resources/terraform_aws_provider_resources_listing.py +66 -0
- awslabs/terraform_mcp_server/impl/resources/terraform_awscc_provider_resources_listing.py +69 -0
- awslabs/terraform_mcp_server/impl/tools/__init__.py +33 -0
- awslabs/terraform_mcp_server/impl/tools/execute_terraform_command.py +223 -0
- awslabs/terraform_mcp_server/impl/tools/execute_terragrunt_command.py +320 -0
- awslabs/terraform_mcp_server/impl/tools/run_checkov_scan.py +376 -0
- awslabs/terraform_mcp_server/impl/tools/search_aws_provider_docs.py +691 -0
- awslabs/terraform_mcp_server/impl/tools/search_awscc_provider_docs.py +641 -0
- awslabs/terraform_mcp_server/impl/tools/search_specific_aws_ia_modules.py +458 -0
- awslabs/terraform_mcp_server/impl/tools/search_user_provided_module.py +349 -0
- awslabs/terraform_mcp_server/impl/tools/utils.py +572 -0
- awslabs/terraform_mcp_server/models/__init__.py +49 -0
- awslabs/terraform_mcp_server/models/models.py +381 -0
- awslabs/terraform_mcp_server/scripts/generate_aws_provider_resources.py +1240 -0
- awslabs/terraform_mcp_server/scripts/generate_awscc_provider_resources.py +1039 -0
- awslabs/terraform_mcp_server/scripts/scrape_aws_terraform_best_practices.py +143 -0
- awslabs/terraform_mcp_server/server.py +440 -0
- awslabs/terraform_mcp_server/static/AWSCC_PROVIDER_RESOURCES.md +3125 -0
- awslabs/terraform_mcp_server/static/AWS_PROVIDER_RESOURCES.md +3833 -0
- awslabs/terraform_mcp_server/static/AWS_TERRAFORM_BEST_PRACTICES.md +2523 -0
- awslabs/terraform_mcp_server/static/MCP_INSTRUCTIONS.md +142 -0
- awslabs/terraform_mcp_server/static/TERRAFORM_WORKFLOW_GUIDE.md +330 -0
- awslabs/terraform_mcp_server/static/__init__.py +38 -0
- awslabs_terraform_mcp_server-1.0.14.dist-info/METADATA +166 -0
- awslabs_terraform_mcp_server-1.0.14.dist-info/RECORD +30 -0
- awslabs_terraform_mcp_server-1.0.14.dist-info/WHEEL +4 -0
- awslabs_terraform_mcp_server-1.0.14.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Implementation of AWS provider documentation search tool."""
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
import requests
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
from awslabs.terraform_mcp_server.models import TerraformAWSProviderDocsResult
|
|
22
|
+
from loguru import logger
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Dict, List, Literal, Optional, Tuple, cast
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Configure logger for enhanced diagnostics with stacktraces
|
|
28
|
+
logger.configure(
|
|
29
|
+
handlers=[
|
|
30
|
+
{
|
|
31
|
+
'sink': sys.stderr,
|
|
32
|
+
'backtrace': True,
|
|
33
|
+
'diagnose': True,
|
|
34
|
+
'format': '<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>',
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Path to the static markdown file
|
|
40
|
+
STATIC_RESOURCES_PATH = (
|
|
41
|
+
Path(__file__).parent.parent.parent / 'static' / 'AWS_PROVIDER_RESOURCES.md'
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Base URLs for AWS provider documentation
|
|
45
|
+
AWS_DOCS_BASE_URL = 'https://registry.terraform.io/providers/hashicorp/aws/latest/docs'
|
|
46
|
+
GITHUB_RAW_BASE_URL = (
|
|
47
|
+
'https://raw.githubusercontent.com/hashicorp/terraform-provider-aws/main/website/docs'
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Simple in-memory cache
|
|
51
|
+
_GITHUB_DOC_CACHE = {}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resource_to_github_path(
|
|
55
|
+
asset_name: str, asset_type: str = 'resource', correlation_id: str = ''
|
|
56
|
+
) -> Tuple[str, str]:
|
|
57
|
+
"""Convert AWS resource type to GitHub documentation file path.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
asset_name: The name of the asset to search (e.g., 'aws_s3_bucket')
|
|
61
|
+
asset_type: Type of asset to search for - 'resource' or 'data_source'
|
|
62
|
+
correlation_id: Identifier for tracking this request in logs
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
A tuple of (path, url) for the GitHub documentation file
|
|
66
|
+
"""
|
|
67
|
+
# Validate input parameters
|
|
68
|
+
if not isinstance(asset_name, str) or not asset_name:
|
|
69
|
+
logger.error(f'[{correlation_id}] Invalid asset_name: {asset_name}')
|
|
70
|
+
raise ValueError('asset_name must be a non-empty string')
|
|
71
|
+
|
|
72
|
+
# Sanitize asset_name to prevent path traversal and URL manipulation
|
|
73
|
+
# Only allow alphanumeric characters, underscores, and hyphens
|
|
74
|
+
sanitized_name = asset_name
|
|
75
|
+
if not re.match(r'^[a-zA-Z0-9_-]+$', sanitized_name.replace('aws_', '')):
|
|
76
|
+
logger.error(f'[{correlation_id}] Invalid characters in asset_name: {asset_name}')
|
|
77
|
+
raise ValueError('asset_name contains invalid characters')
|
|
78
|
+
|
|
79
|
+
# Validate asset_type
|
|
80
|
+
valid_asset_types = ['resource', 'data_source', 'both']
|
|
81
|
+
if asset_type not in valid_asset_types:
|
|
82
|
+
logger.error(f'[{correlation_id}] Invalid asset_type: {asset_type}')
|
|
83
|
+
raise ValueError(f'asset_type must be one of {valid_asset_types}')
|
|
84
|
+
|
|
85
|
+
# Remove the 'aws_' prefix if present
|
|
86
|
+
if sanitized_name.startswith('aws_'):
|
|
87
|
+
resource_name = sanitized_name[4:]
|
|
88
|
+
logger.trace(f"[{correlation_id}] Removed 'aws_' prefix: {resource_name}")
|
|
89
|
+
else:
|
|
90
|
+
resource_name = sanitized_name
|
|
91
|
+
logger.trace(f"[{correlation_id}] No 'aws_' prefix to remove: {resource_name}")
|
|
92
|
+
|
|
93
|
+
# Determine document type based on asset_type parameter
|
|
94
|
+
if asset_type == 'data_source':
|
|
95
|
+
doc_type = 'd' # data sources
|
|
96
|
+
elif asset_type == 'resource':
|
|
97
|
+
doc_type = 'r' # resources
|
|
98
|
+
else:
|
|
99
|
+
# For "both" or any other value, determine based on name pattern
|
|
100
|
+
# Data sources typically have 'data' in the name or follow other patterns
|
|
101
|
+
is_data_source = 'data' in sanitized_name.lower()
|
|
102
|
+
doc_type = 'd' if is_data_source else 'r'
|
|
103
|
+
|
|
104
|
+
# Create the file path for the markdown documentation
|
|
105
|
+
file_path = f'{doc_type}/{resource_name}.html.markdown'
|
|
106
|
+
logger.trace(f'[{correlation_id}] Constructed GitHub file path: {file_path}')
|
|
107
|
+
|
|
108
|
+
# Create the full URL to the raw GitHub content
|
|
109
|
+
github_url = f'{GITHUB_RAW_BASE_URL}/{file_path}'
|
|
110
|
+
logger.trace(f'[{correlation_id}] GitHub raw URL: {github_url}')
|
|
111
|
+
|
|
112
|
+
return file_path, github_url
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def fetch_github_documentation(
|
|
116
|
+
asset_name: str, asset_type: str, cache_enabled: bool, correlation_id: str = ''
|
|
117
|
+
) -> Optional[Dict[str, Any]]:
|
|
118
|
+
"""Fetch documentation from GitHub for a specific resource type.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
asset_name: The asset name (e.g., 'aws_s3_bucket')
|
|
122
|
+
asset_type: Either 'resource' or 'data_source'
|
|
123
|
+
cache_enabled: Whether local cache is enabled or not
|
|
124
|
+
correlation_id: Identifier for tracking this request in logs
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Dictionary with markdown content and metadata, or None if not found
|
|
128
|
+
"""
|
|
129
|
+
start_time = time.time()
|
|
130
|
+
logger.info(f"[{correlation_id}] Fetching documentation from GitHub for '{asset_name}'")
|
|
131
|
+
|
|
132
|
+
# Create a cache key that includes both asset_name and asset_type
|
|
133
|
+
# Use a hash function to ensure the cache key is safe
|
|
134
|
+
cache_key = f'{asset_name}_{asset_type}'
|
|
135
|
+
|
|
136
|
+
# Check cache first
|
|
137
|
+
if cache_enabled:
|
|
138
|
+
if cache_key in _GITHUB_DOC_CACHE:
|
|
139
|
+
logger.info(
|
|
140
|
+
f"[{correlation_id}] Using cached documentation for '{asset_name}' (asset_type: {asset_type})"
|
|
141
|
+
)
|
|
142
|
+
return _GITHUB_DOC_CACHE[cache_key]
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
# Convert resource type to GitHub path and URL
|
|
146
|
+
# This will validate and sanitize the input
|
|
147
|
+
try:
|
|
148
|
+
_, github_url = resource_to_github_path(asset_name, asset_type, correlation_id)
|
|
149
|
+
except ValueError as e:
|
|
150
|
+
logger.error(f'[{correlation_id}] Invalid input parameters: {str(e)}')
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
# Validate the constructed URL to ensure it points to the expected domain
|
|
154
|
+
if not github_url.startswith(GITHUB_RAW_BASE_URL):
|
|
155
|
+
logger.error(f'[{correlation_id}] Invalid GitHub URL constructed: {github_url}')
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
# Fetch the markdown content from GitHub
|
|
159
|
+
logger.info(f'[{correlation_id}] Fetching from GitHub URL: {github_url}')
|
|
160
|
+
response = requests.get(github_url, timeout=10)
|
|
161
|
+
|
|
162
|
+
if response.status_code != 200:
|
|
163
|
+
logger.warning(
|
|
164
|
+
f'[{correlation_id}] GitHub request failed: HTTP {response.status_code}'
|
|
165
|
+
)
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
markdown_content = response.text
|
|
169
|
+
content_length = len(markdown_content)
|
|
170
|
+
logger.debug(f'[{correlation_id}] Received markdown content: {content_length} bytes')
|
|
171
|
+
|
|
172
|
+
if content_length > 0:
|
|
173
|
+
preview_length = min(200, content_length)
|
|
174
|
+
logger.trace(
|
|
175
|
+
f'[{correlation_id}] Markdown preview: {markdown_content[:preview_length]}...'
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Parse the markdown content
|
|
179
|
+
result = parse_markdown_documentation(
|
|
180
|
+
markdown_content, asset_name, github_url, correlation_id
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Cache the result with the composite key
|
|
184
|
+
if cache_enabled:
|
|
185
|
+
_GITHUB_DOC_CACHE[cache_key] = result
|
|
186
|
+
|
|
187
|
+
fetch_time = time.time() - start_time
|
|
188
|
+
logger.info(f'[{correlation_id}] GitHub documentation fetched in {fetch_time:.2f} seconds')
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
except requests.exceptions.Timeout as e:
|
|
192
|
+
logger.warning(f'[{correlation_id}] Timeout error fetching from GitHub: {str(e)}')
|
|
193
|
+
return None
|
|
194
|
+
except requests.exceptions.RequestException as e:
|
|
195
|
+
logger.warning(f'[{correlation_id}] Request error fetching from GitHub: {str(e)}')
|
|
196
|
+
return None
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logger.error(
|
|
199
|
+
f'[{correlation_id}] Unexpected error fetching from GitHub: {type(e).__name__}: {str(e)}'
|
|
200
|
+
)
|
|
201
|
+
# Don't log the full stack trace to avoid information disclosure
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def parse_markdown_documentation(
|
|
206
|
+
content: str, asset_name: str, url: str, correlation_id: str = ''
|
|
207
|
+
) -> Dict[str, Any]:
|
|
208
|
+
"""Parse markdown documentation content for a resource.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
content: The markdown content
|
|
212
|
+
asset_name: The asset name
|
|
213
|
+
url: The source URL for this documentation
|
|
214
|
+
correlation_id: Identifier for tracking this request in logs
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Dictionary with parsed documentation details
|
|
218
|
+
"""
|
|
219
|
+
start_time = time.time()
|
|
220
|
+
logger.debug(f"[{correlation_id}] Parsing markdown documentation for '{asset_name}'")
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
# Find the title (typically the first heading)
|
|
224
|
+
title_match = re.search(r'^#\s+(.*?)$', content, re.MULTILINE)
|
|
225
|
+
if title_match:
|
|
226
|
+
title = title_match.group(1).strip()
|
|
227
|
+
logger.debug(f"[{correlation_id}] Found title: '{title}'")
|
|
228
|
+
else:
|
|
229
|
+
title = f'AWS {asset_name}'
|
|
230
|
+
logger.debug(f"[{correlation_id}] No title found, using default: '{title}'")
|
|
231
|
+
|
|
232
|
+
# Find the main description section (all content after resource title before next heading)
|
|
233
|
+
description = ''
|
|
234
|
+
resource_heading_pattern = re.compile(
|
|
235
|
+
rf'# Resource: {re.escape(asset_name)}\s*(.*?)(?=\n##|\Z)', re.DOTALL
|
|
236
|
+
)
|
|
237
|
+
resource_match = resource_heading_pattern.search(content)
|
|
238
|
+
|
|
239
|
+
if resource_match:
|
|
240
|
+
# Extract the description text and clean it up
|
|
241
|
+
description = resource_match.group(1).strip()
|
|
242
|
+
logger.debug(
|
|
243
|
+
f"[{correlation_id}] Found resource description section: '{description[:100]}...'"
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
# Fall back to the description found on the starting markdown table of each github markdown page
|
|
247
|
+
desc_match = re.search(r'description:\s*\|-\n(.*?)\n---', content, re.MULTILINE)
|
|
248
|
+
if desc_match:
|
|
249
|
+
description = desc_match.group(1).strip()
|
|
250
|
+
logger.debug(
|
|
251
|
+
f"[{correlation_id}] Using fallback description: '{description[:100]}...'"
|
|
252
|
+
)
|
|
253
|
+
else:
|
|
254
|
+
description = f'Documentation for AWS {asset_name}'
|
|
255
|
+
logger.debug(f'[{correlation_id}] No description found, using default')
|
|
256
|
+
|
|
257
|
+
# Find all example snippets
|
|
258
|
+
example_snippets = []
|
|
259
|
+
|
|
260
|
+
# First try to extract from the Example Usage section
|
|
261
|
+
example_section_match = re.search(r'## Example Usage\n([\s\S]*?)(?=\n## |\Z)', content)
|
|
262
|
+
|
|
263
|
+
if example_section_match:
|
|
264
|
+
# logger.debug(f"example_section_match: {example_section_match.group()}")
|
|
265
|
+
example_section = example_section_match.group(1).strip()
|
|
266
|
+
logger.debug(
|
|
267
|
+
f'[{correlation_id}] Found Example Usage section ({len(example_section)} chars)'
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Find all subheadings in the Example Usage section with a more robust pattern
|
|
271
|
+
subheading_list = list(
|
|
272
|
+
re.finditer(r'### (.*?)[\r\n]+(.*?)(?=###|\Z)', example_section, re.DOTALL)
|
|
273
|
+
)
|
|
274
|
+
logger.debug(
|
|
275
|
+
f'[{correlation_id}] Found {len(subheading_list)} subheadings in Example Usage section'
|
|
276
|
+
)
|
|
277
|
+
subheading_found = False
|
|
278
|
+
|
|
279
|
+
# Check if there are any subheadings
|
|
280
|
+
for match in subheading_list:
|
|
281
|
+
# logger.info(f"subheading match: {match.group()}")
|
|
282
|
+
subheading_found = True
|
|
283
|
+
title = match.group(1).strip()
|
|
284
|
+
subcontent = match.group(2).strip()
|
|
285
|
+
|
|
286
|
+
logger.debug(
|
|
287
|
+
f"[{correlation_id}] Found subheading '{title}' with {len(subcontent)} chars content"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Find code blocks in this subsection - pattern to match terraform code blocks
|
|
291
|
+
code_match = re.search(r'```(?:terraform|hcl)?\s*(.*?)```', subcontent, re.DOTALL)
|
|
292
|
+
if code_match:
|
|
293
|
+
code_snippet = code_match.group(1).strip()
|
|
294
|
+
example_snippets.append({'title': title, 'code': code_snippet})
|
|
295
|
+
logger.debug(
|
|
296
|
+
f"[{correlation_id}] Added example snippet for '{title}' ({len(code_snippet)} chars)"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# If no subheadings were found, look for direct code blocks under Example Usage
|
|
300
|
+
if not subheading_found:
|
|
301
|
+
logger.debug(
|
|
302
|
+
f'[{correlation_id}] No subheadings found, looking for direct code blocks'
|
|
303
|
+
)
|
|
304
|
+
# Improved pattern for code blocks
|
|
305
|
+
code_blocks = re.finditer(
|
|
306
|
+
r'```(?:terraform|hcl)?\s*(.*?)```', example_section, re.DOTALL
|
|
307
|
+
)
|
|
308
|
+
code_found = False
|
|
309
|
+
|
|
310
|
+
for code_match in code_blocks:
|
|
311
|
+
code_found = True
|
|
312
|
+
code_snippet = code_match.group(1).strip()
|
|
313
|
+
example_snippets.append({'title': 'Example Usage', 'code': code_snippet})
|
|
314
|
+
logger.debug(
|
|
315
|
+
f'[{correlation_id}] Added direct example snippet ({len(code_snippet)} chars)'
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
if not code_found:
|
|
319
|
+
logger.debug(
|
|
320
|
+
f'[{correlation_id}] No code blocks found in Example Usage section'
|
|
321
|
+
)
|
|
322
|
+
else:
|
|
323
|
+
logger.debug(f'[{correlation_id}] No Example Usage section found')
|
|
324
|
+
|
|
325
|
+
if example_snippets:
|
|
326
|
+
logger.info(f'[{correlation_id}] Found {len(example_snippets)} example snippets')
|
|
327
|
+
else:
|
|
328
|
+
logger.debug(f'[{correlation_id}] No example snippets found')
|
|
329
|
+
|
|
330
|
+
# Extract Arguments Reference section
|
|
331
|
+
arguments = []
|
|
332
|
+
arg_ref_section_match = re.search(
|
|
333
|
+
r'## Argument Reference\n([\s\S]*?)(?=\n## |\Z)', content
|
|
334
|
+
)
|
|
335
|
+
if arg_ref_section_match:
|
|
336
|
+
arg_section = arg_ref_section_match.group(1).strip()
|
|
337
|
+
logger.debug(
|
|
338
|
+
f'[{correlation_id}] Found Argument Reference section ({len(arg_section)} chars)'
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Look for arguments directly under the main Argument Reference section
|
|
342
|
+
args_under_main_section_match = re.search(
|
|
343
|
+
r'(.*?)(?=\n###|\n##|$)', arg_section, re.DOTALL
|
|
344
|
+
)
|
|
345
|
+
if args_under_main_section_match:
|
|
346
|
+
args_under_main_section = args_under_main_section_match.group(1).strip()
|
|
347
|
+
logger.debug(
|
|
348
|
+
f'[{correlation_id}] Found arguments directly under the Argument Reference section ({len(args_under_main_section)} chars)'
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Find arguments in this subsection
|
|
352
|
+
arg_matches = re.finditer(
|
|
353
|
+
r'\*\s+`([^`]+)`\s+-\s+(.*?)(?=\n\*\s+`|$)',
|
|
354
|
+
args_under_main_section,
|
|
355
|
+
re.DOTALL,
|
|
356
|
+
)
|
|
357
|
+
arg_list = list(arg_matches)
|
|
358
|
+
logger.debug(
|
|
359
|
+
f'[{correlation_id}] Found {len(arg_list)} arguments directly under the Argument Reference section'
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
for match in arg_list:
|
|
363
|
+
arg_name = match.group(1).strip()
|
|
364
|
+
arg_desc = match.group(2).strip() if match.group(2) else None
|
|
365
|
+
# Do not add arguments that do not have a description
|
|
366
|
+
if arg_name is not None and arg_desc is not None:
|
|
367
|
+
arguments.append(
|
|
368
|
+
{'name': arg_name, 'description': arg_desc, 'argument_section': 'main'}
|
|
369
|
+
)
|
|
370
|
+
else:
|
|
371
|
+
logger.debug(
|
|
372
|
+
f"[{correlation_id}] Added argument '{arg_name}': '{arg_desc[:50] if arg_desc else 'No description found'}...' (truncated)"
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Now, Find all subheadings in the Argument Reference section with a more robust pattern
|
|
376
|
+
subheading_list = list(
|
|
377
|
+
re.finditer(r'### (.*?)[\r\n]+(.*?)(?=###|\Z)', arg_section, re.DOTALL)
|
|
378
|
+
)
|
|
379
|
+
logger.debug(
|
|
380
|
+
f'[{correlation_id}] Found {len(subheading_list)} subheadings in Argument Reference section'
|
|
381
|
+
)
|
|
382
|
+
subheading_found = False
|
|
383
|
+
|
|
384
|
+
# Check if there are any subheadings
|
|
385
|
+
for match in subheading_list:
|
|
386
|
+
subheading_found = True
|
|
387
|
+
title = match.group(1).strip()
|
|
388
|
+
subcontent = match.group(2).strip()
|
|
389
|
+
logger.debug(
|
|
390
|
+
f"[{correlation_id}] Found subheading '{title}' with {len(subcontent)} chars content"
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Find arguments in this subsection
|
|
394
|
+
arg_matches = re.finditer(
|
|
395
|
+
r'\*\s+`([^`]+)`\s+-\s+(.*?)(?=\n\*\s+`|$)',
|
|
396
|
+
subcontent,
|
|
397
|
+
re.DOTALL,
|
|
398
|
+
)
|
|
399
|
+
arg_list = list(arg_matches)
|
|
400
|
+
logger.debug(
|
|
401
|
+
f'[{correlation_id}] Found {len(arg_list)} arguments in subheading {title}'
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
for match in arg_list:
|
|
405
|
+
arg_name = match.group(1).strip()
|
|
406
|
+
arg_desc = match.group(2).strip() if match.group(2) else None
|
|
407
|
+
# Do not add arguments that do not have a description
|
|
408
|
+
if arg_name is not None and arg_desc is not None:
|
|
409
|
+
arguments.append(
|
|
410
|
+
{'name': arg_name, 'description': arg_desc, 'argument_section': title}
|
|
411
|
+
)
|
|
412
|
+
else:
|
|
413
|
+
logger.debug(
|
|
414
|
+
f"[{correlation_id}] Added argument '{arg_name}': '{arg_desc[:50] if arg_desc else 'No description found'}...' (truncated)"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
arguments = arguments if arguments else None
|
|
418
|
+
if arguments:
|
|
419
|
+
logger.info(
|
|
420
|
+
f'[{correlation_id}] Found {len(arguments)} arguments across all sections'
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
else:
|
|
424
|
+
logger.debug(f'[{correlation_id}] No Argument Reference section found')
|
|
425
|
+
|
|
426
|
+
# Extract Attributes Reference section
|
|
427
|
+
attributes = []
|
|
428
|
+
attr_ref_match = re.search(r'## Attribute Reference\n([\s\S]*?)(?=\n## |\Z)', content)
|
|
429
|
+
if attr_ref_match:
|
|
430
|
+
attr_section = attr_ref_match.group(1).strip()
|
|
431
|
+
logger.debug(
|
|
432
|
+
f'[{correlation_id}] Found Attribute Reference section ({len(attr_section)} chars)'
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# Parse attributes - similar format to arguments
|
|
436
|
+
attr_matches = re.finditer(
|
|
437
|
+
r'[*-]\s+[`"]?([^`":\n]+)[`"]?(?:[`":\s-]+)?(.*?)(?=\n[*-]|\n\n|\Z)',
|
|
438
|
+
attr_section,
|
|
439
|
+
re.DOTALL,
|
|
440
|
+
)
|
|
441
|
+
attr_list = list(attr_matches)
|
|
442
|
+
logger.debug(
|
|
443
|
+
f'[{correlation_id}] Found {len(attr_list)} attributes in Attribute Reference section'
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
for match in attr_list:
|
|
447
|
+
attr_name = match.group(1).strip()
|
|
448
|
+
attr_desc = (
|
|
449
|
+
match.group(2).strip() if match.group(2) else 'No description available'
|
|
450
|
+
)
|
|
451
|
+
attributes.append({'name': attr_name, 'description': attr_desc})
|
|
452
|
+
logger.debug(
|
|
453
|
+
f"[{correlation_id}] Added attribute '{attr_name}': '{attr_desc[:50]}...' (truncated)"
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
attributes = attributes if attributes else None
|
|
457
|
+
if attributes:
|
|
458
|
+
logger.info(f'[{correlation_id}] Found {len(attributes)} attributes')
|
|
459
|
+
else:
|
|
460
|
+
logger.debug(f'[{correlation_id}] No Attribute Reference section found')
|
|
461
|
+
|
|
462
|
+
# Return the parsed information
|
|
463
|
+
parse_time = time.time() - start_time
|
|
464
|
+
logger.debug(f'[{correlation_id}] Markdown parsing completed in {parse_time:.2f} seconds')
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
'title': title,
|
|
468
|
+
'description': description,
|
|
469
|
+
'example_snippets': example_snippets,
|
|
470
|
+
'url': url,
|
|
471
|
+
'arguments': arguments,
|
|
472
|
+
'attributes': attributes,
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
except Exception as e:
|
|
476
|
+
logger.exception(f'[{correlation_id}] Error parsing markdown content')
|
|
477
|
+
logger.error(f'[{correlation_id}] Error type: {type(e).__name__}, message: {str(e)}')
|
|
478
|
+
|
|
479
|
+
# Return partial info if available
|
|
480
|
+
return {
|
|
481
|
+
'title': f'AWS {asset_name}',
|
|
482
|
+
'description': f'Documentation for AWS {asset_name} (Error parsing details: {str(e)})',
|
|
483
|
+
'url': url,
|
|
484
|
+
'example_snippets': None,
|
|
485
|
+
'arguments': None,
|
|
486
|
+
'attributes': None,
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
async def search_aws_provider_docs_impl(
|
|
491
|
+
asset_name: str, asset_type: str = 'resource', cache_enabled: bool = False
|
|
492
|
+
) -> List[TerraformAWSProviderDocsResult]:
|
|
493
|
+
"""Search AWS provider documentation for resources and data sources.
|
|
494
|
+
|
|
495
|
+
This tool searches the Terraform AWS provider documentation for information about
|
|
496
|
+
specific assets, which can either be resources or data sources. It retrieves comprehensive details including
|
|
497
|
+
descriptions, example code snippets, argument references, and attribute references.
|
|
498
|
+
|
|
499
|
+
The implementation fetches documentation directly from the official Terraform AWS provider
|
|
500
|
+
GitHub repository to ensure the most up-to-date information. Results are cached for
|
|
501
|
+
improved performance on subsequent queries.
|
|
502
|
+
|
|
503
|
+
Use the 'asset_type' parameter to specify if you are looking for information about provider
|
|
504
|
+
resources, data sources, or both. The tool will automatically handle prefixes - you can
|
|
505
|
+
search for either 'aws_s3_bucket' or 's3_bucket'.
|
|
506
|
+
|
|
507
|
+
Examples:
|
|
508
|
+
- To get documentation for an S3 bucket resource:
|
|
509
|
+
search_aws_provider_docs_impl(asset_name='aws_s3_bucket')
|
|
510
|
+
|
|
511
|
+
- To search only for data sources:
|
|
512
|
+
search_aws_provider_docs_impl(asset_name='aws_ami', asset_type='data_source')
|
|
513
|
+
|
|
514
|
+
- To search only for resources:
|
|
515
|
+
search_aws_provider_docs_impl(asset_name='aws_instance', asset_type='resource')
|
|
516
|
+
|
|
517
|
+
Parameters:
|
|
518
|
+
asset_name: Name of the AWS Provider resource or data source to look for (e.g., 'aws_s3_bucket', 'aws_lambda_function')
|
|
519
|
+
asset_type: Type of documentation to search - 'resource' (default), 'data_source', or 'both'. Some resources and data sources share the same name.
|
|
520
|
+
cache_enabled: Whether the local cache of results is enabled or not
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
A list of matching documentation entries with details including:
|
|
524
|
+
- Asset name, type, and description
|
|
525
|
+
- URL to the official documentation
|
|
526
|
+
- Example code snippets
|
|
527
|
+
- Arguments with descriptions
|
|
528
|
+
- Attributes with descriptions
|
|
529
|
+
"""
|
|
530
|
+
start_time = time.time()
|
|
531
|
+
correlation_id = f'search-{int(start_time * 1000)}'
|
|
532
|
+
logger.info(f"[{correlation_id}] Starting AWS provider docs search for '{asset_name}'")
|
|
533
|
+
|
|
534
|
+
# Validate input parameters
|
|
535
|
+
if not isinstance(asset_name, str) or not asset_name:
|
|
536
|
+
logger.error(f'[{correlation_id}] Invalid asset_name parameter: {asset_name}')
|
|
537
|
+
return [
|
|
538
|
+
TerraformAWSProviderDocsResult(
|
|
539
|
+
asset_name='Error',
|
|
540
|
+
asset_type=cast(Literal['both', 'resource', 'data_source'], asset_type),
|
|
541
|
+
description='Invalid asset_name parameter. Must be a non-empty string.',
|
|
542
|
+
url=None,
|
|
543
|
+
example_usage=None,
|
|
544
|
+
arguments=None,
|
|
545
|
+
attributes=None,
|
|
546
|
+
)
|
|
547
|
+
]
|
|
548
|
+
|
|
549
|
+
# Validate asset_type
|
|
550
|
+
valid_asset_types = ['resource', 'data_source', 'both']
|
|
551
|
+
if asset_type not in valid_asset_types:
|
|
552
|
+
logger.error(f'[{correlation_id}] Invalid asset_type parameter: {asset_type}')
|
|
553
|
+
return [
|
|
554
|
+
TerraformAWSProviderDocsResult(
|
|
555
|
+
asset_name='Error',
|
|
556
|
+
asset_type=cast(Literal['both', 'resource', 'data_source'], 'resource'),
|
|
557
|
+
description=f'Invalid asset_type parameter. Must be one of {valid_asset_types}.',
|
|
558
|
+
url=None,
|
|
559
|
+
example_usage=None,
|
|
560
|
+
arguments=None,
|
|
561
|
+
attributes=None,
|
|
562
|
+
)
|
|
563
|
+
]
|
|
564
|
+
|
|
565
|
+
search_term = asset_name.lower()
|
|
566
|
+
|
|
567
|
+
try:
|
|
568
|
+
# Try fetching from GitHub
|
|
569
|
+
logger.info(f'[{correlation_id}] Fetching from GitHub')
|
|
570
|
+
|
|
571
|
+
results = []
|
|
572
|
+
|
|
573
|
+
# If asset_type is "both", try both resource and data source paths
|
|
574
|
+
if asset_type == 'both':
|
|
575
|
+
logger.info(f'[{correlation_id}] Searching for both resources and data sources')
|
|
576
|
+
|
|
577
|
+
# First try as a resource
|
|
578
|
+
github_result = fetch_github_documentation(
|
|
579
|
+
search_term, 'resource', cache_enabled, correlation_id
|
|
580
|
+
)
|
|
581
|
+
if github_result:
|
|
582
|
+
logger.info(f'[{correlation_id}] Found documentation as a resource')
|
|
583
|
+
# Create result object
|
|
584
|
+
description = github_result['description']
|
|
585
|
+
|
|
586
|
+
result = TerraformAWSProviderDocsResult(
|
|
587
|
+
asset_name=asset_name,
|
|
588
|
+
asset_type='resource',
|
|
589
|
+
description=description,
|
|
590
|
+
url=github_result['url'],
|
|
591
|
+
example_usage=github_result.get('example_snippets'),
|
|
592
|
+
arguments=github_result.get('arguments'),
|
|
593
|
+
attributes=github_result.get('attributes'),
|
|
594
|
+
)
|
|
595
|
+
results.append(result)
|
|
596
|
+
|
|
597
|
+
# Then try as a data source
|
|
598
|
+
data_result = fetch_github_documentation(
|
|
599
|
+
search_term, 'data_source', cache_enabled, correlation_id
|
|
600
|
+
)
|
|
601
|
+
if data_result:
|
|
602
|
+
logger.info(f'[{correlation_id}] Found documentation as a data source')
|
|
603
|
+
# Create result object
|
|
604
|
+
description = data_result['description']
|
|
605
|
+
|
|
606
|
+
result = TerraformAWSProviderDocsResult(
|
|
607
|
+
asset_name=asset_name,
|
|
608
|
+
asset_type='data_source',
|
|
609
|
+
description=description,
|
|
610
|
+
url=data_result['url'],
|
|
611
|
+
example_usage=data_result.get('example_snippets'),
|
|
612
|
+
arguments=data_result.get('arguments'),
|
|
613
|
+
attributes=data_result.get('attributes'),
|
|
614
|
+
)
|
|
615
|
+
results.append(result)
|
|
616
|
+
|
|
617
|
+
if results:
|
|
618
|
+
logger.info(f'[{correlation_id}] Found {len(results)} documentation entries')
|
|
619
|
+
end_time = time.time()
|
|
620
|
+
logger.info(
|
|
621
|
+
f'[{correlation_id}] Search completed in {end_time - start_time:.2f} seconds (GitHub source)'
|
|
622
|
+
)
|
|
623
|
+
return results
|
|
624
|
+
else:
|
|
625
|
+
# Search for either resource or data source based on asset_type parameter
|
|
626
|
+
github_result = fetch_github_documentation(
|
|
627
|
+
search_term, asset_type, cache_enabled, correlation_id
|
|
628
|
+
)
|
|
629
|
+
if github_result:
|
|
630
|
+
logger.info(f'[{correlation_id}] Successfully found GitHub documentation')
|
|
631
|
+
|
|
632
|
+
# Create result object
|
|
633
|
+
description = github_result['description']
|
|
634
|
+
result = TerraformAWSProviderDocsResult(
|
|
635
|
+
asset_name=asset_name,
|
|
636
|
+
asset_type=cast(Literal['both', 'resource', 'data_source'], asset_type),
|
|
637
|
+
description=description,
|
|
638
|
+
url=github_result['url'],
|
|
639
|
+
example_usage=github_result.get('example_snippets'),
|
|
640
|
+
arguments=github_result.get('arguments'),
|
|
641
|
+
attributes=github_result.get('attributes'),
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
end_time = time.time()
|
|
645
|
+
logger.info(
|
|
646
|
+
f'[{correlation_id}] Search completed in {end_time - start_time:.2f} seconds (GitHub source)'
|
|
647
|
+
)
|
|
648
|
+
return [result]
|
|
649
|
+
|
|
650
|
+
# If GitHub approach fails, return a "not found" result
|
|
651
|
+
logger.warning(f"[{correlation_id}] Documentation not found on GitHub for '{search_term}'")
|
|
652
|
+
|
|
653
|
+
# Return a "not found" result
|
|
654
|
+
logger.warning(f'[{correlation_id}] No documentation found for asset {asset_name}')
|
|
655
|
+
end_time = time.time()
|
|
656
|
+
logger.info(
|
|
657
|
+
f'[{correlation_id}] Search completed in {end_time - start_time:.2f} seconds (no results)'
|
|
658
|
+
)
|
|
659
|
+
return [
|
|
660
|
+
TerraformAWSProviderDocsResult(
|
|
661
|
+
asset_name='Not found',
|
|
662
|
+
asset_type=cast(Literal['both', 'resource', 'data_source'], asset_type),
|
|
663
|
+
description=f"No documentation found for resource type '{asset_name}'.",
|
|
664
|
+
url=None,
|
|
665
|
+
example_usage=None,
|
|
666
|
+
arguments=None,
|
|
667
|
+
attributes=None,
|
|
668
|
+
)
|
|
669
|
+
]
|
|
670
|
+
|
|
671
|
+
except Exception as e:
|
|
672
|
+
logger.error(
|
|
673
|
+
f'[{correlation_id}] Error searching AWS provider docs: {type(e).__name__}: {str(e)}'
|
|
674
|
+
)
|
|
675
|
+
# Don't log the full stack trace to avoid information disclosure
|
|
676
|
+
|
|
677
|
+
end_time = time.time()
|
|
678
|
+
logger.info(f'[{correlation_id}] Search failed in {end_time - start_time:.2f} seconds')
|
|
679
|
+
|
|
680
|
+
# Return a generic error message without exposing internal details
|
|
681
|
+
return [
|
|
682
|
+
TerraformAWSProviderDocsResult(
|
|
683
|
+
asset_name='Error',
|
|
684
|
+
asset_type=cast(Literal['both', 'resource', 'data_source'], asset_type),
|
|
685
|
+
description='Failed to search AWS provider documentation. Please check your input and try again.',
|
|
686
|
+
url=f'{AWS_DOCS_BASE_URL}/resources',
|
|
687
|
+
example_usage=None,
|
|
688
|
+
arguments=None,
|
|
689
|
+
attributes=None,
|
|
690
|
+
)
|
|
691
|
+
]
|