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,320 @@
|
|
|
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 Terragrunt command execution tool."""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import re
|
|
20
|
+
import subprocess
|
|
21
|
+
from awslabs.terraform_mcp_server.impl.tools.utils import get_dangerous_patterns
|
|
22
|
+
from awslabs.terraform_mcp_server.models import (
|
|
23
|
+
TerragruntExecutionRequest,
|
|
24
|
+
TerragruntExecutionResult,
|
|
25
|
+
)
|
|
26
|
+
from loguru import logger
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def execute_terragrunt_command_impl(
|
|
30
|
+
request: TerragruntExecutionRequest,
|
|
31
|
+
) -> TerragruntExecutionResult:
|
|
32
|
+
"""Execute Terragrunt workflow commands against an AWS account.
|
|
33
|
+
|
|
34
|
+
This tool runs Terragrunt commands (init, plan, validate, apply, destroy, run-all) in the
|
|
35
|
+
specified working directory, with optional variables and region settings.
|
|
36
|
+
|
|
37
|
+
Parameters:
|
|
38
|
+
request: Details about the Terragrunt command to execute
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
A TerragruntExecutionResult object containing command output and status
|
|
42
|
+
"""
|
|
43
|
+
logger.info(f"Executing 'terragrunt {request.command}' in {request.working_directory}")
|
|
44
|
+
|
|
45
|
+
# Helper function to clean output text
|
|
46
|
+
def clean_output_text(text: str) -> str:
|
|
47
|
+
"""Clean output text by removing or replacing problematic Unicode characters.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
text: The text to clean
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Cleaned text with ASCII-friendly replacements
|
|
54
|
+
"""
|
|
55
|
+
if not text:
|
|
56
|
+
return text
|
|
57
|
+
|
|
58
|
+
# First remove ANSI escape sequences (color codes, cursor movement)
|
|
59
|
+
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
60
|
+
text = ansi_escape.sub('', text)
|
|
61
|
+
|
|
62
|
+
# Remove C0 and C1 control characters (except common whitespace)
|
|
63
|
+
control_chars = re.compile(r'[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F]')
|
|
64
|
+
text = control_chars.sub('', text)
|
|
65
|
+
|
|
66
|
+
# Replace HTML entities
|
|
67
|
+
html_entities = {
|
|
68
|
+
'->': '->', # Replace HTML arrow
|
|
69
|
+
'<': '<', # Less than
|
|
70
|
+
'>': '>', # Greater than
|
|
71
|
+
'&': '&', # Ampersand
|
|
72
|
+
}
|
|
73
|
+
for entity, replacement in html_entities.items():
|
|
74
|
+
text = text.replace(entity, replacement)
|
|
75
|
+
|
|
76
|
+
# Replace box-drawing and other special Unicode characters with ASCII equivalents
|
|
77
|
+
unicode_chars = {
|
|
78
|
+
'\u2500': '-', # Horizontal line
|
|
79
|
+
'\u2502': '|', # Vertical line
|
|
80
|
+
'\u2514': '+', # Up and right
|
|
81
|
+
'\u2518': '+', # Up and left
|
|
82
|
+
'\u2551': '|', # Double vertical
|
|
83
|
+
'\u2550': '-', # Double horizontal
|
|
84
|
+
'\u2554': '+', # Double down and right
|
|
85
|
+
'\u2557': '+', # Double down and left
|
|
86
|
+
'\u255a': '+', # Double up and right
|
|
87
|
+
'\u255d': '+', # Double up and left
|
|
88
|
+
'\u256c': '+', # Double cross
|
|
89
|
+
'\u2588': '#', # Full block
|
|
90
|
+
'\u25cf': '*', # Black circle
|
|
91
|
+
'\u2574': '-', # Left box drawing
|
|
92
|
+
'\u2576': '-', # Right box drawing
|
|
93
|
+
'\u2577': '|', # Down box drawing
|
|
94
|
+
'\u2575': '|', # Up box drawing
|
|
95
|
+
}
|
|
96
|
+
for char, replacement in unicode_chars.items():
|
|
97
|
+
text = text.replace(char, replacement)
|
|
98
|
+
|
|
99
|
+
return text
|
|
100
|
+
|
|
101
|
+
# Set environment variables for AWS region if provided
|
|
102
|
+
env = os.environ.copy()
|
|
103
|
+
if request.aws_region:
|
|
104
|
+
env['AWS_REGION'] = request.aws_region
|
|
105
|
+
|
|
106
|
+
# Security check for command injection
|
|
107
|
+
allowed_commands = ['init', 'plan', 'validate', 'apply', 'destroy', 'output', 'run-all']
|
|
108
|
+
if request.command not in allowed_commands:
|
|
109
|
+
logger.error(f'Invalid Terragrunt command: {request.command}')
|
|
110
|
+
return TerragruntExecutionResult(
|
|
111
|
+
command=f'terragrunt {request.command}',
|
|
112
|
+
status='error',
|
|
113
|
+
error_message=f'Invalid Terragrunt command: {request.command}. Allowed commands are: {", ".join(allowed_commands)}',
|
|
114
|
+
working_directory=request.working_directory,
|
|
115
|
+
outputs=None,
|
|
116
|
+
affected_dirs=None,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Validate that terragrunt_config is not used with run-all
|
|
120
|
+
if request.terragrunt_config and request.command == 'run-all':
|
|
121
|
+
logger.error('terragrunt_config cannot be used with run-all command')
|
|
122
|
+
return TerragruntExecutionResult(
|
|
123
|
+
command=f'terragrunt {request.command}',
|
|
124
|
+
status='error',
|
|
125
|
+
error_message='Invalid configuration: --terragrunt-config cannot be used with run-all command',
|
|
126
|
+
working_directory=request.working_directory,
|
|
127
|
+
outputs=None,
|
|
128
|
+
affected_dirs=None,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Check for potentially dangerous characters or command injection attempts
|
|
132
|
+
dangerous_patterns = get_dangerous_patterns()
|
|
133
|
+
logger.debug(f'Checking for {len(dangerous_patterns)} dangerous patterns')
|
|
134
|
+
|
|
135
|
+
for pattern in dangerous_patterns:
|
|
136
|
+
if request.variables:
|
|
137
|
+
# Check if the pattern is in any of the variable values
|
|
138
|
+
for var_name, var_value in request.variables.items():
|
|
139
|
+
if pattern in str(var_value) or pattern in str(var_name):
|
|
140
|
+
logger.error(
|
|
141
|
+
f'Potentially dangerous pattern detected in variable {var_name}: {pattern}'
|
|
142
|
+
)
|
|
143
|
+
return TerragruntExecutionResult(
|
|
144
|
+
command=f'terragrunt {request.command}',
|
|
145
|
+
status='error',
|
|
146
|
+
error_message=f"Security violation: Potentially dangerous pattern '{pattern}' detected in variable '{var_name}'",
|
|
147
|
+
working_directory=request.working_directory,
|
|
148
|
+
outputs=None,
|
|
149
|
+
affected_dirs=None,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Check terragrunt_config for dangerous patterns
|
|
153
|
+
if request.terragrunt_config and pattern in str(request.terragrunt_config):
|
|
154
|
+
logger.error(f'Potentially dangerous pattern detected in terragrunt_config: {pattern}')
|
|
155
|
+
return TerragruntExecutionResult(
|
|
156
|
+
command=f'terragrunt {request.command}',
|
|
157
|
+
status='error',
|
|
158
|
+
error_message=f"Security violation: Potentially dangerous pattern '{pattern}' detected in terragrunt_config",
|
|
159
|
+
working_directory=request.working_directory,
|
|
160
|
+
outputs=None,
|
|
161
|
+
affected_dirs=None,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Also check include_dirs and exclude_dirs for dangerous patterns
|
|
165
|
+
if request.include_dirs:
|
|
166
|
+
for dir_path in request.include_dirs:
|
|
167
|
+
if pattern in str(dir_path):
|
|
168
|
+
logger.error(
|
|
169
|
+
f'Potentially dangerous pattern detected in include_dirs: {pattern}'
|
|
170
|
+
)
|
|
171
|
+
return TerragruntExecutionResult(
|
|
172
|
+
command=f'terragrunt {request.command}',
|
|
173
|
+
status='error',
|
|
174
|
+
error_message=f"Security violation: Potentially dangerous pattern '{pattern}' detected in include_dirs",
|
|
175
|
+
working_directory=request.working_directory,
|
|
176
|
+
outputs=None,
|
|
177
|
+
affected_dirs=None,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if request.exclude_dirs:
|
|
181
|
+
for dir_path in request.exclude_dirs:
|
|
182
|
+
if pattern in str(dir_path):
|
|
183
|
+
logger.error(
|
|
184
|
+
f'Potentially dangerous pattern detected in exclude_dirs: {pattern}'
|
|
185
|
+
)
|
|
186
|
+
return TerragruntExecutionResult(
|
|
187
|
+
command=f'terragrunt {request.command}',
|
|
188
|
+
status='error',
|
|
189
|
+
error_message=f"Security violation: Potentially dangerous pattern '{pattern}' detected in exclude_dirs",
|
|
190
|
+
working_directory=request.working_directory,
|
|
191
|
+
outputs=None,
|
|
192
|
+
affected_dirs=None,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Build the command
|
|
196
|
+
base_cmd = ['terragrunt']
|
|
197
|
+
|
|
198
|
+
# Handle run-all command differently
|
|
199
|
+
if request.command == 'run-all':
|
|
200
|
+
base_cmd.append('run-all')
|
|
201
|
+
# The actual terraform command becomes the first argument
|
|
202
|
+
# Default to 'apply' if not specified in the command
|
|
203
|
+
base_cmd.append('apply')
|
|
204
|
+
else:
|
|
205
|
+
base_cmd.append(request.command)
|
|
206
|
+
|
|
207
|
+
# Add auto-approve flag for apply and destroy commands to make them non-interactive
|
|
208
|
+
if request.command in ['apply', 'destroy'] or (request.command == 'run-all'):
|
|
209
|
+
logger.info(f'Adding -auto-approve flag to {request.command} command')
|
|
210
|
+
base_cmd.append('-auto-approve')
|
|
211
|
+
|
|
212
|
+
# Add terragrunt_config if specified and not using run-all
|
|
213
|
+
if request.terragrunt_config:
|
|
214
|
+
logger.info(f'Using custom terragrunt config file: {request.terragrunt_config}')
|
|
215
|
+
base_cmd.append(f'--terragrunt-config={request.terragrunt_config}')
|
|
216
|
+
|
|
217
|
+
# Add variables only for commands that accept them (plan, apply, destroy, output)
|
|
218
|
+
if request.command in ['plan', 'apply', 'destroy', 'output', 'run-all'] and request.variables:
|
|
219
|
+
logger.info(f'Adding {len(request.variables)} variables to {request.command} command')
|
|
220
|
+
for key, value in request.variables.items():
|
|
221
|
+
base_cmd.append(f'-var={key}={value}')
|
|
222
|
+
|
|
223
|
+
# Add include-dirs if specified
|
|
224
|
+
if request.include_dirs and request.command == 'run-all':
|
|
225
|
+
for dir_path in request.include_dirs:
|
|
226
|
+
base_cmd.append(f'--queue-include-dir={dir_path}')
|
|
227
|
+
|
|
228
|
+
# Add exclude-dirs if specified
|
|
229
|
+
if request.exclude_dirs and request.command == 'run-all':
|
|
230
|
+
for dir_path in request.exclude_dirs:
|
|
231
|
+
base_cmd.append(f'--queue-exclude-dir={dir_path}')
|
|
232
|
+
|
|
233
|
+
# Execute command
|
|
234
|
+
try:
|
|
235
|
+
# nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit
|
|
236
|
+
# Safe: Command is validated against allowlist, variables are checked for dangerous patterns,
|
|
237
|
+
# working_directory is user-controlled but subprocess uses cwd parameter (not shell injection)
|
|
238
|
+
process = subprocess.run( # noqa: B603 - Safe: allowlisted commands, validated variables, no shell injection
|
|
239
|
+
base_cmd, cwd=request.working_directory, capture_output=True, text=True, env=env
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Prepare the result
|
|
243
|
+
stdout = process.stdout
|
|
244
|
+
stderr = process.stderr if process.stderr else ''
|
|
245
|
+
|
|
246
|
+
# Clean output text if requested
|
|
247
|
+
if request.strip_ansi:
|
|
248
|
+
logger.debug('Cleaning command output text (ANSI codes and control characters)')
|
|
249
|
+
stdout = clean_output_text(stdout)
|
|
250
|
+
stderr = clean_output_text(stderr)
|
|
251
|
+
|
|
252
|
+
# Extract affected directories for run-all command
|
|
253
|
+
affected_dirs = None
|
|
254
|
+
if request.command == 'run-all':
|
|
255
|
+
affected_dirs = []
|
|
256
|
+
# Look for directory paths in the output
|
|
257
|
+
dir_pattern = re.compile(r'Module at\s+"([^"]+)"')
|
|
258
|
+
for match in dir_pattern.finditer(stdout):
|
|
259
|
+
affected_dirs.append(match.group(1))
|
|
260
|
+
|
|
261
|
+
result = {
|
|
262
|
+
'command': f'terragrunt {request.command}',
|
|
263
|
+
'status': 'success' if process.returncode == 0 else 'error',
|
|
264
|
+
'return_code': process.returncode,
|
|
265
|
+
'stdout': stdout,
|
|
266
|
+
'stderr': stderr,
|
|
267
|
+
'working_directory': request.working_directory,
|
|
268
|
+
'outputs': None,
|
|
269
|
+
'affected_dirs': affected_dirs,
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
# Get outputs if this was a successful apply or output command
|
|
273
|
+
if (
|
|
274
|
+
request.command in ['apply', 'output'] or (request.command == 'run-all')
|
|
275
|
+
) and process.returncode == 0:
|
|
276
|
+
try:
|
|
277
|
+
logger.info('Getting Terragrunt outputs')
|
|
278
|
+
output_process = subprocess.run( # noqa: B603 - Safe: hardcoded terragrunt output command with no user input
|
|
279
|
+
['terragrunt', 'output', '-json'],
|
|
280
|
+
cwd=request.working_directory,
|
|
281
|
+
capture_output=True,
|
|
282
|
+
text=True,
|
|
283
|
+
env=env,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if output_process.returncode == 0 and output_process.stdout:
|
|
287
|
+
# Get output and clean it if needed
|
|
288
|
+
output_stdout = output_process.stdout
|
|
289
|
+
if request.strip_ansi:
|
|
290
|
+
output_stdout = clean_output_text(output_stdout)
|
|
291
|
+
|
|
292
|
+
# Parse the JSON output
|
|
293
|
+
raw_outputs = json.loads(output_stdout)
|
|
294
|
+
|
|
295
|
+
# Process outputs to extract values from complex structure
|
|
296
|
+
processed_outputs = {}
|
|
297
|
+
for key, value in raw_outputs.items():
|
|
298
|
+
# Terraform outputs in JSON format have a nested structure
|
|
299
|
+
# with 'value', 'type', and sometimes 'sensitive'
|
|
300
|
+
if isinstance(value, dict) and 'value' in value:
|
|
301
|
+
processed_outputs[key] = value['value']
|
|
302
|
+
else:
|
|
303
|
+
processed_outputs[key] = value
|
|
304
|
+
|
|
305
|
+
result['outputs'] = processed_outputs
|
|
306
|
+
logger.info(f'Extracted {len(processed_outputs)} Terragrunt outputs')
|
|
307
|
+
except Exception as e:
|
|
308
|
+
logger.warning(f'Failed to get Terragrunt outputs: {e}')
|
|
309
|
+
|
|
310
|
+
# Return the output
|
|
311
|
+
return TerragruntExecutionResult(**result)
|
|
312
|
+
except Exception as e:
|
|
313
|
+
return TerragruntExecutionResult(
|
|
314
|
+
command=f'terragrunt {request.command}',
|
|
315
|
+
status='error',
|
|
316
|
+
error_message=str(e),
|
|
317
|
+
working_directory=request.working_directory,
|
|
318
|
+
outputs=None,
|
|
319
|
+
affected_dirs=None,
|
|
320
|
+
)
|