Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 36 additions & 16 deletions ai_reviewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from dotenv import load_dotenv
from groq import Groq
from groq.types.chat import ChatCompletionUserMessageParam
import glob

load_dotenv()

Expand All @@ -13,28 +14,47 @@

client = Groq(api_key=GROQ_API_KEY)

with open("main.tf", "r") as file:
code_to_review = file.read()
tf_files = glob.glob("**/*.tf", recursive=True)
if not tf_files:
print("No Terraform files found.")
sys.exit(0)

prompt = f"Act as a Cloud security and terraform expert. Check if this code has any errors (it is meant for Azure), security vulnerabilities, invalid syntax, or bad practices. If it has, you MUST start your response with the exact word 'REJECTED'. Only reject the code if there are undeniable, critical security vulnerabilities or actual syntax errors. Do not reject for stylistic choices or implicit Terraform dependencies. If the code is fine and safe, start with the exact word 'APPROVED'. Then provide a brief explanation. \nTerraform Code: \n{code_to_review}"
has_failures = False

messages: list[ChatCompletionUserMessageParam] = [{"role": "user", "content": prompt}]
for file_path in tf_files:
print(f"\nAnalyzing: {file_path}")
with open(file_path, "r", encoding="utf-8") as file:
code_to_review = file.read()

response = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=messages,
temperature=0.1
)
if not code_to_review.strip():
print("Skipping empty file.")
continue

response_text = response.choices[0].message.content
prompt = f"Act as a Cloud security and terraform expert. Check if this code has any errors (it is meant for Azure), security vulnerabilities, invalid syntax, or bad practices. If it has, you MUST start your response with the exact word 'REJECTED'. Only reject the code if there are undeniable, critical security vulnerabilities or actual syntax errors. Do not reject for stylistic choices or implicit Terraform dependencies. If the code is fine and safe, start with the exact word 'APPROVED'. Then provide a brief explanation. \nTerraform Code: \n{code_to_review}"

print("AI Review of the Code:")
print(response_text)
print("\n")
messages: list[ChatCompletionUserMessageParam] = [{"role": "user", "content": prompt}]

if response_text.strip().upper().startswith("REJECTED"):
print("\nCRITICAL ERROR: AI has rejected this code! The pipeline will now stop.")
response = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=messages,
temperature=0.1
)

response_text = response.choices[0].message.content

print("AI Review of the Code:")
print(response_text)
print("\n")

if response_text.strip().upper().startswith("REJECTED"):
print(f"❌ CRITICAL ERROR: AI has rejected this code in {file_path}!")
has_failures = True
else:
print(f"✅ SUCCESS: AI has approved this code in {file_path}.")

if has_failures:
print("\nCRITICAL ERROR: AI has rejected one or more Terraform files! The pipeline will now stop.")
sys.exit(1) # this tells GitHub: stop pipeline
else:
print("\nSUCCESS: AI has approved this code.")
print("\nSUCCESS: AI has approved all Terraform code.")
sys.exit(0)
117 changes: 117 additions & 0 deletions iac_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from dotenv import load_dotenv
from groq import Groq
import os
import re
import sys

load_dotenv()

GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
if not GROQ_API_KEY:
print("\nERROR: No API key found in environment variables.")
sys.exit(1)

client = Groq(api_key=GROQ_API_KEY)

# Color codes for terminal
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
RESET = '\033[0m'
BOLD = '\033[1m'

def get_user_inputs():
print(f"\n{BOLD} IAC Generator with AI {RESET}\n")

dir = input("Enter the directory where you want to generate the IaC files (e.g., './iac_output'): ") or "./iac_output"
cloud = input("Enter the cloud provider (e.g., 'AWS', 'Azure', 'GCP'): ") or "azure"

desc = input("Enter a brief description of the infrastructure you want to generate: ")
if not desc:
print(f"\n{RED} ERROR: No description provided. {RESET}")
sys.exit(1)

print("\nAnswer the following questions (ENTER for default values): ")
proj_name = input("Project name [myproject]: ") or "myproject"
envs = input("Environments (comma-separated) [staging, prod]: ") or "staging, prod"

return dir, cloud, desc, proj_name, envs


def generate_iac(dir, cloud, desc, proj_name, envs):

print(f"\n{YELLOW} Generating iac for: {desc}... {RESET}\n")

prompt = f"""Act as a Senior Cloud and DevOps Engineer expert in Terraform.
Your task is to write production-ready Terraform code for {cloud.upper()}.

Project Details:
- Project Name: {proj_name}
- Environments needed: {envs}
- Architecture Requirements: {desc}

CRITICAL INSTRUCTIONS FOR OUTPUT FORMAT:
You must output multiple files. For EACH file, use EXACTLY this format:

### [relative/path/to/file.tf]
```hcl
# File content here
```

Make sure to include necessary files like:
- providers.tf
- variables.tf
- main.tf (or split by resource like network.tf, compute.tf, databases.tf)
- outputs.tf
- envs/ (folder with .tfvars files for the requested environments)

Do not add extra explanations outside of the file blocks. Just output the files."""


answer = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=[{"role": "user", "content": prompt}],
temperature=0.2
)

return answer.choices[0].message.content


def extract_files_and_save(ia_text, base_dir):
# Use REGEX expressions to find all blocks in the requested format
standard = re.compile(r"###\s*\[?(.+?)\]?\n.*?```(?:hcl|terraform|tf|json|yaml)?\n(.*?)```", re.DOTALL)
matches = standard.findall(ia_text)

if not matches:
print(f"\n{RED} ERROR: No files found in the AI response. {RESET}")
return

print(f"\n{len(matches)} generated files. Review each one before confirming:\n")

for path, content in matches:
print(f"{GREEN}[NEW]{RESET} {path.strip()}")

confirm = input("\nDo you want to save these files? (y/n): ").lower()

if confirm != 'y':
print(f"\n{RED} Aborting file save. {RESET}")
return

for path, content in matches:
clean_path = path.strip().strip("/") # clean spaces and initial "/"
full_path = os.path.join(base_dir, clean_path)

os.makedirs(os.path.dirname(full_path), exist_ok=True)

with open(full_path, "w", encoding="utf-8") as f:
f.write(content.strip() + "\n")

print(f"\n{GREEN} All files saved successfully in {base_dir}! {RESET}\n")

def main():
dir, cloud, desc, proj_name, envs = get_user_inputs()
ia_text = generate_iac(dir, cloud, desc, proj_name, envs)
extract_files_and_save(ia_text, dir)

if __name__ == "__main__":
main()
20 changes: 20 additions & 0 deletions iac_output/compute.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Create a service plan
resource "azurerm_service_plan" "plan" {
name = "myproject-plan"
resource_group_name = var.resource_group_name
location = var.location
os_type = "Linux"
sku_name = "P1v2"
}

# Create a web app
resource "azurerm_linux_web_app" "app" {
name = var.app_service_name
resource_group_name = var.resource_group_name
location = var.location
service_plan_name = azurerm_service_plan.plan.name

site_config {
linux_fx_version = "NODE|14-lts"
}
}
25 changes: 25 additions & 0 deletions iac_output/databases.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Create a PostgreSQL server
resource "azurerm_postgresql_server" "database" {
name = "myproject-postgres"
resource_group_name = var.resource_group_name
location = var.location
sku_name = "GP_Gen5_2"

storage_mb = 5120
backup_retention_days = 7
geo_redundant_backup_enabled = false
auto_grow_enabled = true

administrator_login = var.database_username
administrator_login_password = var.database_password
version = "11"
}

# Create a PostgreSQL database
resource "azurerm_postgresql_database" "db" {
name = var.database_name
resource_group_name = var.resource_group_name
server_name = azurerm_postgresql_server.database.name
charset = "UTF8"
collation = "English_United States.1252"
}
8 changes: 8 additions & 0 deletions iac_output/envs/prod.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
environment = "prod"
location = "East US"
resource_group_name = "myproject-prod-rg"
storage_account_name = "myprojectprodstorage"
app_service_name = "myproject-prod-app"
database_name = "myproject_prod_db"
database_username = "myproject_prod_user"
database_password = "myproject_prod_password"
8 changes: 8 additions & 0 deletions iac_output/envs/staging.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
environment = "staging"
location = "West US"
resource_group_name = "myproject-staging-rg"
storage_account_name = "myprojectstagingstorage"
app_service_name = "myproject-staging-app"
database_name = "myproject_staging_db"
database_username = "myproject_staging_user"
database_password = "myproject_staging_password"
15 changes: 15 additions & 0 deletions iac_output/network.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Create a virtual network
resource "azurerm_virtual_network" "vnet" {
name = "myproject-vnet"
address_space = ["10.0.0.0/16"]
location = var.location
resource_group_name = var.resource_group_name
}

# Create a subnet
resource "azurerm_subnet" "subnet" {
name = "myproject-subnet"
resource_group_name = var.resource_group_name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.0.1.0/24"]
}
15 changes: 15 additions & 0 deletions iac_output/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Output the storage account URL
output "storage_account_url" {
value = azurerm_storage_account.storage.primary_web_endpoint
}

# Output the web app URL
output "web_app_url" {
value = azurerm_linux_web_app.app.default_hostname
}

# Output the database connection string
output "database_connection_string" {
value = "host=${azurerm_postgresql_server.database.fqdn} port=5432 dbname=${azurerm_postgresql_database.db.name} user=${var.database_username} password=${var.database_password} sslmode=require"
sensitive = true
}
4 changes: 4 additions & 0 deletions iac_output/providers.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Configure the Azure Provider
provider "azurerm" {
features {}
}
13 changes: 13 additions & 0 deletions iac_output/storage.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Create a storage account
resource "azurerm_storage_account" "storage" {
name = var.storage_account_name
resource_group_name = var.resource_group_name
location = var.location
account_tier = "Standard"
account_replication_type = "LRS"

static_website {
index_document = "index.html"
error_404_document = "404.html"
}
}
41 changes: 41 additions & 0 deletions iac_output/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Define input variables
variable "environment" {
type = string
description = "Environment name"
}

variable "location" {
type = string
description = "Location for all resources"
}

variable "resource_group_name" {
type = string
description = "Resource group name"
}

variable "storage_account_name" {
type = string
description = "Storage account name"
}

variable "app_service_name" {
type = string
description = "App service name"
}

variable "database_name" {
type = string
description = "Database name"
}

variable "database_username" {
type = string
description = "Database username"
}

variable "database_password" {
type = string
sensitive = true
description = "Database password"
}
Loading