mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 20:15:56 +00:00
feature/terraform language server support (#277)
Adds terraform support
This commit is contained in:
1 parent
a24dcea427
commit
1df9732390
13 files changed
+693
-2
No files matched your search
@@ -14,7 +14,6 @@ jobs:
|
||||
cpu:
|
||||
name: Tests on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
if: "!contains(github.event.head_commit.message, 'ci skip')"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -52,6 +51,40 @@ jobs:
|
||||
uses: DeLaGuardo/setup-clojure@13.4
|
||||
with:
|
||||
cli: latest
|
||||
- name: Install Terraform
|
||||
uses: hashicorp/setup-terraform@v3
|
||||
with:
|
||||
terraform_version: "1.5.0"
|
||||
terraform_wrapper: false
|
||||
- name: Install terraform-ls (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Installing terraform-ls manually for Windows"
|
||||
$ver = '0.32.7'
|
||||
$zip = "terraform-ls_${ver}_windows_amd64.zip"
|
||||
Invoke-WebRequest -Uri "https://releases.hashicorp.com/terraform-ls/$ver/$zip" -OutFile $zip
|
||||
|
||||
$dest = "$env:USERPROFILE\terraform-ls"
|
||||
New-Item -ItemType Directory -Force -Path $dest | Out-Null
|
||||
Expand-Archive $zip -DestinationPath $dest -Force
|
||||
|
||||
Write-Host "terraform-ls installed to: $dest"
|
||||
echo "$dest" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
- name: Install terraform-ls (Linux/macOS)
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
ver=0.32.7
|
||||
os=$(uname | tr '[:upper:]' '[:lower:]')
|
||||
echo "Installing terraform-ls ${ver} for ${os}"
|
||||
curl -sSL -o tfls.zip \
|
||||
"https://releases.hashicorp.com/terraform-ls/${ver}/terraform-ls_${ver}_${os}_amd64.zip"
|
||||
mkdir -p "$HOME/bin"
|
||||
unzip -q tfls.zip -d "$HOME/bin"
|
||||
chmod +x "$HOME/bin/terraform-ls"
|
||||
echo "$HOME/bin" >> "$GITHUB_PATH"
|
||||
echo "terraform-ls installed to $HOME/bin"
|
||||
- name: Install uv
|
||||
shell: bash
|
||||
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
|
||||
Vendored
+2
-1
@@ -11,4 +11,5 @@
|
||||
"sensai",
|
||||
"vibing"
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,6 +248,7 @@ markers = [
|
||||
"php: language server running for PHP",
|
||||
"csharp: language server running for C#",
|
||||
"elixir: language server running for Elixir",
|
||||
"terraform: language server running for Terraform",
|
||||
"snapshot: snapshot tests for symbolic editing operations",
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"_description": "The parameters sent by the client when initializing the language server with the \"initialize\" request. More details at https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize",
|
||||
"processId": "os.getpid()",
|
||||
"locale": "en",
|
||||
"rootPath": "$rootPath",
|
||||
"rootUri": "$rootUri",
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"synchronization": {
|
||||
"didSave": true,
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"completion": {
|
||||
"dynamicRegistration": true,
|
||||
"completionItem": {
|
||||
"snippetSupport": true
|
||||
}
|
||||
},
|
||||
"definition": {
|
||||
"dynamicRegistration": true
|
||||
},
|
||||
"documentSymbol": {
|
||||
"dynamicRegistration": true,
|
||||
"hierarchicalDocumentSymbolSupport": true,
|
||||
"symbolKind": {
|
||||
"valueSet": [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
|
||||
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": true,
|
||||
"didChangeConfiguration": {
|
||||
"dynamicRegistration": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": "$uri",
|
||||
"name": "$name"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"_description": "Used to download the runtime dependencies for running terraform-ls. Obtained from https://releases.hashicorp.com/terraform-ls/",
|
||||
"runtimeDependencies": [
|
||||
{
|
||||
"id": "TerraformLS",
|
||||
"description": "terraform-ls for macOS (ARM64)",
|
||||
"url": "https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_arm64.zip",
|
||||
"platformId": "osx-arm64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "terraform-ls"
|
||||
},
|
||||
{
|
||||
"id": "TerraformLS",
|
||||
"description": "terraform-ls for macOS (x64)",
|
||||
"url": "https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_darwin_amd64.zip",
|
||||
"platformId": "osx-x64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "terraform-ls"
|
||||
},
|
||||
{
|
||||
"id": "TerraformLS",
|
||||
"description": "terraform-ls for Linux (x64)",
|
||||
"url": "https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_linux_amd64.zip",
|
||||
"platformId": "linux-x64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "terraform-ls"
|
||||
},
|
||||
{
|
||||
"id": "TerraformLS",
|
||||
"description": "terraform-ls for Windows (x64)",
|
||||
"url": "https://releases.hashicorp.com/terraform-ls/0.36.5/terraform-ls_0.36.5_windows_amd64.zip",
|
||||
"platformId": "win-x64",
|
||||
"archiveType": "zip",
|
||||
"binaryName": "terraform-ls.exe"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_logger import LanguageServerLogger
|
||||
from solidlsp.ls_utils import FileUtils, PlatformUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
|
||||
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
|
||||
|
||||
|
||||
class TerraformLS(SolidLanguageServer):
|
||||
"""
|
||||
Provides Terraform specific instantiation of the LanguageServer class using terraform-ls.
|
||||
"""
|
||||
|
||||
@override
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
# For Terraform projects, we should ignore:
|
||||
# - .terraform: Terraform working directory with providers and modules
|
||||
# - terraform.tfstate.d: Terraform workspace state directories
|
||||
# - .git: Version control
|
||||
# - node_modules: If the project has JavaScript components
|
||||
return super().is_ignored_dirname(dirname) or dirname in [".terraform", "terraform.tfstate.d", "node_modules"]
|
||||
|
||||
@staticmethod
|
||||
def _get_terraform_version(logger=None):
|
||||
"""Get the installed Terraform version or None if not found."""
|
||||
if logger:
|
||||
logger.log("Starting terraform version detection...", logging.DEBUG)
|
||||
else:
|
||||
logging.debug("Starting terraform version detection...")
|
||||
|
||||
# 1. Try to find terraform using shutil.which (standard Python way)
|
||||
terraform_cmd = shutil.which("terraform")
|
||||
if terraform_cmd:
|
||||
if logger:
|
||||
logger.log(f"Found terraform via shutil.which: {terraform_cmd}", logging.DEBUG)
|
||||
else:
|
||||
logging.debug(f"Found terraform via shutil.which: {terraform_cmd}")
|
||||
else:
|
||||
if logger:
|
||||
logger.log("terraform not found via shutil.which", logging.DEBUG)
|
||||
else:
|
||||
logging.debug("terraform not found via shutil.which")
|
||||
|
||||
# 2. Fallback to TERRAFORM_CLI_PATH (set by hashicorp/setup-terraform action)
|
||||
if not terraform_cmd:
|
||||
terraform_cli_path = os.environ.get('TERRAFORM_CLI_PATH')
|
||||
if terraform_cli_path:
|
||||
if logger:
|
||||
logger.log(f"Trying TERRAFORM_CLI_PATH: {terraform_cli_path}", logging.DEBUG)
|
||||
else:
|
||||
logging.debug(f"Trying TERRAFORM_CLI_PATH: {terraform_cli_path}")
|
||||
terraform_exe = os.path.join(terraform_cli_path, "terraform.exe")
|
||||
if os.path.exists(terraform_exe):
|
||||
terraform_cmd = terraform_exe
|
||||
if logger:
|
||||
logger.log(f"Found terraform via TERRAFORM_CLI_PATH: {terraform_cmd}", logging.DEBUG)
|
||||
else:
|
||||
logging.debug(f"Found terraform via TERRAFORM_CLI_PATH: {terraform_cmd}")
|
||||
else:
|
||||
if logger:
|
||||
logger.log(f"terraform.exe not found at {terraform_exe}", logging.DEBUG)
|
||||
else:
|
||||
logging.debug(f"terraform.exe not found at {terraform_exe}")
|
||||
else:
|
||||
if logger:
|
||||
logger.log("TERRAFORM_CLI_PATH not set", logging.DEBUG)
|
||||
else:
|
||||
logging.debug("TERRAFORM_CLI_PATH not set")
|
||||
|
||||
# 3. Try to run the terraform command if found
|
||||
if terraform_cmd:
|
||||
try:
|
||||
if logger:
|
||||
logger.log(f"Attempting to run: {terraform_cmd} version (with 15s timeout)", logging.DEBUG)
|
||||
else:
|
||||
logging.debug(f"Attempting to run: {terraform_cmd} version (with 15s timeout)")
|
||||
result = subprocess.run(
|
||||
[terraform_cmd, "version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=15 # CRITICAL: 15 second timeout to prevent hangs
|
||||
)
|
||||
if result.returncode == 0:
|
||||
if logger:
|
||||
logger.log("terraform version command succeeded", logging.DEBUG)
|
||||
else:
|
||||
logging.debug("terraform version command succeeded")
|
||||
return result.stdout.strip()
|
||||
else:
|
||||
if logger:
|
||||
logger.log(f"terraform version command failed with return code {result.returncode}", logging.DEBUG)
|
||||
logger.log(f"stderr: {result.stderr}", logging.DEBUG)
|
||||
else:
|
||||
logging.debug(f"terraform version command failed with return code {result.returncode}")
|
||||
logging.debug(f"stderr: {result.stderr}")
|
||||
except subprocess.TimeoutExpired:
|
||||
if logger:
|
||||
logger.log("terraform version command timed out after 15 seconds", logging.ERROR)
|
||||
else:
|
||||
logging.error("terraform version command timed out after 15 seconds")
|
||||
except (FileNotFoundError, OSError) as e:
|
||||
if logger:
|
||||
logger.log(f"Failed to run terraform command: {e}", logging.DEBUG)
|
||||
else:
|
||||
logging.debug(f"Failed to run terraform command: {e}")
|
||||
else:
|
||||
if logger:
|
||||
logger.log("No terraform executable found", logging.DEBUG)
|
||||
else:
|
||||
logging.debug("No terraform executable found")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def setup_runtime_dependencies(self, logger: LanguageServerLogger, config: LanguageServerConfig) -> str:
|
||||
"""
|
||||
Setup runtime dependencies for terraform-ls.
|
||||
Downloads and installs terraform-ls if not already present.
|
||||
"""
|
||||
# First check if Terraform is available
|
||||
terraform_version = self._get_terraform_version(logger)
|
||||
if not terraform_version:
|
||||
raise RuntimeError(
|
||||
"Terraform executable not found or failed to execute. "
|
||||
"Please ensure Terraform is installed and accessible in your system's PATH.\n"
|
||||
"If it's installed, check for permission issues or corrupted installation.\n"
|
||||
"Download from https://www.terraform.io/downloads"
|
||||
)
|
||||
|
||||
platform_id = PlatformUtils.get_platform_id()
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"), encoding="utf-8") as f:
|
||||
d = json.load(f)
|
||||
del d["_description"]
|
||||
|
||||
runtime_dependencies = d["runtimeDependencies"]
|
||||
runtime_dependencies = [dependency for dependency in runtime_dependencies if dependency["platformId"] == platform_id.value]
|
||||
assert len(runtime_dependencies) == 1, f"Expected exactly one runtime dependency for platform {platform_id.value}, found {len(runtime_dependencies)}"
|
||||
dependency = runtime_dependencies[0]
|
||||
|
||||
terraform_ls_dir = os.path.join(os.path.dirname(__file__), "static", "TerraformLS")
|
||||
terraform_ls_executable_path = os.path.join(terraform_ls_dir, dependency["binaryName"])
|
||||
|
||||
if not os.path.exists(terraform_ls_dir):
|
||||
os.makedirs(terraform_ls_dir)
|
||||
|
||||
if not os.path.exists(terraform_ls_executable_path):
|
||||
logger.log(f"Downloading terraform-ls from {dependency['url']}", logging.INFO)
|
||||
FileUtils.download_and_extract_archive(logger, dependency["url"], terraform_ls_dir, dependency["archiveType"])
|
||||
|
||||
assert os.path.exists(terraform_ls_executable_path), f"terraform-ls executable not found at {terraform_ls_executable_path}"
|
||||
|
||||
# Make the executable file executable on Unix-like systems
|
||||
if platform_id.value != "win-x64":
|
||||
os.chmod(terraform_ls_executable_path, stat.S_IEXEC | stat.S_IREAD)
|
||||
|
||||
return terraform_ls_executable_path
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str):
|
||||
"""
|
||||
Creates a TerraformLS instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
|
||||
"""
|
||||
terraform_ls_executable_path = self.setup_runtime_dependencies(logger, config)
|
||||
|
||||
super().__init__(
|
||||
config,
|
||||
logger,
|
||||
repository_root_path,
|
||||
ProcessLaunchInfo(cmd=f"{terraform_ls_executable_path} serve", cwd=repository_root_path),
|
||||
"terraform",
|
||||
)
|
||||
self.server_ready = threading.Event()
|
||||
self.request_id = 0
|
||||
|
||||
|
||||
def _get_initialize_params(self, repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the Terraform Language Server.
|
||||
"""
|
||||
with open(os.path.join(os.path.dirname(__file__), "initialize_params.json"), encoding="utf-8") as f:
|
||||
d = json.load(f)
|
||||
|
||||
del d["_description"]
|
||||
|
||||
d["processId"] = os.getpid()
|
||||
assert d["rootPath"] == "$rootPath"
|
||||
d["rootPath"] = repository_absolute_path
|
||||
|
||||
assert d["rootUri"] == "$rootUri"
|
||||
d["rootUri"] = pathlib.Path(repository_absolute_path).as_uri()
|
||||
|
||||
assert d["workspaceFolders"][0]["uri"] == "$uri"
|
||||
d["workspaceFolders"][0]["uri"] = pathlib.Path(repository_absolute_path).as_uri()
|
||||
|
||||
assert d["workspaceFolders"][0]["name"] == "$name"
|
||||
d["workspaceFolders"][0]["name"] = os.path.basename(repository_absolute_path)
|
||||
|
||||
return d
|
||||
|
||||
def _start_server(self):
|
||||
"""Start terraform-ls server process"""
|
||||
|
||||
def register_capability_handler(params):
|
||||
return
|
||||
|
||||
def window_log_message(msg):
|
||||
self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
|
||||
|
||||
def do_nothing(params):
|
||||
return
|
||||
|
||||
self.server.on_request("client/registerCapability", register_capability_handler)
|
||||
self.server.on_notification("window/logMessage", window_log_message)
|
||||
self.server.on_notification("$/progress", do_nothing)
|
||||
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
|
||||
|
||||
self.logger.log("Starting terraform-ls server process", logging.INFO)
|
||||
self.server.start()
|
||||
initialize_params = self._get_initialize_params(self.repository_root_path)
|
||||
|
||||
self.logger.log(
|
||||
"Sending initialize request from LSP client to LSP server and awaiting response",
|
||||
logging.INFO,
|
||||
)
|
||||
init_response = self.server.send.initialize(initialize_params)
|
||||
|
||||
# Verify server capabilities
|
||||
assert "textDocumentSync" in init_response["capabilities"]
|
||||
assert "completionProvider" in init_response["capabilities"]
|
||||
assert "definitionProvider" in init_response["capabilities"]
|
||||
|
||||
self.server.notify.initialized({})
|
||||
self.completions_available.set()
|
||||
|
||||
# terraform-ls server is typically ready immediately after initialization
|
||||
self.server_ready.set()
|
||||
self.server_ready.wait()
|
||||
@@ -187,6 +187,11 @@ class SolidLanguageServer(ABC):
|
||||
|
||||
ls = ElixirTools(config, logger, repository_root_path)
|
||||
|
||||
elif config.code_language == Language.TERRAFORM:
|
||||
from solidlsp.language_servers.terraform_ls.terraform_ls import TerraformLS
|
||||
|
||||
ls = TerraformLS(config, logger, repository_root_path)
|
||||
|
||||
else:
|
||||
logger.log(f"Language {config.code_language} is not supported", logging.ERROR)
|
||||
raise LanguageServerException(f"Language {config.code_language} is not supported")
|
||||
|
||||
@@ -39,6 +39,7 @@ class Language(str, Enum):
|
||||
PHP = "php"
|
||||
CLOJURE = "clojure"
|
||||
ELIXIR = "elixir"
|
||||
TERRAFORM = "terraform"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
@@ -77,6 +78,8 @@ class Language(str, Enum):
|
||||
return FilenameMatcher("*.clj", "*.cljs", "*.cljc", "*.edn") # codespell:ignore edn
|
||||
case self.ELIXIR:
|
||||
return FilenameMatcher("*.ex", "*.exs")
|
||||
case self.TERRAFORM:
|
||||
return FilenameMatcher("*.tf", "*.tfvars", "*.tfstate")
|
||||
case _:
|
||||
raise ValueError(f"Unhandled language: {self}")
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Data sources for the Terraform configuration
|
||||
|
||||
# Get the latest Ubuntu AMI
|
||||
data "aws_ami" "ubuntu" {
|
||||
most_recent = true
|
||||
owners = ["099720109477"] # Canonical
|
||||
|
||||
filter {
|
||||
name = "name"
|
||||
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
|
||||
}
|
||||
|
||||
filter {
|
||||
name = "virtualization-type"
|
||||
values = ["hvm"]
|
||||
}
|
||||
}
|
||||
|
||||
# Get available availability zones
|
||||
data "aws_availability_zones" "available" {
|
||||
state = "available"
|
||||
}
|
||||
|
||||
# Get current AWS caller identity
|
||||
data "aws_caller_identity" "current" {}
|
||||
|
||||
# Get current AWS region
|
||||
data "aws_region" "current" {}
|
||||
@@ -0,0 +1,126 @@
|
||||
# Main Terraform configuration
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = var.aws_region
|
||||
}
|
||||
|
||||
# EC2 Instance
|
||||
resource "aws_instance" "web_server" {
|
||||
ami = data.aws_ami.ubuntu.id
|
||||
instance_type = var.instance_type
|
||||
|
||||
vpc_security_group_ids = [aws_security_group.web_sg.id]
|
||||
subnet_id = aws_subnet.public.id
|
||||
|
||||
user_data = <<-EOF
|
||||
#!/bin/bash
|
||||
apt-get update
|
||||
apt-get install -y nginx
|
||||
systemctl start nginx
|
||||
systemctl enable nginx
|
||||
EOF
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-web-server"
|
||||
Environment = var.environment
|
||||
Project = var.project_name
|
||||
}
|
||||
}
|
||||
|
||||
# S3 Bucket
|
||||
resource "aws_s3_bucket" "app_bucket" {
|
||||
bucket = "${var.project_name}-${var.environment}-bucket"
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-bucket"
|
||||
Environment = var.environment
|
||||
Project = var.project_name
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket_versioning" "app_bucket_versioning" {
|
||||
bucket = aws_s3_bucket.app_bucket.id
|
||||
versioning_configuration {
|
||||
status = "Enabled"
|
||||
}
|
||||
}
|
||||
|
||||
# VPC
|
||||
resource "aws_vpc" "main" {
|
||||
cidr_block = "10.0.0.0/16"
|
||||
enable_dns_hostnames = true
|
||||
enable_dns_support = true
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-vpc"
|
||||
Environment = var.environment
|
||||
Project = var.project_name
|
||||
}
|
||||
}
|
||||
|
||||
# Internet Gateway
|
||||
resource "aws_internet_gateway" "main" {
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-igw"
|
||||
Environment = var.environment
|
||||
Project = var.project_name
|
||||
}
|
||||
}
|
||||
|
||||
# Public Subnet
|
||||
resource "aws_subnet" "public" {
|
||||
vpc_id = aws_vpc.main.id
|
||||
cidr_block = "10.0.1.0/24"
|
||||
availability_zone = data.aws_availability_zones.available.names[0]
|
||||
map_public_ip_on_launch = true
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-public-subnet"
|
||||
Environment = var.environment
|
||||
Project = var.project_name
|
||||
}
|
||||
}
|
||||
|
||||
# Security Group
|
||||
resource "aws_security_group" "web_sg" {
|
||||
name_prefix = "${var.project_name}-web-"
|
||||
vpc_id = aws_vpc.main.id
|
||||
|
||||
ingress {
|
||||
from_port = 80
|
||||
to_port = 80
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
ingress {
|
||||
from_port = 443
|
||||
to_port = 443
|
||||
protocol = "tcp"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
egress {
|
||||
from_port = 0
|
||||
to_port = 0
|
||||
protocol = "-1"
|
||||
cidr_blocks = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = "${var.project_name}-web-sg"
|
||||
Environment = var.environment
|
||||
Project = var.project_name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
# Output values for the Terraform configuration
|
||||
|
||||
output "instance_id" {
|
||||
description = "ID of the EC2 instance"
|
||||
value = aws_instance.web_server.id
|
||||
}
|
||||
|
||||
output "instance_public_ip" {
|
||||
description = "Public IP address of the EC2 instance"
|
||||
value = aws_instance.web_server.public_ip
|
||||
}
|
||||
|
||||
output "instance_public_dns" {
|
||||
description = "Public DNS name of the EC2 instance"
|
||||
value = aws_instance.web_server.public_dns
|
||||
}
|
||||
|
||||
output "s3_bucket_name" {
|
||||
description = "Name of the S3 bucket"
|
||||
value = aws_s3_bucket.app_bucket.bucket
|
||||
}
|
||||
|
||||
output "s3_bucket_arn" {
|
||||
description = "ARN of the S3 bucket"
|
||||
value = aws_s3_bucket.app_bucket.arn
|
||||
}
|
||||
|
||||
output "vpc_id" {
|
||||
description = "ID of the VPC"
|
||||
value = aws_vpc.main.id
|
||||
}
|
||||
|
||||
output "subnet_id" {
|
||||
description = "ID of the public subnet"
|
||||
value = aws_subnet.public.id
|
||||
}
|
||||
|
||||
output "security_group_id" {
|
||||
description = "ID of the security group"
|
||||
value = aws_security_group.web_sg.id
|
||||
}
|
||||
|
||||
output "application_url" {
|
||||
description = "URL to access the application"
|
||||
value = "http://${aws_instance.web_server.public_dns}"
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
# Input variables for the Terraform configuration
|
||||
|
||||
variable "aws_region" {
|
||||
description = "AWS region for resources"
|
||||
type = string
|
||||
default = "us-west-2"
|
||||
}
|
||||
|
||||
variable "instance_type" {
|
||||
description = "EC2 instance type"
|
||||
type = string
|
||||
default = "t3.micro"
|
||||
|
||||
validation {
|
||||
condition = contains([
|
||||
"t3.micro", "t3.small", "t3.medium",
|
||||
"t2.micro", "t2.small", "t2.medium"
|
||||
], var.instance_type)
|
||||
error_message = "Instance type must be a valid t2 or t3 instance type."
|
||||
}
|
||||
}
|
||||
|
||||
variable "environment" {
|
||||
description = "Environment name (dev, staging, prod)"
|
||||
type = string
|
||||
default = "dev"
|
||||
|
||||
validation {
|
||||
condition = contains(["dev", "staging", "prod"], var.environment)
|
||||
error_message = "Environment must be dev, staging, or prod."
|
||||
}
|
||||
}
|
||||
|
||||
variable "project_name" {
|
||||
description = "Name of the project"
|
||||
type = string
|
||||
default = "terraform-test"
|
||||
|
||||
validation {
|
||||
condition = can(regex("^[a-z0-9-]+$", var.project_name))
|
||||
error_message = "Project name must contain only lowercase letters, numbers, and hyphens."
|
||||
}
|
||||
}
|
||||
|
||||
variable "enable_monitoring" {
|
||||
description = "Enable CloudWatch monitoring"
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "allowed_cidr_blocks" {
|
||||
description = "List of CIDR blocks allowed to access the application"
|
||||
type = list(string)
|
||||
default = ["0.0.0.0/0"]
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
description = "Additional tags to apply to resources"
|
||||
type = map(string)
|
||||
default = {}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Basic integration tests for the Terraform language server functionality.
|
||||
|
||||
These tests validate the functionality of the language server APIs
|
||||
like request_references using the test repository.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
|
||||
@pytest.mark.terraform
|
||||
class TestLanguageServerBasics:
|
||||
"""Test basic functionality of the Terraform language server."""
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True)
|
||||
def test_basic_definition(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test basic definition lookup functionality."""
|
||||
# Simple test to verify the language server is working
|
||||
file_path = "main.tf"
|
||||
# Just try to get document symbols - this should work without hanging
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
assert len(symbols) > 0, "Should find at least some symbols in main.tf"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True)
|
||||
def test_request_references_aws_instance(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test request_references on an aws_instance resource."""
|
||||
# Get references to an aws_instance resource in main.tf
|
||||
file_path = "main.tf"
|
||||
# Find aws_instance resources
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
aws_instance_symbol = next((s for s in symbols[0] if s.get("name") == 'resource "aws_instance" "web_server"'), None)
|
||||
if not aws_instance_symbol or "selectionRange" not in aws_instance_symbol:
|
||||
raise AssertionError("aws_instance symbol or its selectionRange not found")
|
||||
sel_start = aws_instance_symbol["selectionRange"]["start"]
|
||||
references = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
|
||||
assert len(references) >= 1, "aws_instance should be referenced at least once"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.TERRAFORM], indirect=True)
|
||||
def test_request_references_variable(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test request_references on a variable."""
|
||||
# Get references to a variable in variables.tf
|
||||
file_path = "variables.tf"
|
||||
# Find variable definitions
|
||||
symbols = language_server.request_document_symbols(file_path)
|
||||
var_symbol = next((s for s in symbols[0] if s.get("name") == 'variable "instance_type"'), None)
|
||||
if not var_symbol or "selectionRange" not in var_symbol:
|
||||
raise AssertionError("variable symbol or its selectionRange not found")
|
||||
sel_start = var_symbol["selectionRange"]["start"]
|
||||
references = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
|
||||
assert len(references) >= 1, "variable should be referenced at least once"
|
||||
Reference in new issue
Block a user