diagram-to-iac 0.7.0__py3-none-any.whl → 0.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.
- diagram_to_iac/__init__.py +10 -0
- diagram_to_iac/actions/__init__.py +7 -0
- diagram_to_iac/actions/git_entry.py +174 -0
- diagram_to_iac/actions/supervisor_entry.py +116 -0
- diagram_to_iac/actions/terraform_agent_entry.py +207 -0
- diagram_to_iac/agents/__init__.py +26 -0
- diagram_to_iac/agents/demonstrator_langgraph/__init__.py +10 -0
- diagram_to_iac/agents/demonstrator_langgraph/agent.py +826 -0
- diagram_to_iac/agents/git_langgraph/__init__.py +10 -0
- diagram_to_iac/agents/git_langgraph/agent.py +1018 -0
- diagram_to_iac/agents/git_langgraph/pr.py +146 -0
- diagram_to_iac/agents/hello_langgraph/__init__.py +9 -0
- diagram_to_iac/agents/hello_langgraph/agent.py +621 -0
- diagram_to_iac/agents/policy_agent/__init__.py +15 -0
- diagram_to_iac/agents/policy_agent/agent.py +507 -0
- diagram_to_iac/agents/policy_agent/integration_example.py +191 -0
- diagram_to_iac/agents/policy_agent/tools/__init__.py +14 -0
- diagram_to_iac/agents/policy_agent/tools/tfsec_tool.py +259 -0
- diagram_to_iac/agents/shell_langgraph/__init__.py +21 -0
- diagram_to_iac/agents/shell_langgraph/agent.py +122 -0
- diagram_to_iac/agents/shell_langgraph/detector.py +50 -0
- diagram_to_iac/agents/supervisor_langgraph/__init__.py +17 -0
- diagram_to_iac/agents/supervisor_langgraph/agent.py +1947 -0
- diagram_to_iac/agents/supervisor_langgraph/demonstrator.py +22 -0
- diagram_to_iac/agents/supervisor_langgraph/guards.py +23 -0
- diagram_to_iac/agents/supervisor_langgraph/pat_loop.py +49 -0
- diagram_to_iac/agents/supervisor_langgraph/router.py +9 -0
- diagram_to_iac/agents/terraform_langgraph/__init__.py +15 -0
- diagram_to_iac/agents/terraform_langgraph/agent.py +1216 -0
- diagram_to_iac/agents/terraform_langgraph/parser.py +76 -0
- diagram_to_iac/core/__init__.py +7 -0
- diagram_to_iac/core/agent_base.py +19 -0
- diagram_to_iac/core/enhanced_memory.py +302 -0
- diagram_to_iac/core/errors.py +4 -0
- diagram_to_iac/core/issue_tracker.py +49 -0
- diagram_to_iac/core/memory.py +132 -0
- diagram_to_iac/services/__init__.py +10 -0
- diagram_to_iac/services/observability.py +59 -0
- diagram_to_iac/services/step_summary.py +77 -0
- diagram_to_iac/tools/__init__.py +11 -0
- diagram_to_iac/tools/api_utils.py +108 -26
- diagram_to_iac/tools/git/__init__.py +45 -0
- diagram_to_iac/tools/git/git.py +956 -0
- diagram_to_iac/tools/hello/__init__.py +30 -0
- diagram_to_iac/tools/hello/cal_utils.py +31 -0
- diagram_to_iac/tools/hello/text_utils.py +97 -0
- diagram_to_iac/tools/llm_utils/__init__.py +20 -0
- diagram_to_iac/tools/llm_utils/anthropic_driver.py +87 -0
- diagram_to_iac/tools/llm_utils/base_driver.py +90 -0
- diagram_to_iac/tools/llm_utils/gemini_driver.py +89 -0
- diagram_to_iac/tools/llm_utils/openai_driver.py +93 -0
- diagram_to_iac/tools/llm_utils/router.py +303 -0
- diagram_to_iac/tools/sec_utils.py +4 -2
- diagram_to_iac/tools/shell/__init__.py +17 -0
- diagram_to_iac/tools/shell/shell.py +415 -0
- diagram_to_iac/tools/text_utils.py +277 -0
- diagram_to_iac/tools/tf/terraform.py +851 -0
- diagram_to_iac-0.8.0.dist-info/METADATA +99 -0
- diagram_to_iac-0.8.0.dist-info/RECORD +64 -0
- {diagram_to_iac-0.7.0.dist-info → diagram_to_iac-0.8.0.dist-info}/WHEEL +1 -1
- diagram_to_iac-0.8.0.dist-info/entry_points.txt +4 -0
- diagram_to_iac/agents/codegen_agent.py +0 -0
- diagram_to_iac/agents/consensus_agent.py +0 -0
- diagram_to_iac/agents/deployment_agent.py +0 -0
- diagram_to_iac/agents/github_agent.py +0 -0
- diagram_to_iac/agents/interpretation_agent.py +0 -0
- diagram_to_iac/agents/question_agent.py +0 -0
- diagram_to_iac/agents/supervisor.py +0 -0
- diagram_to_iac/agents/vision_agent.py +0 -0
- diagram_to_iac/core/config.py +0 -0
- diagram_to_iac/tools/cv_utils.py +0 -0
- diagram_to_iac/tools/gh_utils.py +0 -0
- diagram_to_iac/tools/tf_utils.py +0 -0
- diagram_to_iac-0.7.0.dist-info/METADATA +0 -16
- diagram_to_iac-0.7.0.dist-info/RECORD +0 -32
- diagram_to_iac-0.7.0.dist-info/entry_points.txt +0 -2
- {diagram_to_iac-0.7.0.dist-info → diagram_to_iac-0.8.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,851 @@
|
|
1
|
+
"""
|
2
|
+
Terraform tools implementation following our established comprehensive pattern.
|
3
|
+
|
4
|
+
This module provides secure Terraform operations with:
|
5
|
+
- Configuration-driven behavior via ``shell_exec``
|
6
|
+
- Comprehensive logging with structured messages
|
7
|
+
- Memory integration for operation tracking
|
8
|
+
- Robust error handling with graceful fallbacks
|
9
|
+
- Integration with our ``ShellExecutor`` for safe command execution
|
10
|
+
- Pydantic schemas for type safety and validation
|
11
|
+
"""
|
12
|
+
|
13
|
+
import os
|
14
|
+
import time
|
15
|
+
import logging
|
16
|
+
import yaml
|
17
|
+
from typing import Optional, Dict, Any
|
18
|
+
from pathlib import Path
|
19
|
+
from pydantic import BaseModel, Field, field_validator
|
20
|
+
from langchain_core.tools import tool
|
21
|
+
|
22
|
+
from diagram_to_iac.core.memory import create_memory
|
23
|
+
from diagram_to_iac.tools.shell import get_shell_executor, ShellExecInput
|
24
|
+
|
25
|
+
|
26
|
+
# --- Pydantic Schemas for Terraform Operations ---
|
27
|
+
class TerraformInitInput(BaseModel):
|
28
|
+
"""Input schema for terraform init operations following our established pattern."""
|
29
|
+
repo_path: str = Field(..., description="Path to Terraform repository/directory")
|
30
|
+
upgrade: Optional[bool] = Field(False, description="Pass -upgrade flag to terraform init")
|
31
|
+
backend_config: Optional[Dict[str, str]] = Field(None, description="Backend configuration parameters")
|
32
|
+
|
33
|
+
@field_validator('repo_path')
|
34
|
+
@classmethod
|
35
|
+
def validate_repo_path(cls, v):
|
36
|
+
"""Validate that repo_path exists and is a directory."""
|
37
|
+
if not v or not isinstance(v, str):
|
38
|
+
raise ValueError("Repository path must be a non-empty string")
|
39
|
+
|
40
|
+
path = Path(v)
|
41
|
+
if not path.exists():
|
42
|
+
raise ValueError(f"Repository path does not exist: {v}")
|
43
|
+
|
44
|
+
if not path.is_dir():
|
45
|
+
raise ValueError(f"Repository path must be a directory: {v}")
|
46
|
+
|
47
|
+
return str(path.absolute())
|
48
|
+
|
49
|
+
|
50
|
+
class TerraformInitOutput(BaseModel):
|
51
|
+
"""Output schema for terraform init operations following our established pattern."""
|
52
|
+
status: str = Field(..., description="Operation status: SUCCESS, ERROR, TIMEOUT")
|
53
|
+
error_message: Optional[str] = Field(None, description="Error details if operation failed")
|
54
|
+
duration: float = Field(..., description="Operation duration in seconds")
|
55
|
+
command_executed: Optional[str] = Field(None, description="Terraform command that was executed")
|
56
|
+
repo_path: str = Field(..., description="Repository path used for operation")
|
57
|
+
output: Optional[str] = Field(None, description="Terraform command output")
|
58
|
+
|
59
|
+
|
60
|
+
class TerraformPlanInput(BaseModel):
|
61
|
+
"""Input schema for terraform plan operations following our established pattern."""
|
62
|
+
repo_path: str = Field(..., description="Path to Terraform repository/directory")
|
63
|
+
out_file: Optional[str] = Field(None, description="Output file for plan (default: plan.tfplan)")
|
64
|
+
var_file: Optional[str] = Field(None, description="Variables file to use")
|
65
|
+
vars: Optional[Dict[str, str]] = Field(None, description="Variables to pass to terraform plan")
|
66
|
+
destroy: Optional[bool] = Field(False, description="Create a destroy plan")
|
67
|
+
|
68
|
+
@field_validator('repo_path')
|
69
|
+
@classmethod
|
70
|
+
def validate_repo_path(cls, v):
|
71
|
+
"""Validate that repo_path exists and is a directory."""
|
72
|
+
if not v or not isinstance(v, str):
|
73
|
+
raise ValueError("Repository path must be a non-empty string")
|
74
|
+
|
75
|
+
path = Path(v)
|
76
|
+
if not path.exists():
|
77
|
+
raise ValueError(f"Repository path does not exist: {v}")
|
78
|
+
|
79
|
+
if not path.is_dir():
|
80
|
+
raise ValueError(f"Repository path must be a directory: {v}")
|
81
|
+
|
82
|
+
return str(path.absolute())
|
83
|
+
|
84
|
+
|
85
|
+
class TerraformPlanOutput(BaseModel):
|
86
|
+
"""Output schema for terraform plan operations following our established pattern."""
|
87
|
+
status: str = Field(..., description="Operation status: SUCCESS, ERROR, TIMEOUT")
|
88
|
+
plan_file: Optional[str] = Field(None, description="Path to generated plan file if successful")
|
89
|
+
error_message: Optional[str] = Field(None, description="Error details if operation failed")
|
90
|
+
duration: float = Field(..., description="Operation duration in seconds")
|
91
|
+
command_executed: Optional[str] = Field(None, description="Terraform command that was executed")
|
92
|
+
repo_path: str = Field(..., description="Repository path used for operation")
|
93
|
+
output: Optional[str] = Field(None, description="Terraform command output")
|
94
|
+
|
95
|
+
|
96
|
+
class TerraformApplyInput(BaseModel):
|
97
|
+
"""Input schema for terraform apply operations following our established pattern."""
|
98
|
+
repo_path: str = Field(..., description="Path to Terraform repository/directory")
|
99
|
+
plan_file: Optional[str] = Field(None, description="Plan file to apply (if not provided, applies current state)")
|
100
|
+
auto_approve: Optional[bool] = Field(True, description="Pass -auto-approve flag (default: True for automation)")
|
101
|
+
var_file: Optional[str] = Field(None, description="Variables file to use")
|
102
|
+
vars: Optional[Dict[str, str]] = Field(None, description="Variables to pass to terraform apply")
|
103
|
+
|
104
|
+
@field_validator('repo_path')
|
105
|
+
@classmethod
|
106
|
+
def validate_repo_path(cls, v):
|
107
|
+
"""Validate that repo_path exists and is a directory."""
|
108
|
+
if not v or not isinstance(v, str):
|
109
|
+
raise ValueError("Repository path must be a non-empty string")
|
110
|
+
|
111
|
+
path = Path(v)
|
112
|
+
if not path.exists():
|
113
|
+
raise ValueError(f"Repository path does not exist: {v}")
|
114
|
+
|
115
|
+
if not path.is_dir():
|
116
|
+
raise ValueError(f"Repository path must be a directory: {v}")
|
117
|
+
|
118
|
+
return str(path.absolute())
|
119
|
+
|
120
|
+
|
121
|
+
class TerraformApplyOutput(BaseModel):
|
122
|
+
"""Output schema for terraform apply operations following our established pattern."""
|
123
|
+
status: str = Field(..., description="Operation status: SUCCESS, ERROR, TIMEOUT")
|
124
|
+
error_message: Optional[str] = Field(None, description="Error details if operation failed")
|
125
|
+
duration: float = Field(..., description="Operation duration in seconds")
|
126
|
+
command_executed: Optional[str] = Field(None, description="Terraform command that was executed")
|
127
|
+
repo_path: str = Field(..., description="Repository path used for operation")
|
128
|
+
output: Optional[str] = Field(None, description="Terraform command output")
|
129
|
+
|
130
|
+
|
131
|
+
class TerraformExecutor:
|
132
|
+
"""
|
133
|
+
TerraformExecutor provides secure Terraform operations following our established pattern.
|
134
|
+
|
135
|
+
Features:
|
136
|
+
- Configuration-driven behavior via shell_exec
|
137
|
+
- Comprehensive logging with structured messages
|
138
|
+
- Memory integration for operation tracking
|
139
|
+
- Robust error handling with graceful fallbacks
|
140
|
+
- Integration with ShellExecutor for secure command execution
|
141
|
+
- Terraform-specific error detection and handling
|
142
|
+
"""
|
143
|
+
|
144
|
+
def __init__(self, config_path: str = None, memory_type: str = "persistent"):
|
145
|
+
"""
|
146
|
+
Initialize TerraformExecutor following our established pattern.
|
147
|
+
|
148
|
+
Args:
|
149
|
+
config_path: Optional path to terraform tools configuration file
|
150
|
+
memory_type: Type of memory to use ("persistent", "memory", or "langgraph")
|
151
|
+
"""
|
152
|
+
# Configure logger following our pattern
|
153
|
+
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
|
154
|
+
if not logging.getLogger().hasHandlers():
|
155
|
+
logging.basicConfig(
|
156
|
+
level=logging.INFO,
|
157
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(threadName)s - %(message)s',
|
158
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
159
|
+
)
|
160
|
+
|
161
|
+
# Load configuration following our pattern
|
162
|
+
if config_path is None:
|
163
|
+
# Use shared shell configuration which contains terraform allowlist
|
164
|
+
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
165
|
+
config_path = os.path.join(base_dir, 'agents', 'shell_langgraph', 'tools', 'shell_tools_config.yaml')
|
166
|
+
self.logger.debug(f"Default config path set to: {config_path}")
|
167
|
+
|
168
|
+
try:
|
169
|
+
with open(config_path, 'r') as f:
|
170
|
+
loaded_config = yaml.safe_load(f)
|
171
|
+
if loaded_config is None:
|
172
|
+
self.logger.warning(f"Configuration file at {config_path} is empty. Using default values.")
|
173
|
+
self._set_default_config()
|
174
|
+
else:
|
175
|
+
# Start with default config and merge loaded config
|
176
|
+
self._set_default_config()
|
177
|
+
# Merge shell_executor config from loaded file
|
178
|
+
if 'shell_executor' in loaded_config:
|
179
|
+
self.config['shell_executor'].update(loaded_config['shell_executor'])
|
180
|
+
self.logger.info(f"Terraform configuration loaded and merged from {config_path}")
|
181
|
+
except FileNotFoundError:
|
182
|
+
self.logger.warning(f"Configuration file not found at {config_path}. Using default values.")
|
183
|
+
self._set_default_config()
|
184
|
+
except yaml.YAMLError as e:
|
185
|
+
self.logger.error(f"Error parsing YAML configuration from {config_path}: {e}. Using default values.", exc_info=True)
|
186
|
+
self._set_default_config()
|
187
|
+
|
188
|
+
# Initialize memory system following our pattern
|
189
|
+
self.memory = create_memory(memory_type)
|
190
|
+
self.logger.info(f"Terraform executor memory system initialized: {type(self.memory).__name__}")
|
191
|
+
|
192
|
+
# Initialize shell executor dependency
|
193
|
+
self.shell_executor = get_shell_executor()
|
194
|
+
self.logger.info("Terraform executor initialized with shell executor dependency")
|
195
|
+
|
196
|
+
# Log configuration summary
|
197
|
+
shell_config = self.config.get('shell_executor', {})
|
198
|
+
self.logger.info(f"Terraform executor initialized with workspace: {shell_config.get('workspace_base', '/workspace')}")
|
199
|
+
self.logger.info(f"Default timeout: {shell_config.get('default_timeout', 30)}s")
|
200
|
+
self.logger.info(f"Allowed binaries: {shell_config.get('allowed_binaries', [])}")
|
201
|
+
|
202
|
+
def _set_default_config(self):
|
203
|
+
"""Set default configuration following our established pattern."""
|
204
|
+
self.logger.info("Setting default configuration for TerraformExecutor.")
|
205
|
+
self.config = {
|
206
|
+
'shell_executor': {
|
207
|
+
'allowed_binaries': ['terraform', 'git', 'bash', 'sh'],
|
208
|
+
'default_timeout': 300, # 5 minutes for Terraform operations
|
209
|
+
'workspace_base': '/workspace',
|
210
|
+
'restrict_to_workspace': True,
|
211
|
+
'enable_detailed_logging': True
|
212
|
+
},
|
213
|
+
'terraform_executor': {
|
214
|
+
'default_plan_file': 'plan.tfplan',
|
215
|
+
'default_auto_approve': True,
|
216
|
+
'enable_detailed_logging': True,
|
217
|
+
'store_operations_in_memory': True
|
218
|
+
},
|
219
|
+
'error_messages': {
|
220
|
+
'terraform_not_found': "Terraform binary not found or not allowed",
|
221
|
+
'invalid_repo_path': "Terraform executor: Invalid repository path '{repo_path}'",
|
222
|
+
'init_failed': "Terraform init failed",
|
223
|
+
'plan_failed': "Terraform plan failed",
|
224
|
+
'apply_failed': "Terraform apply failed",
|
225
|
+
'execution_timeout': "Terraform operation timed out after {timeout} seconds"
|
226
|
+
},
|
227
|
+
'success_messages': {
|
228
|
+
'init_started': "Starting terraform init in '{repo_path}'",
|
229
|
+
'init_completed': "Successfully completed terraform init in '{repo_path}' in {duration:.2f}s",
|
230
|
+
'plan_started': "Starting terraform plan in '{repo_path}'",
|
231
|
+
'plan_completed': "Successfully completed terraform plan in '{repo_path}' in {duration:.2f}s",
|
232
|
+
'apply_started': "Starting terraform apply in '{repo_path}'",
|
233
|
+
'apply_completed': "Successfully completed terraform apply in '{repo_path}' in {duration:.2f}s"
|
234
|
+
},
|
235
|
+
'status_codes': {
|
236
|
+
'success': 'SUCCESS',
|
237
|
+
'error': 'ERROR',
|
238
|
+
'timeout': 'TIMEOUT'
|
239
|
+
}
|
240
|
+
}
|
241
|
+
|
242
|
+
def _build_terraform_init_command(self, tf_input: TerraformInitInput) -> str:
|
243
|
+
"""Build terraform init command following our pattern."""
|
244
|
+
# Start with basic init command
|
245
|
+
cmd_parts = ['terraform', 'init']
|
246
|
+
|
247
|
+
# Add upgrade flag if specified
|
248
|
+
if tf_input.upgrade:
|
249
|
+
cmd_parts.append('-upgrade')
|
250
|
+
|
251
|
+
# Add backend config if provided
|
252
|
+
if tf_input.backend_config:
|
253
|
+
for key, value in tf_input.backend_config.items():
|
254
|
+
cmd_parts.extend(['-backend-config', f'{key}={value}'])
|
255
|
+
|
256
|
+
# Join command parts
|
257
|
+
command = ' '.join(cmd_parts)
|
258
|
+
|
259
|
+
self.logger.debug(f"Built terraform init command: {command}")
|
260
|
+
return command
|
261
|
+
|
262
|
+
def _build_terraform_plan_command(self, tf_input: TerraformPlanInput) -> str:
|
263
|
+
"""Build terraform plan command following our pattern."""
|
264
|
+
terraform_config = self.config.get('terraform_executor', {})
|
265
|
+
|
266
|
+
# Start with basic plan command
|
267
|
+
cmd_parts = ['terraform', 'plan']
|
268
|
+
|
269
|
+
# Determine the out_file to use (default if not provided)
|
270
|
+
out_file = tf_input.out_file or terraform_config.get('default_plan_file', 'plan.tfplan')
|
271
|
+
|
272
|
+
# Only add output file if not using remote backend
|
273
|
+
# Remote backends (like Terraform Cloud) don't support saving plans locally
|
274
|
+
if not self._is_remote_backend(tf_input.repo_path):
|
275
|
+
cmd_parts.extend(['-out', out_file])
|
276
|
+
else:
|
277
|
+
self.logger.warning("Remote backend detected - skipping -out flag")
|
278
|
+
|
279
|
+
# Add destroy flag if specified
|
280
|
+
if tf_input.destroy:
|
281
|
+
cmd_parts.append('-destroy')
|
282
|
+
|
283
|
+
# Add var file if specified
|
284
|
+
if tf_input.var_file:
|
285
|
+
cmd_parts.extend(['-var-file', tf_input.var_file])
|
286
|
+
|
287
|
+
# Add variables if provided
|
288
|
+
if tf_input.vars:
|
289
|
+
for key, value in tf_input.vars.items():
|
290
|
+
cmd_parts.extend(['-var', f'{key}={value}'])
|
291
|
+
|
292
|
+
# Join command parts
|
293
|
+
command = ' '.join(cmd_parts)
|
294
|
+
|
295
|
+
self.logger.debug(f"Built terraform plan command: {command}")
|
296
|
+
return command
|
297
|
+
|
298
|
+
def _is_remote_backend(self, repo_path: str) -> bool:
|
299
|
+
"""Check if the repository uses a remote backend configuration."""
|
300
|
+
try:
|
301
|
+
# Check for common remote backend indicators in Terraform files
|
302
|
+
for tf_file in Path(repo_path).glob("*.tf"):
|
303
|
+
with open(tf_file, 'r') as f:
|
304
|
+
content = f.read()
|
305
|
+
# Look for remote backend configurations
|
306
|
+
if any(backend in content.lower() for backend in ['backend "remote"', 'backend "cloud"', 'terraform cloud']):
|
307
|
+
return True
|
308
|
+
return False
|
309
|
+
except Exception as e:
|
310
|
+
self.logger.debug(f"Could not check backend type: {e}")
|
311
|
+
return False # Default to local backend if unsure
|
312
|
+
|
313
|
+
def _setup_terraform_credentials(self) -> Dict[str, str]:
|
314
|
+
"""Setup Terraform credentials for CLI authentication."""
|
315
|
+
env_vars = {}
|
316
|
+
|
317
|
+
# Check for TFE_TOKEN and convert to proper Terraform CLI format
|
318
|
+
tfe_token = os.environ.get('TFE_TOKEN')
|
319
|
+
if tfe_token:
|
320
|
+
# Terraform CLI expects TF_TOKEN_app_terraform_io for app.terraform.io
|
321
|
+
env_vars['TF_TOKEN_app_terraform_io'] = tfe_token
|
322
|
+
self.logger.debug("Terraform Cloud credentials configured via TF_TOKEN_app_terraform_io")
|
323
|
+
else:
|
324
|
+
self.logger.warning("TFE_TOKEN not found - Terraform Cloud operations may fail")
|
325
|
+
|
326
|
+
return env_vars
|
327
|
+
|
328
|
+
def _build_terraform_apply_command(self, tf_input: TerraformApplyInput) -> str:
|
329
|
+
"""Build terraform apply command following our pattern."""
|
330
|
+
terraform_config = self.config.get('terraform_executor', {})
|
331
|
+
|
332
|
+
# Start with basic apply command
|
333
|
+
cmd_parts = ['terraform', 'apply']
|
334
|
+
|
335
|
+
# Add auto-approve flag (default to True for automation)
|
336
|
+
auto_approve = tf_input.auto_approve
|
337
|
+
if auto_approve is None:
|
338
|
+
auto_approve = terraform_config.get('default_auto_approve', True)
|
339
|
+
|
340
|
+
if auto_approve:
|
341
|
+
cmd_parts.append('-auto-approve')
|
342
|
+
|
343
|
+
# Add plan file if specified, otherwise use var file and vars
|
344
|
+
if tf_input.plan_file:
|
345
|
+
cmd_parts.append(tf_input.plan_file)
|
346
|
+
else:
|
347
|
+
# Add var file if specified
|
348
|
+
if tf_input.var_file:
|
349
|
+
cmd_parts.extend(['-var-file', tf_input.var_file])
|
350
|
+
|
351
|
+
# Add variables if provided
|
352
|
+
if tf_input.vars:
|
353
|
+
for key, value in tf_input.vars.items():
|
354
|
+
cmd_parts.extend(['-var', f'{key}={value}'])
|
355
|
+
|
356
|
+
# Join command parts
|
357
|
+
command = ' '.join(cmd_parts)
|
358
|
+
|
359
|
+
self.logger.debug(f"Built terraform apply command: {command}")
|
360
|
+
return command
|
361
|
+
|
362
|
+
def terraform_init(self, tf_input: TerraformInitInput) -> TerraformInitOutput:
|
363
|
+
"""
|
364
|
+
Execute terraform init operation following our established pattern.
|
365
|
+
|
366
|
+
Args:
|
367
|
+
tf_input: Terraform init input parameters
|
368
|
+
|
369
|
+
Returns:
|
370
|
+
TerraformInitOutput: Result of the init operation
|
371
|
+
"""
|
372
|
+
start_time = time.time()
|
373
|
+
terraform_config = self.config.get('terraform_executor', {})
|
374
|
+
|
375
|
+
try:
|
376
|
+
# Log operation start
|
377
|
+
start_msg = self.config.get('success_messages', {}).get(
|
378
|
+
'init_started',
|
379
|
+
"Starting terraform init in '{repo_path}'"
|
380
|
+
).format(repo_path=tf_input.repo_path)
|
381
|
+
|
382
|
+
self.logger.info(start_msg)
|
383
|
+
|
384
|
+
# Store operation start in memory
|
385
|
+
if terraform_config.get('store_operations_in_memory', True):
|
386
|
+
self.memory.add_to_conversation(
|
387
|
+
"system",
|
388
|
+
start_msg,
|
389
|
+
{
|
390
|
+
"tool": "terraform_executor",
|
391
|
+
"operation": "init",
|
392
|
+
"repo_path": tf_input.repo_path
|
393
|
+
}
|
394
|
+
)
|
395
|
+
|
396
|
+
# Build terraform command
|
397
|
+
command = self._build_terraform_init_command(tf_input)
|
398
|
+
|
399
|
+
# Setup Terraform credentials
|
400
|
+
additional_env = self._setup_terraform_credentials()
|
401
|
+
|
402
|
+
# Execute command using shell executor
|
403
|
+
shell_input = ShellExecInput(
|
404
|
+
command=command,
|
405
|
+
cwd=tf_input.repo_path,
|
406
|
+
timeout=self.config.get('shell_executor', {}).get('default_timeout', 300),
|
407
|
+
env_vars=additional_env
|
408
|
+
)
|
409
|
+
|
410
|
+
shell_result = self.shell_executor.shell_exec(shell_input)
|
411
|
+
duration = time.time() - start_time
|
412
|
+
|
413
|
+
# Log successful execution
|
414
|
+
success_msg = self.config.get('success_messages', {}).get(
|
415
|
+
'init_completed',
|
416
|
+
"Successfully completed terraform init in '{repo_path}' in {duration:.2f}s"
|
417
|
+
).format(repo_path=tf_input.repo_path, duration=duration)
|
418
|
+
|
419
|
+
self.logger.info(success_msg)
|
420
|
+
|
421
|
+
# Store success in memory
|
422
|
+
if terraform_config.get('store_operations_in_memory', True):
|
423
|
+
self.memory.add_to_conversation(
|
424
|
+
"system",
|
425
|
+
success_msg,
|
426
|
+
{
|
427
|
+
"tool": "terraform_executor",
|
428
|
+
"operation": "init",
|
429
|
+
"repo_path": tf_input.repo_path,
|
430
|
+
"duration": duration,
|
431
|
+
"command": command
|
432
|
+
}
|
433
|
+
)
|
434
|
+
|
435
|
+
return TerraformInitOutput(
|
436
|
+
status=self.config.get('status_codes', {}).get('success', 'SUCCESS'),
|
437
|
+
duration=duration,
|
438
|
+
command_executed=command,
|
439
|
+
repo_path=tf_input.repo_path,
|
440
|
+
output=shell_result.output
|
441
|
+
)
|
442
|
+
|
443
|
+
except Exception as e:
|
444
|
+
duration = time.time() - start_time
|
445
|
+
error_msg = self.config.get('error_messages', {}).get(
|
446
|
+
'init_failed',
|
447
|
+
"Terraform init failed"
|
448
|
+
)
|
449
|
+
|
450
|
+
self.logger.error(f"{error_msg}: {str(e)}")
|
451
|
+
|
452
|
+
# Store error in memory
|
453
|
+
if terraform_config.get('store_operations_in_memory', True):
|
454
|
+
self.memory.add_to_conversation(
|
455
|
+
"system",
|
456
|
+
f"Terraform init failed: {str(e)}",
|
457
|
+
{
|
458
|
+
"tool": "terraform_executor",
|
459
|
+
"operation": "init",
|
460
|
+
"repo_path": tf_input.repo_path,
|
461
|
+
"error": True,
|
462
|
+
"duration": duration
|
463
|
+
}
|
464
|
+
)
|
465
|
+
|
466
|
+
return TerraformInitOutput(
|
467
|
+
status=self.config.get('status_codes', {}).get('error', 'ERROR'),
|
468
|
+
error_message=f"{error_msg}: {str(e)}",
|
469
|
+
duration=duration,
|
470
|
+
command_executed=self._build_terraform_init_command(tf_input),
|
471
|
+
repo_path=tf_input.repo_path
|
472
|
+
)
|
473
|
+
|
474
|
+
def terraform_plan(self, tf_input: TerraformPlanInput) -> TerraformPlanOutput:
|
475
|
+
"""
|
476
|
+
Execute terraform plan operation following our established pattern.
|
477
|
+
|
478
|
+
Args:
|
479
|
+
tf_input: Terraform plan input parameters
|
480
|
+
|
481
|
+
Returns:
|
482
|
+
TerraformPlanOutput: Result of the plan operation
|
483
|
+
"""
|
484
|
+
start_time = time.time()
|
485
|
+
terraform_config = self.config.get('terraform_executor', {})
|
486
|
+
|
487
|
+
try:
|
488
|
+
# Log operation start
|
489
|
+
start_msg = self.config.get('success_messages', {}).get(
|
490
|
+
'plan_started',
|
491
|
+
"Starting terraform plan in '{repo_path}'"
|
492
|
+
).format(repo_path=tf_input.repo_path)
|
493
|
+
|
494
|
+
self.logger.info(start_msg)
|
495
|
+
|
496
|
+
# Store operation start in memory
|
497
|
+
if terraform_config.get('store_operations_in_memory', True):
|
498
|
+
self.memory.add_to_conversation(
|
499
|
+
"system",
|
500
|
+
start_msg,
|
501
|
+
{
|
502
|
+
"tool": "terraform_executor",
|
503
|
+
"operation": "plan",
|
504
|
+
"repo_path": tf_input.repo_path
|
505
|
+
}
|
506
|
+
)
|
507
|
+
|
508
|
+
# Build terraform command
|
509
|
+
command = self._build_terraform_plan_command(tf_input)
|
510
|
+
|
511
|
+
# Setup Terraform credentials
|
512
|
+
additional_env = self._setup_terraform_credentials()
|
513
|
+
|
514
|
+
# Execute command using shell executor
|
515
|
+
shell_input = ShellExecInput(
|
516
|
+
command=command,
|
517
|
+
cwd=tf_input.repo_path,
|
518
|
+
timeout=self.config.get('shell_executor', {}).get('default_timeout', 300),
|
519
|
+
env_vars=additional_env
|
520
|
+
)
|
521
|
+
|
522
|
+
shell_result = self.shell_executor.shell_exec(shell_input)
|
523
|
+
duration = time.time() - start_time
|
524
|
+
|
525
|
+
# Determine plan file path
|
526
|
+
out_file = tf_input.out_file or terraform_config.get('default_plan_file', 'plan.tfplan')
|
527
|
+
plan_file_path = os.path.join(tf_input.repo_path, out_file)
|
528
|
+
|
529
|
+
# Log successful execution
|
530
|
+
success_msg = self.config.get('success_messages', {}).get(
|
531
|
+
'plan_completed',
|
532
|
+
"Successfully completed terraform plan in '{repo_path}' in {duration:.2f}s"
|
533
|
+
).format(repo_path=tf_input.repo_path, duration=duration)
|
534
|
+
|
535
|
+
self.logger.info(success_msg)
|
536
|
+
|
537
|
+
# Store success in memory
|
538
|
+
if terraform_config.get('store_operations_in_memory', True):
|
539
|
+
self.memory.add_to_conversation(
|
540
|
+
"system",
|
541
|
+
success_msg,
|
542
|
+
{
|
543
|
+
"tool": "terraform_executor",
|
544
|
+
"operation": "plan",
|
545
|
+
"repo_path": tf_input.repo_path,
|
546
|
+
"duration": duration,
|
547
|
+
"command": command,
|
548
|
+
"plan_file": plan_file_path
|
549
|
+
}
|
550
|
+
)
|
551
|
+
|
552
|
+
return TerraformPlanOutput(
|
553
|
+
status=self.config.get('status_codes', {}).get('success', 'SUCCESS'),
|
554
|
+
plan_file=plan_file_path,
|
555
|
+
duration=duration,
|
556
|
+
command_executed=command,
|
557
|
+
repo_path=tf_input.repo_path,
|
558
|
+
output=shell_result.output
|
559
|
+
)
|
560
|
+
|
561
|
+
except Exception as e:
|
562
|
+
duration = time.time() - start_time
|
563
|
+
error_msg = self.config.get('error_messages', {}).get(
|
564
|
+
'plan_failed',
|
565
|
+
"Terraform plan failed"
|
566
|
+
)
|
567
|
+
|
568
|
+
self.logger.error(f"{error_msg}: {str(e)}")
|
569
|
+
|
570
|
+
# Store error in memory
|
571
|
+
if terraform_config.get('store_operations_in_memory', True):
|
572
|
+
self.memory.add_to_conversation(
|
573
|
+
"system",
|
574
|
+
f"Terraform plan failed: {str(e)}",
|
575
|
+
{
|
576
|
+
"tool": "terraform_executor",
|
577
|
+
"operation": "plan",
|
578
|
+
"repo_path": tf_input.repo_path,
|
579
|
+
"error": True,
|
580
|
+
"duration": duration
|
581
|
+
}
|
582
|
+
)
|
583
|
+
|
584
|
+
return TerraformPlanOutput(
|
585
|
+
status=self.config.get('status_codes', {}).get('error', 'ERROR'),
|
586
|
+
error_message=f"{error_msg}: {str(e)}",
|
587
|
+
duration=duration,
|
588
|
+
command_executed=self._build_terraform_plan_command(tf_input),
|
589
|
+
repo_path=tf_input.repo_path
|
590
|
+
)
|
591
|
+
|
592
|
+
def terraform_apply(self, tf_input: TerraformApplyInput) -> TerraformApplyOutput:
|
593
|
+
"""
|
594
|
+
Execute terraform apply operation following our established pattern.
|
595
|
+
|
596
|
+
Args:
|
597
|
+
tf_input: Terraform apply input parameters
|
598
|
+
|
599
|
+
Returns:
|
600
|
+
TerraformApplyOutput: Result of the apply operation
|
601
|
+
"""
|
602
|
+
start_time = time.time()
|
603
|
+
terraform_config = self.config.get('terraform_executor', {})
|
604
|
+
|
605
|
+
try:
|
606
|
+
# Log operation start
|
607
|
+
start_msg = self.config.get('success_messages', {}).get(
|
608
|
+
'apply_started',
|
609
|
+
"Starting terraform apply in '{repo_path}'"
|
610
|
+
).format(repo_path=tf_input.repo_path)
|
611
|
+
|
612
|
+
self.logger.info(start_msg)
|
613
|
+
|
614
|
+
# Store operation start in memory
|
615
|
+
if terraform_config.get('store_operations_in_memory', True):
|
616
|
+
self.memory.add_to_conversation(
|
617
|
+
"system",
|
618
|
+
start_msg,
|
619
|
+
{
|
620
|
+
"tool": "terraform_executor",
|
621
|
+
"operation": "apply",
|
622
|
+
"repo_path": tf_input.repo_path
|
623
|
+
}
|
624
|
+
)
|
625
|
+
|
626
|
+
# Build terraform command
|
627
|
+
command = self._build_terraform_apply_command(tf_input)
|
628
|
+
|
629
|
+
# Setup Terraform credentials
|
630
|
+
additional_env = self._setup_terraform_credentials()
|
631
|
+
|
632
|
+
# Execute command using shell executor
|
633
|
+
shell_input = ShellExecInput(
|
634
|
+
command=command,
|
635
|
+
cwd=tf_input.repo_path,
|
636
|
+
timeout=self.config.get('shell_executor', {}).get('default_timeout', 300),
|
637
|
+
env_vars=additional_env
|
638
|
+
)
|
639
|
+
|
640
|
+
shell_result = self.shell_executor.shell_exec(shell_input)
|
641
|
+
duration = time.time() - start_time
|
642
|
+
|
643
|
+
# Log successful execution
|
644
|
+
success_msg = self.config.get('success_messages', {}).get(
|
645
|
+
'apply_completed',
|
646
|
+
"Successfully completed terraform apply in '{repo_path}' in {duration:.2f}s"
|
647
|
+
).format(repo_path=tf_input.repo_path, duration=duration)
|
648
|
+
|
649
|
+
self.logger.info(success_msg)
|
650
|
+
|
651
|
+
# Store success in memory
|
652
|
+
if terraform_config.get('store_operations_in_memory', True):
|
653
|
+
self.memory.add_to_conversation(
|
654
|
+
"system",
|
655
|
+
success_msg,
|
656
|
+
{
|
657
|
+
"tool": "terraform_executor",
|
658
|
+
"operation": "apply",
|
659
|
+
"repo_path": tf_input.repo_path,
|
660
|
+
"duration": duration,
|
661
|
+
"command": command
|
662
|
+
}
|
663
|
+
)
|
664
|
+
|
665
|
+
return TerraformApplyOutput(
|
666
|
+
status=self.config.get('status_codes', {}).get('success', 'SUCCESS'),
|
667
|
+
duration=duration,
|
668
|
+
command_executed=command,
|
669
|
+
repo_path=tf_input.repo_path,
|
670
|
+
output=shell_result.output
|
671
|
+
)
|
672
|
+
|
673
|
+
except Exception as e:
|
674
|
+
duration = time.time() - start_time
|
675
|
+
error_msg = self.config.get('error_messages', {}).get(
|
676
|
+
'apply_failed',
|
677
|
+
"Terraform apply failed"
|
678
|
+
)
|
679
|
+
|
680
|
+
self.logger.error(f"{error_msg}: {str(e)}")
|
681
|
+
|
682
|
+
# Store error in memory
|
683
|
+
if terraform_config.get('store_operations_in_memory', True):
|
684
|
+
self.memory.add_to_conversation(
|
685
|
+
"system",
|
686
|
+
f"Terraform apply failed: {str(e)}",
|
687
|
+
{
|
688
|
+
"tool": "terraform_executor",
|
689
|
+
"operation": "apply",
|
690
|
+
"repo_path": tf_input.repo_path,
|
691
|
+
"error": True,
|
692
|
+
"duration": duration
|
693
|
+
}
|
694
|
+
)
|
695
|
+
|
696
|
+
return TerraformApplyOutput(
|
697
|
+
status=self.config.get('status_codes', {}).get('error', 'ERROR'),
|
698
|
+
error_message=f"{error_msg}: {str(e)}",
|
699
|
+
duration=duration,
|
700
|
+
command_executed=self._build_terraform_apply_command(tf_input),
|
701
|
+
repo_path=tf_input.repo_path
|
702
|
+
)
|
703
|
+
|
704
|
+
|
705
|
+
# Global executor instance following our pattern
|
706
|
+
_terraform_executor = None
|
707
|
+
|
708
|
+
def get_terraform_executor(config_path: str = None, memory_type: str = "persistent") -> TerraformExecutor:
|
709
|
+
"""Get or create the global terraform executor instance."""
|
710
|
+
global _terraform_executor
|
711
|
+
if _terraform_executor is None:
|
712
|
+
_terraform_executor = TerraformExecutor(config_path=config_path, memory_type=memory_type)
|
713
|
+
return _terraform_executor
|
714
|
+
|
715
|
+
|
716
|
+
# --- LangChain Tool Integration ---
|
717
|
+
@tool(args_schema=TerraformInitInput)
|
718
|
+
def terraform_init(repo_path: str, upgrade: bool = False, backend_config: Dict[str, str] = None) -> str:
|
719
|
+
"""
|
720
|
+
Initialize a Terraform repository following our established pattern.
|
721
|
+
|
722
|
+
Args:
|
723
|
+
repo_path: Path to Terraform repository/directory
|
724
|
+
upgrade: Pass -upgrade flag to terraform init
|
725
|
+
backend_config: Backend configuration parameters
|
726
|
+
|
727
|
+
Returns:
|
728
|
+
String description of the operation result
|
729
|
+
"""
|
730
|
+
try:
|
731
|
+
executor = get_terraform_executor()
|
732
|
+
tf_input = TerraformInitInput(
|
733
|
+
repo_path=repo_path,
|
734
|
+
upgrade=upgrade,
|
735
|
+
backend_config=backend_config
|
736
|
+
)
|
737
|
+
result = executor.terraform_init(tf_input)
|
738
|
+
|
739
|
+
if result.status == "SUCCESS":
|
740
|
+
return f"✅ Terraform init completed successfully in {result.duration:.2f}s"
|
741
|
+
else:
|
742
|
+
return f"❌ Terraform init failed: {result.error_message}"
|
743
|
+
|
744
|
+
except Exception as e:
|
745
|
+
return f"❌ Terraform init error: {str(e)}"
|
746
|
+
|
747
|
+
|
748
|
+
@tool(args_schema=TerraformPlanInput)
|
749
|
+
def terraform_plan(repo_path: str, out_file: str = None, var_file: str = None,
|
750
|
+
vars: Dict[str, str] = None, destroy: bool = False) -> str:
|
751
|
+
"""
|
752
|
+
Create a Terraform execution plan following our established pattern.
|
753
|
+
|
754
|
+
Args:
|
755
|
+
repo_path: Path to Terraform repository/directory
|
756
|
+
out_file: Output file for plan (default: plan.tfplan)
|
757
|
+
var_file: Variables file to use
|
758
|
+
vars: Variables to pass to terraform plan
|
759
|
+
destroy: Create a destroy plan
|
760
|
+
|
761
|
+
Returns:
|
762
|
+
String description of the operation result
|
763
|
+
"""
|
764
|
+
try:
|
765
|
+
executor = get_terraform_executor()
|
766
|
+
tf_input = TerraformPlanInput(
|
767
|
+
repo_path=repo_path,
|
768
|
+
out_file=out_file,
|
769
|
+
var_file=var_file,
|
770
|
+
vars=vars,
|
771
|
+
destroy=destroy
|
772
|
+
)
|
773
|
+
result = executor.terraform_plan(tf_input)
|
774
|
+
|
775
|
+
if result.status == "SUCCESS":
|
776
|
+
return f"✅ Terraform plan completed successfully in {result.duration:.2f}s. Plan saved to: {result.plan_file}"
|
777
|
+
else:
|
778
|
+
return f"❌ Terraform plan failed: {result.error_message}"
|
779
|
+
|
780
|
+
except Exception as e:
|
781
|
+
return f"❌ Terraform plan error: {str(e)}"
|
782
|
+
|
783
|
+
|
784
|
+
@tool(args_schema=TerraformApplyInput)
|
785
|
+
def terraform_apply(repo_path: str, plan_file: str = None, auto_approve: bool = True,
|
786
|
+
var_file: str = None, vars: Dict[str, str] = None) -> str:
|
787
|
+
"""
|
788
|
+
Apply a Terraform execution plan following our established pattern.
|
789
|
+
|
790
|
+
Args:
|
791
|
+
repo_path: Path to Terraform repository/directory
|
792
|
+
plan_file: Plan file to apply (if not provided, applies current state)
|
793
|
+
auto_approve: Pass -auto-approve flag (default: True for automation)
|
794
|
+
var_file: Variables file to use
|
795
|
+
vars: Variables to pass to terraform apply
|
796
|
+
|
797
|
+
Returns:
|
798
|
+
String description of the operation result
|
799
|
+
"""
|
800
|
+
try:
|
801
|
+
executor = get_terraform_executor()
|
802
|
+
tf_input = TerraformApplyInput(
|
803
|
+
repo_path=repo_path,
|
804
|
+
plan_file=plan_file,
|
805
|
+
auto_approve=auto_approve,
|
806
|
+
var_file=var_file,
|
807
|
+
vars=vars
|
808
|
+
)
|
809
|
+
result = executor.terraform_apply(tf_input)
|
810
|
+
|
811
|
+
if result.status == "SUCCESS":
|
812
|
+
return f"✅ Terraform apply completed successfully in {result.duration:.2f}s"
|
813
|
+
else:
|
814
|
+
return f"❌ Terraform apply failed: {result.error_message}"
|
815
|
+
|
816
|
+
except Exception as e:
|
817
|
+
return f"❌ Terraform apply error: {str(e)}"
|
818
|
+
|
819
|
+
|
820
|
+
# Convenience functions following our pattern
|
821
|
+
def terraform_init_simple(repo_path: str, upgrade: bool = False) -> TerraformInitOutput:
|
822
|
+
"""
|
823
|
+
Simple terraform init function matching the original interface.
|
824
|
+
|
825
|
+
This maintains backward compatibility while using our comprehensive framework.
|
826
|
+
"""
|
827
|
+
executor = get_terraform_executor()
|
828
|
+
tf_input = TerraformInitInput(repo_path=repo_path, upgrade=upgrade)
|
829
|
+
return executor.terraform_init(tf_input)
|
830
|
+
|
831
|
+
|
832
|
+
def terraform_plan_simple(repo_path: str, out_file: str = None) -> TerraformPlanOutput:
|
833
|
+
"""
|
834
|
+
Simple terraform plan function matching the original interface.
|
835
|
+
|
836
|
+
This maintains backward compatibility while using our comprehensive framework.
|
837
|
+
"""
|
838
|
+
executor = get_terraform_executor()
|
839
|
+
tf_input = TerraformPlanInput(repo_path=repo_path, out_file=out_file)
|
840
|
+
return executor.terraform_plan(tf_input)
|
841
|
+
|
842
|
+
|
843
|
+
def terraform_apply_simple(repo_path: str, plan_file: str = None, auto_approve: bool = True) -> TerraformApplyOutput:
|
844
|
+
"""
|
845
|
+
Simple terraform apply function matching the original interface.
|
846
|
+
|
847
|
+
This maintains backward compatibility while using our comprehensive framework.
|
848
|
+
"""
|
849
|
+
executor = get_terraform_executor()
|
850
|
+
tf_input = TerraformApplyInput(repo_path=repo_path, plan_file=plan_file, auto_approve=auto_approve)
|
851
|
+
return executor.terraform_apply(tf_input)
|