tfmcp 0.1.9

Terraform Model Context Protocol Tool - A CLI tool to manage Terraform through MCP
Documentation
//! Terraform import helper for importing existing resources.

use serde::{Deserialize, Serialize};
use std::path::Path;
use std::process::Command;

/// Import preview information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportPreview {
    pub resource_address: String,
    pub resource_id: String,
    pub resource_type: String,
    pub suggested_config: String,
    pub warnings: Vec<String>,
}

/// Import result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportResult {
    pub success: bool,
    pub resource_address: String,
    pub resource_id: String,
    pub message: String,
    pub output: Option<String>,
}

/// Generate a preview of what would be imported
pub fn preview_import(
    resource_type: &str,
    resource_id: &str,
    name: &str,
) -> anyhow::Result<ImportPreview> {
    let resource_address = format!("{}.{}", resource_type, name);

    // Generate suggested configuration based on resource type
    let suggested_config = generate_suggested_config(resource_type, name);
    let warnings = generate_import_warnings(resource_type);

    Ok(ImportPreview {
        resource_address,
        resource_id: resource_id.to_string(),
        resource_type: resource_type.to_string(),
        suggested_config,
        warnings,
    })
}

/// Execute the import
pub fn execute_import(
    terraform_path: &Path,
    project_dir: &Path,
    resource_type: &str,
    resource_id: &str,
    name: &str,
) -> anyhow::Result<ImportResult> {
    let resource_address = format!("{}.{}", resource_type, name);

    // Run terraform import
    let output = Command::new(terraform_path)
        .arg("import")
        .arg(&resource_address)
        .arg(resource_id)
        .current_dir(project_dir)
        .output()?;

    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    if output.status.success() {
        Ok(ImportResult {
            success: true,
            resource_address,
            resource_id: resource_id.to_string(),
            message: "Resource imported successfully".to_string(),
            output: Some(stdout.to_string()),
        })
    } else {
        // Parse common error messages
        let message = if stderr.contains("Cannot import non-existent remote object") {
            format!(
                "Resource with ID '{}' does not exist in the cloud",
                resource_id
            )
        } else if stderr.contains("Resource already managed by Terraform") {
            format!(
                "Resource '{}' is already managed by Terraform",
                resource_address
            )
        } else if stderr.contains("configuration for") && stderr.contains("is not present") {
            format!(
                "No configuration found for '{}'. Add a resource block before importing.",
                resource_address
            )
        } else {
            format!("Import failed: {}", stderr)
        };

        Ok(ImportResult {
            success: false,
            resource_address,
            resource_id: resource_id.to_string(),
            message,
            output: Some(stderr.to_string()),
        })
    }
}

/// Generate suggested configuration for a resource type
fn generate_suggested_config(resource_type: &str, name: &str) -> String {
    // Provider-specific templates
    match resource_type {
        // AWS resources
        "aws_instance" => format!(
            r#"resource "{}" "{}" {{
  # Required attributes after import:
  ami           = "ami-xxxxxxxx"  # Update with actual AMI ID
  instance_type = "t3.micro"      # Update with actual instance type

  # Optional: Add tags, VPC settings, etc.
  tags = {{
    Name = "{}"
  }}
}}"#,
            resource_type, name, name
        ),
        "aws_s3_bucket" => format!(
            r#"resource "{}" "{}" {{
  # The bucket name will be imported from the resource ID
  # Add any additional configuration as needed

  tags = {{
    Name = "{}"
  }}
}}"#,
            resource_type, name, name
        ),
        "aws_security_group" => format!(
            r#"resource "{}" "{}" {{
  name        = "{}"
  description = "Imported security group"

  # Ingress and egress rules will need to be added manually
  # after running 'terraform plan' to see the current state
}}"#,
            resource_type, name, name
        ),
        "aws_vpc" => format!(
            r#"resource "{}" "{}" {{
  cidr_block = "10.0.0.0/16"  # Update with actual CIDR

  tags = {{
    Name = "{}"
  }}
}}"#,
            resource_type, name, name
        ),
        "aws_subnet" => format!(
            r#"resource "{}" "{}" {{
  vpc_id     = aws_vpc.main.id  # Update with actual VPC reference
  cidr_block = "10.0.1.0/24"    # Update with actual CIDR

  tags = {{
    Name = "{}"
  }}
}}"#,
            resource_type, name, name
        ),
        "aws_db_instance" => format!(
            r#"resource "{}" "{}" {{
  identifier        = "{}"
  instance_class    = "db.t3.micro"  # Update with actual instance class
  engine            = "mysql"         # Update with actual engine
  allocated_storage = 20              # Update with actual storage

  # Password must be specified or use manage_master_user_password
  # username = "admin"
  # password = "..."

  skip_final_snapshot = true  # Set to false in production
}}"#,
            resource_type, name, name
        ),

        // Google Cloud resources
        "google_compute_instance" => format!(
            r#"resource "{}" "{}" {{
  name         = "{}"
  machine_type = "e2-medium"  # Update with actual machine type
  zone         = "us-central1-a"

  boot_disk {{
    initialize_params {{
      image = "debian-cloud/debian-11"
    }}
  }}

  network_interface {{
    network = "default"
  }}
}}"#,
            resource_type, name, name
        ),
        "google_storage_bucket" => format!(
            r#"resource "{}" "{}" {{
  name     = "{}"
  location = "US"  # Update with actual location

  force_destroy = false
}}"#,
            resource_type, name, name
        ),

        // Azure resources
        "azurerm_virtual_machine"
        | "azurerm_linux_virtual_machine"
        | "azurerm_windows_virtual_machine" => format!(
            r#"resource "{}" "{}" {{
  name                = "{}"
  resource_group_name = "my-resource-group"  # Update
  location            = "East US"             # Update
  size                = "Standard_B1s"        # Update

  # Additional required attributes depend on the VM type
}}"#,
            resource_type, name, name
        ),
        "azurerm_storage_account" => format!(
            r#"resource "{}" "{}" {{
  name                     = "{}"
  resource_group_name      = "my-resource-group"  # Update
  location                 = "East US"             # Update
  account_tier             = "Standard"
  account_replication_type = "LRS"
}}"#,
            resource_type, name, name
        ),

        // Generic template for unknown resource types
        _ => format!(
            r#"resource "{}" "{}" {{
  # Add required attributes for this resource type
  # Run 'terraform plan' after import to see the current state
  # and identify any required attributes that are missing
}}"#,
            resource_type, name
        ),
    }
}

/// Generate warnings for import based on resource type
fn generate_import_warnings(resource_type: &str) -> Vec<String> {
    let mut warnings = Vec::new();

    // Common warning
    warnings.push(
        "After import, run 'terraform plan' to see if your configuration matches the imported state"
            .to_string(),
    );

    // Resource-specific warnings
    match resource_type {
        "aws_db_instance" | "google_sql_database_instance" | "azurerm_sql_database" => {
            warnings.push(
                "Database passwords are not imported - you may need to update your configuration"
                    .to_string(),
            );
        }
        "aws_instance" | "google_compute_instance" | "azurerm_virtual_machine" => {
            warnings.push("Instance user data / startup scripts are not imported".to_string());
        }
        "aws_security_group" | "google_compute_firewall" | "azurerm_network_security_group" => {
            warnings.push("Review all ingress/egress rules after import for security".to_string());
        }
        "aws_iam_role" | "aws_iam_policy" | "google_project_iam_binding" => {
            warnings.push(
                "IAM resources are security-sensitive - review all permissions carefully"
                    .to_string(),
            );
        }
        "aws_s3_bucket" | "google_storage_bucket" | "azurerm_storage_account" => {
            warnings.push("Bucket policies and ACLs may need separate import".to_string());
        }
        _ => {}
    }

    // Check for data resources
    if resource_type.starts_with("data.") {
        warnings.push("Data sources cannot be imported - they are read-only".to_string());
    }

    warnings
}

/// Get import ID format hint for common resource types
#[allow(dead_code)]
pub fn get_import_id_hint(resource_type: &str) -> String {
    match resource_type {
        // AWS
        "aws_instance" => "Instance ID (e.g., i-1234567890abcdef0)".to_string(),
        "aws_s3_bucket" => "Bucket name (e.g., my-bucket-name)".to_string(),
        "aws_security_group" => "Security group ID (e.g., sg-1234567890abcdef0)".to_string(),
        "aws_vpc" => "VPC ID (e.g., vpc-1234567890abcdef0)".to_string(),
        "aws_subnet" => "Subnet ID (e.g., subnet-1234567890abcdef0)".to_string(),
        "aws_db_instance" => "DB instance identifier (e.g., my-database)".to_string(),
        "aws_iam_role" => "Role name (e.g., my-role)".to_string(),
        "aws_iam_policy" => "Policy ARN (e.g., arn:aws:iam::123456789012:policy/my-policy)".to_string(),
        "aws_lambda_function" => "Function name (e.g., my-function)".to_string(),

        // Google Cloud
        "google_compute_instance" => "projects/{project}/zones/{zone}/instances/{name}".to_string(),
        "google_storage_bucket" => "Bucket name (e.g., my-bucket)".to_string(),
        "google_compute_network" => "projects/{project}/global/networks/{name}".to_string(),

        // Azure
        "azurerm_resource_group" => "/subscriptions/{subscription_id}/resourceGroups/{name}".to_string(),
        "azurerm_virtual_machine" => "/subscriptions/{subscription_id}/resourceGroups/{rg}/providers/Microsoft.Compute/virtualMachines/{name}".to_string(),
        "azurerm_storage_account" => "/subscriptions/{subscription_id}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/{name}".to_string(),

        _ => "Resource-specific ID format - check Terraform provider documentation".to_string(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_preview_import() {
        let preview = preview_import("aws_instance", "i-12345", "web").unwrap();
        assert_eq!(preview.resource_address, "aws_instance.web");
        assert_eq!(preview.resource_id, "i-12345");
        assert!(!preview.suggested_config.is_empty());
        assert!(!preview.warnings.is_empty());
    }

    #[test]
    fn test_import_id_hint() {
        let hint = get_import_id_hint("aws_instance");
        assert!(hint.contains("Instance ID"));

        let hint = get_import_id_hint("aws_s3_bucket");
        assert!(hint.contains("Bucket name"));
    }

    #[test]
    fn test_generate_warnings() {
        let warnings = generate_import_warnings("aws_db_instance");
        assert!(warnings.iter().any(|w| w.contains("password")));

        let warnings = generate_import_warnings("aws_security_group");
        assert!(warnings.iter().any(|w| w.contains("security")));
    }
}