diagram-to-iac 0.6.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.
Files changed (77) hide show
  1. diagram_to_iac/__init__.py +10 -0
  2. diagram_to_iac/actions/__init__.py +7 -0
  3. diagram_to_iac/actions/git_entry.py +174 -0
  4. diagram_to_iac/actions/supervisor_entry.py +116 -0
  5. diagram_to_iac/actions/terraform_agent_entry.py +207 -0
  6. diagram_to_iac/agents/__init__.py +26 -0
  7. diagram_to_iac/agents/demonstrator_langgraph/__init__.py +10 -0
  8. diagram_to_iac/agents/demonstrator_langgraph/agent.py +826 -0
  9. diagram_to_iac/agents/git_langgraph/__init__.py +10 -0
  10. diagram_to_iac/agents/git_langgraph/agent.py +1018 -0
  11. diagram_to_iac/agents/git_langgraph/pr.py +146 -0
  12. diagram_to_iac/agents/hello_langgraph/__init__.py +9 -0
  13. diagram_to_iac/agents/hello_langgraph/agent.py +621 -0
  14. diagram_to_iac/agents/policy_agent/__init__.py +15 -0
  15. diagram_to_iac/agents/policy_agent/agent.py +507 -0
  16. diagram_to_iac/agents/policy_agent/integration_example.py +191 -0
  17. diagram_to_iac/agents/policy_agent/tools/__init__.py +14 -0
  18. diagram_to_iac/agents/policy_agent/tools/tfsec_tool.py +259 -0
  19. diagram_to_iac/agents/shell_langgraph/__init__.py +21 -0
  20. diagram_to_iac/agents/shell_langgraph/agent.py +122 -0
  21. diagram_to_iac/agents/shell_langgraph/detector.py +50 -0
  22. diagram_to_iac/agents/supervisor_langgraph/__init__.py +17 -0
  23. diagram_to_iac/agents/supervisor_langgraph/agent.py +1947 -0
  24. diagram_to_iac/agents/supervisor_langgraph/demonstrator.py +22 -0
  25. diagram_to_iac/agents/supervisor_langgraph/guards.py +23 -0
  26. diagram_to_iac/agents/supervisor_langgraph/pat_loop.py +49 -0
  27. diagram_to_iac/agents/supervisor_langgraph/router.py +9 -0
  28. diagram_to_iac/agents/terraform_langgraph/__init__.py +15 -0
  29. diagram_to_iac/agents/terraform_langgraph/agent.py +1216 -0
  30. diagram_to_iac/agents/terraform_langgraph/parser.py +76 -0
  31. diagram_to_iac/core/__init__.py +7 -0
  32. diagram_to_iac/core/agent_base.py +19 -0
  33. diagram_to_iac/core/enhanced_memory.py +302 -0
  34. diagram_to_iac/core/errors.py +4 -0
  35. diagram_to_iac/core/issue_tracker.py +49 -0
  36. diagram_to_iac/core/memory.py +132 -0
  37. diagram_to_iac/services/__init__.py +10 -0
  38. diagram_to_iac/services/observability.py +59 -0
  39. diagram_to_iac/services/step_summary.py +77 -0
  40. diagram_to_iac/tools/__init__.py +11 -0
  41. diagram_to_iac/tools/api_utils.py +108 -26
  42. diagram_to_iac/tools/git/__init__.py +45 -0
  43. diagram_to_iac/tools/git/git.py +956 -0
  44. diagram_to_iac/tools/hello/__init__.py +30 -0
  45. diagram_to_iac/tools/hello/cal_utils.py +31 -0
  46. diagram_to_iac/tools/hello/text_utils.py +97 -0
  47. diagram_to_iac/tools/llm_utils/__init__.py +20 -0
  48. diagram_to_iac/tools/llm_utils/anthropic_driver.py +87 -0
  49. diagram_to_iac/tools/llm_utils/base_driver.py +90 -0
  50. diagram_to_iac/tools/llm_utils/gemini_driver.py +89 -0
  51. diagram_to_iac/tools/llm_utils/openai_driver.py +93 -0
  52. diagram_to_iac/tools/llm_utils/router.py +303 -0
  53. diagram_to_iac/tools/sec_utils.py +4 -2
  54. diagram_to_iac/tools/shell/__init__.py +17 -0
  55. diagram_to_iac/tools/shell/shell.py +415 -0
  56. diagram_to_iac/tools/text_utils.py +277 -0
  57. diagram_to_iac/tools/tf/terraform.py +851 -0
  58. diagram_to_iac-0.8.0.dist-info/METADATA +99 -0
  59. diagram_to_iac-0.8.0.dist-info/RECORD +64 -0
  60. {diagram_to_iac-0.6.0.dist-info → diagram_to_iac-0.8.0.dist-info}/WHEEL +1 -1
  61. diagram_to_iac-0.8.0.dist-info/entry_points.txt +4 -0
  62. diagram_to_iac/agents/codegen_agent.py +0 -0
  63. diagram_to_iac/agents/consensus_agent.py +0 -0
  64. diagram_to_iac/agents/deployment_agent.py +0 -0
  65. diagram_to_iac/agents/github_agent.py +0 -0
  66. diagram_to_iac/agents/interpretation_agent.py +0 -0
  67. diagram_to_iac/agents/question_agent.py +0 -0
  68. diagram_to_iac/agents/supervisor.py +0 -0
  69. diagram_to_iac/agents/vision_agent.py +0 -0
  70. diagram_to_iac/core/config.py +0 -0
  71. diagram_to_iac/tools/cv_utils.py +0 -0
  72. diagram_to_iac/tools/gh_utils.py +0 -0
  73. diagram_to_iac/tools/tf_utils.py +0 -0
  74. diagram_to_iac-0.6.0.dist-info/METADATA +0 -16
  75. diagram_to_iac-0.6.0.dist-info/RECORD +0 -32
  76. diagram_to_iac-0.6.0.dist-info/entry_points.txt +0 -2
  77. {diagram_to_iac-0.6.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)