From 6f201510417c2930c51cc08dfbc020d660713791 Mon Sep 17 00:00:00 2001 From: Kristofer Atlas Date: Sun, 8 Mar 2026 18:27:43 +0000 Subject: [PATCH 1/3] fix: implement Read/Update/Delete for stub resources - Add Read, Update, Delete for account resource - Add Read, Update for disk_offering resource - Add Read, Delete for domain resource - Add Read for user resource --- cloudstack/resource_cloudstack_account.go | 71 ++++++++++++++++++- .../resource_cloudstack_disk_offering.go | 58 ++++++++++++++- cloudstack/resource_cloudstack_domain.go | 57 ++++++++++++++- cloudstack/resource_cloudstack_user.go | 39 ++++++++++ 4 files changed, 218 insertions(+), 7 deletions(-) diff --git a/cloudstack/resource_cloudstack_account.go b/cloudstack/resource_cloudstack_account.go index db6c1a2c..0eca882e 100644 --- a/cloudstack/resource_cloudstack_account.go +++ b/cloudstack/resource_cloudstack_account.go @@ -37,26 +37,32 @@ func resourceCloudStackAccount() *schema.Resource { "email": { Type: schema.TypeString, Required: true, + ForceNew: true, }, "first_name": { Type: schema.TypeString, Required: true, + ForceNew: true, }, "last_name": { Type: schema.TypeString, Required: true, + ForceNew: true, }, "password": { Type: schema.TypeString, Required: true, + ForceNew: true, }, "username": { Type: schema.TypeString, Required: true, + ForceNew: true, }, "account_type": { Type: schema.TypeInt, Required: true, + ForceNew: true, }, "role_id": { Type: schema.TypeString, @@ -110,9 +116,70 @@ func resourceCloudStackAccountCreate(d *schema.ResourceData, meta interface{}) e return resourceCloudStackAccountRead(d, meta) } -func resourceCloudStackAccountRead(d *schema.ResourceData, meta interface{}) error { return nil } +func resourceCloudStackAccountRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + log.Printf("[DEBUG] Reading Account %s", d.Id()) + + p := cs.Account.NewListAccountsParams() + p.SetId(d.Id()) + + accounts, err := cs.Account.ListAccounts(p) + if err != nil { + return fmt.Errorf("Error retrieving Account %s: %s", d.Id(), err) + } + + if accounts.Count == 0 { + log.Printf("[DEBUG] Account %s does no longer exist", d.Id()) + d.SetId("") + return nil + } + + account := accounts.Accounts[0] + + d.Set("account_type", account.Accounttype) + d.Set("role_id", account.Roleid) + d.Set("account", account.Name) + d.Set("domain_id", account.Domainid) + + if len(account.User) > 0 { + user := account.User[0] + d.Set("email", user.Email) + d.Set("first_name", user.Firstname) + d.Set("last_name", user.Lastname) + d.Set("username", user.Username) + } + + log.Printf("[DEBUG] Account %s successfully read", d.Id()) + return nil +} + +func resourceCloudStackAccountUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + log.Printf("[DEBUG] Updating Account %s", d.Id()) + + p := cs.Account.NewUpdateAccountParams() + p.SetId(d.Id()) + + if d.HasChange("role_id") { + p.SetRoleid(d.Get("role_id").(string)) + } + if d.HasChange("account") { + p.SetNewname(d.Get("account").(string)) + } + if d.HasChange("domain_id") { + p.SetDomainid(d.Get("domain_id").(string)) + } -func resourceCloudStackAccountUpdate(d *schema.ResourceData, meta interface{}) error { return nil } + _, err := cs.Account.UpdateAccount(p) + if err != nil { + return fmt.Errorf("Error updating Account %s: %s", d.Id(), err) + } + + log.Printf("[DEBUG] Account %s successfully updated", d.Id()) + return resourceCloudStackAccountRead(d, meta) +} func resourceCloudStackAccountDelete(d *schema.ResourceData, meta interface{}) error { cs := meta.(*cloudstack.CloudStackClient) diff --git a/cloudstack/resource_cloudstack_disk_offering.go b/cloudstack/resource_cloudstack_disk_offering.go index 197eaf4b..5470a873 100644 --- a/cloudstack/resource_cloudstack_disk_offering.go +++ b/cloudstack/resource_cloudstack_disk_offering.go @@ -20,6 +20,7 @@ package cloudstack import ( + "fmt" "log" "github.com/apache/cloudstack-go/v2/cloudstack" @@ -72,8 +73,59 @@ func resourceCloudStackDiskOfferingCreate(d *schema.ResourceData, meta interface return resourceCloudStackDiskOfferingRead(d, meta) } -func resourceCloudStackDiskOfferingRead(d *schema.ResourceData, meta interface{}) error { return nil } +func resourceCloudStackDiskOfferingRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + log.Printf("[DEBUG] Retrieving disk offering %s", d.Get("name").(string)) + + offering, count, err := cs.DiskOffering.GetDiskOfferingByID(d.Id()) + if err != nil { + if count == 0 { + log.Printf("[DEBUG] Disk offering %s does no longer exist", d.Get("name").(string)) + d.SetId("") + return nil + } + return fmt.Errorf("Error retrieving disk offering %s: %s", d.Id(), err) + } + + d.Set("name", offering.Name) + d.Set("display_text", offering.Displaytext) + d.Set("disk_size", offering.Disksize) + + return nil +} + +func resourceCloudStackDiskOfferingUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + p := cs.DiskOffering.NewUpdateDiskOfferingParams(d.Id()) + + if d.HasChange("name") { + p.SetName(d.Get("name").(string)) + } + if d.HasChange("display_text") { + p.SetDisplaytext(d.Get("display_text").(string)) + } -func resourceCloudStackDiskOfferingUpdate(d *schema.ResourceData, meta interface{}) error { return nil } + log.Printf("[DEBUG] Updating disk offering %s", d.Get("name").(string)) + _, err := cs.DiskOffering.UpdateDiskOffering(p) + if err != nil { + return fmt.Errorf("Error updating disk offering: %s", err) + } -func resourceCloudStackDiskOfferingDelete(d *schema.ResourceData, meta interface{}) error { return nil } + return resourceCloudStackDiskOfferingRead(d, meta) +} + +func resourceCloudStackDiskOfferingDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + p := cs.DiskOffering.NewDeleteDiskOfferingParams(d.Id()) + + log.Printf("[DEBUG] Deleting disk offering %s", d.Get("name").(string)) + _, err := cs.DiskOffering.DeleteDiskOffering(p) + if err != nil { + return fmt.Errorf("Error deleting disk offering: %s", err) + } + + return nil +} diff --git a/cloudstack/resource_cloudstack_domain.go b/cloudstack/resource_cloudstack_domain.go index 3e11b1a7..3a21dce9 100644 --- a/cloudstack/resource_cloudstack_domain.go +++ b/cloudstack/resource_cloudstack_domain.go @@ -89,9 +89,62 @@ func resourceCloudStackDomainCreate(d *schema.ResourceData, meta interface{}) er return resourceCloudStackDomainRead(d, meta) } -func resourceCloudStackDomainRead(d *schema.ResourceData, meta interface{}) error { return nil } +func resourceCloudStackDomainRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + log.Printf("[DEBUG] Reading Domain %s", d.Id()) + + p := cs.Domain.NewListDomainsParams() + p.SetId(d.Id()) + + domains, err := cs.Domain.ListDomains(p) + if err != nil { + return fmt.Errorf("Error reading Domain %s: %s", d.Id(), err) + } + + if domains.Count == 0 { + log.Printf("[DEBUG] Domain %s does no longer exist", d.Id()) + d.SetId("") + return nil + } + + domain := domains.Domains[0] + log.Printf("[DEBUG] Domain %s found: %s", d.Id(), domain.Name) + + d.Set("name", domain.Name) + d.Set("domain_id", domain.Id) + d.Set("network_domain", domain.Networkdomain) + d.Set("parent_domain_id", domain.Parentdomainid) + + return nil +} + +func resourceCloudStackDomainUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + name := d.Get("name").(string) + + if d.HasChange("name") || d.HasChange("network_domain") { + p := cs.Domain.NewUpdateDomainParams(d.Id()) -func resourceCloudStackDomainUpdate(d *schema.ResourceData, meta interface{}) error { return nil } + if d.HasChange("name") { + p.SetName(name) + } + + if d.HasChange("network_domain") { + p.SetNetworkdomain(d.Get("network_domain").(string)) + } + + log.Printf("[DEBUG] Updating Domain %s", name) + _, err := cs.Domain.UpdateDomain(p) + if err != nil { + return fmt.Errorf("Error updating Domain %s: %s", name, err) + } + log.Printf("[DEBUG] Domain %s successfully updated", name) + } + + return resourceCloudStackDomainRead(d, meta) +} func resourceCloudStackDomainDelete(d *schema.ResourceData, meta interface{}) error { cs := meta.(*cloudstack.CloudStackClient) diff --git a/cloudstack/resource_cloudstack_user.go b/cloudstack/resource_cloudstack_user.go index 74fb84a9..a14f1d1b 100644 --- a/cloudstack/resource_cloudstack_user.go +++ b/cloudstack/resource_cloudstack_user.go @@ -88,6 +88,28 @@ func resourceCloudStackUserCreate(d *schema.ResourceData, meta interface{}) erro } func resourceCloudStackUserUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + p := cs.User.NewUpdateUserParams(d.Id()) + + if d.HasChange("email") { + p.SetEmail(d.Get("email").(string)) + } + if d.HasChange("first_name") { + p.SetFirstname(d.Get("first_name").(string)) + } + if d.HasChange("last_name") { + p.SetLastname(d.Get("last_name").(string)) + } + if d.HasChange("password") { + p.SetPassword(d.Get("password").(string)) + } + + _, err := cs.User.UpdateUser(p) + if err != nil { + return err + } + return resourceCloudStackUserRead(d, meta) } @@ -106,5 +128,22 @@ func resourceCloudStackUserDelete(d *schema.ResourceData, meta interface{}) erro } func resourceCloudStackUserRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + user, count, err := cs.User.GetUserByID(d.Id()) + if err != nil { + if count == 0 { + d.SetId("") + return nil + } + return fmt.Errorf("Error reading User %s: %s", d.Id(), err) + } + + d.Set("account", user.Account) + d.Set("email", user.Email) + d.Set("first_name", user.Firstname) + d.Set("last_name", user.Lastname) + d.Set("username", user.Username) + return nil } From 17fcf469b1b8def8a3398d31a19e1e71edf5974f Mon Sep 17 00:00:00 2001 From: Kristofer Atlas Date: Sun, 8 Mar 2026 22:58:22 +0000 Subject: [PATCH 2/3] test: add provider test suites Add comprehensive and networking test configurations for validating the CloudStack Terraform Provider. --- tests/README.md | 42 +++ tests/comprehensive/README.md | 45 +++ tests/comprehensive/main.tf | 330 +++++++++++++++++++ tests/comprehensive/terraform.tfvars.example | 13 + tests/comprehensive/variables.tf | 63 ++++ tests/comprehensive/versions.tf | 9 + tests/networking/main.tf | 154 +++++++++ tests/networking/terraform.tfvars.example | 12 + tests/networking/variables.tf | 52 +++ 9 files changed, 720 insertions(+) create mode 100644 tests/README.md create mode 100644 tests/comprehensive/README.md create mode 100644 tests/comprehensive/main.tf create mode 100644 tests/comprehensive/terraform.tfvars.example create mode 100644 tests/comprehensive/variables.tf create mode 100644 tests/comprehensive/versions.tf create mode 100644 tests/networking/main.tf create mode 100644 tests/networking/terraform.tfvars.example create mode 100644 tests/networking/variables.tf diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..6908221f --- /dev/null +++ b/tests/README.md @@ -0,0 +1,42 @@ +# CloudStack Terraform Provider Test Suites + +This directory contains test configurations for validating the CloudStack Terraform Provider. + +## Test Suites + +### comprehensive/ +Full provider feature test covering VMs, networks, volumes, firewall rules, load balancers, and more. + +### networking/ +Focused test for networking configuration with egress rules. + +## Prerequisites + +1. CloudStack API credentials: + ```bash + export CLOUDSTACK_API_URL="your-api-url" + export CLOUDSTACK_API_KEY="your-api-key" + export CLOUDSTACK_SECRET_KEY="your-secret-key" + ``` + +2. Terraform 1.0+ + +3. Built provider (for local testing): + ```bash + make install + ``` + +## Running Tests + +```bash +cd tests/comprehensive # or tests/networking +terraform init +terraform plan +terraform apply +``` + +## Cleanup + +```bash +terraform destroy +``` diff --git a/tests/comprehensive/README.md b/tests/comprehensive/README.md new file mode 100644 index 00000000..30407f15 --- /dev/null +++ b/tests/comprehensive/README.md @@ -0,0 +1,45 @@ +# Comprehensive Provider Test Suite + +Tests the CloudStack Terraform Provider capabilities and identifies limitations. + +## Test Coverage + +1. SSH Keypair +2. VPC and Networking +3. Isolated Network with Source NAT +4. IP Address allocation +5. Security Groups and Firewall rules +6. Network ACL +7. VM Deployment (2 VMs with cloud-init, SSH keys, affinity groups, tags) +8. Affinity Groups +9. Volumes +10. Volume Attachments +11. Port Forwarding +12. Load Balancer +13. Static NAT +14. Secondary IP + +## Usage + +```bash +terraform init +terraform plan +terraform apply +terraform destroy +``` + +## Configuration + +Copy terraform.tfvars.example to terraform.tfvars and configure: + +- api_url - CloudStack API endpoint +- api_key - API key +- secret_key - API secret +- zone - Zone name +- network_offering - Network offering name +- service_offering - VM service offering +- template - Template name + +## Known Limitations + +Some resources have stub implementations or missing update functions. See provider documentation for details. diff --git a/tests/comprehensive/main.tf b/tests/comprehensive/main.tf new file mode 100644 index 00000000..e5fb70b6 --- /dev/null +++ b/tests/comprehensive/main.tf @@ -0,0 +1,330 @@ +# Test Infrastructure for CloudStack Terraform Provider Verification +# This tests all claimed provider capabilities and limitations + +provider "cloudstack" { + # Using environment variables: CLOUDSTACK_API_URL, CLOUDSTACK_API_KEY, CLOUDSTACK_SECRET_KEY +} + +# Generate unique test identifier +locals { + test_prefix = "tf-test" +} + +# ============================================ +# TEST 1: SSH Keypair (Tests: missing Update function) +# ============================================ + +resource "cloudstack_ssh_keypair" "test" { + name = "${local.test_prefix}-keypair" + public_key = file("~/.ssh/id_rsa.pub") +} + +# ============================================ +# TEST 2: VPC and Networking +# ============================================ + +resource "cloudstack_vpc" "test" { + name = "${local.test_prefix}-vpc" + cidr = "10.10.0.0/16" + vpc_offering = "Default VPC offering" + zone = var.zone +} + +# ============================================ +# TEST 3: Network (Isolated Network with Source NAT) +# ============================================ + +resource "cloudstack_network" "test" { + name = "${local.test_prefix}-network" + cidr = "10.10.1.0/24" + network_offering = "DefaultIsolatedNetworkOfferingForVpcNetworks" + vpc_id = cloudstack_vpc.test.id + zone = var.zone +} + +# ============================================ +# TEST 4: IP Address (Tests: missing Update function) +# ============================================ + +resource "cloudstack_ipaddress" "test" { + vpc_id = cloudstack_vpc.test.id + zone = var.zone +} + +# ============================================ +# TEST 5: Security Group and Firewall Rules +# ============================================ + +resource "cloudstack_security_group" "test" { + name = "${local.test_prefix}-sg" + description = "Test security group for provider verification" +} + +resource "cloudstack_firewall" "test" { + ip_address_id = cloudstack_ipaddress.test.id + + rule { + cidr_list = ["0.0.0.0/0"] + protocol = "tcp" + ports = ["22", "80", "443"] + } + + rule { + cidr_list = ["0.0.0.0/0"] + protocol = "icmp" + icmp_code = "-1" + icmp_type = "-1" + } + + managed = true # Workaround for firewall rule issues +} + +# ============================================ +# TEST 6: Network ACL (Tests: missing Update function) +# ============================================ + +resource "cloudstack_network_acl" "test" { + name = "${local.test_prefix}-acl" + description = "Test ACL for provider verification" + vpc_id = cloudstack_vpc.test.id +} + +resource "cloudstack_network_acl_rule" "test" { + acl_id = cloudstack_network_acl.test.id + + rule { + action = "allow" + cidr_list = ["0.0.0.0/0"] + protocol = "tcp" + port = 22 + traffic_type = "ingress" + rule_number = 100 + } + + rule { + action = "allow" + cidr_list = ["0.0.0.0/0"] + protocol = "tcp" + port = 80 + traffic_type = "ingress" + rule_number = 101 + } +} + +# ============================================ +# TEST 7: VM Deployment - Frontend (Web Server) +# ============================================ + +resource "cloudstack_instance" "frontend" { + name = "${local.test_prefix}-frontend" + service_offering = var.service_offering # Smallest offering + template = var.template + zone = var.zone + network_id = cloudstack_network.test.id + + # Small disk override + root_disk_size = 10 + + # SSH keypair + keypair = cloudstack_ssh_keypair.test.name + + # User data for simple web server + user_data = base64encode(<<-EOF + #!/bin/bash + apt-get update + apt-get install -y nginx + systemctl start nginx + systemctl enable nginx + echo "

Frontend VM - Test Infrastructure

" > /var/www/html/index.html + EOF + ) + + tags = { + role = "frontend" + test_run = "tf-test" + component = "web" + } +} + +# ============================================ +# TEST 8: VM Deployment - Backend (Database/API) +# ============================================ + +resource "cloudstack_instance" "backend" { + name = "${local.test_prefix}-backend" + service_offering = var.service_offering # Smallest offering + template = var.template + zone = var.zone + network_id = cloudstack_network.test.id + + # Small disk override + root_disk_size = 10 + + # SSH keypair + keypair = cloudstack_ssh_keypair.test.name + + # User data for simple backend + user_data = base64encode(<<-EOF + #!/bin/bash + apt-get update + apt-get install -y python3 python3-pip + cat > /tmp/simple_api.py << 'PYEOF' + from http.server import HTTPServer, SimpleHTTPRequestHandler + import json + + class APIHandler(SimpleHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + response = {"status": "ok", "role": "backend", "message": "Backend API running"} + self.wfile.write(json.dumps(response).encode()) + + if __name__ == "__main__": + server = HTTPServer(('', 8080), APIHandler) + print("Backend API running on port 8080") + server.serve_forever() + PYEOF + nohup python3 /tmp/simple_api.py > /tmp/api.log 2>&1 & + EOF + ) + + tags = { + role = "backend" + test_run = "tf-test" + component = "api" + } +} + +# ============================================ +# TEST 9: Affinity Group (Tests: missing Update function) +# DISABLED: Domain admin cannot create affinity groups +# ============================================ + +# resource "cloudstack_affinity_group" "test" { +# name = "${local.test_prefix}-affinity" +# type = "host anti-affinity" +# } + +# ============================================ +# TEST 10: Volume (Tests: missing Update function) +# ============================================ + +resource "cloudstack_volume" "test" { + name = "${local.test_prefix}-data-volume" + disk_offering_id = var.disk_offering_id + zone_id = var.zone_id +} + +# ============================================ +# TEST 11: Volume Attachment +# ============================================ + +resource "cloudstack_attach_volume" "test" { + volume_id = cloudstack_volume.test.id + virtual_machine_id = cloudstack_instance.backend.id +} + +# ============================================ +# TEST 12: Port Forwarding +# ============================================ + +resource "cloudstack_port_forward" "frontend_http" { + ip_address_id = cloudstack_ipaddress.test.id + + forward { + protocol = "tcp" + private_port = 80 + public_port = 80 + virtual_machine_id = cloudstack_instance.frontend.id + } +} + +resource "cloudstack_port_forward" "frontend_https" { + ip_address_id = cloudstack_ipaddress.test.id + + forward { + protocol = "tcp" + private_port = 443 + public_port = 443 + virtual_machine_id = cloudstack_instance.frontend.id + } +} + +resource "cloudstack_port_forward" "backend_api" { + ip_address_id = cloudstack_ipaddress.test.id + + forward { + protocol = "tcp" + private_port = 8080 + public_port = 8080 + virtual_machine_id = cloudstack_instance.backend.id + } +} + +resource "cloudstack_port_forward" "ssh" { + ip_address_id = cloudstack_ipaddress.test.id + + forward { + protocol = "tcp" + private_port = 22 + public_port = 22 + virtual_machine_id = cloudstack_instance.frontend.id + } +} + +# ============================================ +# TEST 13: Load Balancer Rule +# ============================================ + +resource "cloudstack_loadbalancer_rule" "test" { + name = "${local.test_prefix}-lb" + description = "Test load balancer rule" + ip_address_id = cloudstack_ipaddress.test.id + algorithm = "roundrobin" + private_port = 80 + public_port = 8888 + member_ids = [cloudstack_instance.frontend.id] +} + +# ============================================ +# TEST 14: Static NAT (Tests: missing Update function) +# ============================================ + +resource "cloudstack_static_nat" "test" { + ip_address_id = cloudstack_ipaddress.test.id + virtual_machine_id = cloudstack_instance.frontend.id +} + +# ============================================ +# TEST 15: Secondary IP Address (Tests: missing Update function) +# ============================================ + +resource "cloudstack_secondary_ipaddress" "test" { + virtual_machine_id = cloudstack_instance.backend.id +} + +# ============================================ +# Outputs for Verification +# ============================================ + +output "frontend_ip" { + value = cloudstack_ipaddress.test.ip_address + description = "Public IP address for accessing frontend" +} + +output "frontend_vm_id" { + value = cloudstack_instance.frontend.id +} + +output "backend_vm_id" { + value = cloudstack_instance.backend.id +} + +output "test_urls" { + value = { + frontend = "http://${cloudstack_ipaddress.test.ip_address}" + backend = "http://${cloudstack_ipaddress.test.ip_address}:8080" + } + description = "URLs to verify deployment" +} diff --git a/tests/comprehensive/terraform.tfvars.example b/tests/comprehensive/terraform.tfvars.example new file mode 100644 index 00000000..19df2c7b --- /dev/null +++ b/tests/comprehensive/terraform.tfvars.example @@ -0,0 +1,13 @@ +# CloudStack API Configuration +api_url = "https://cloudstack.example.com/client/api" +api_key = "YOUR_API_KEY" +secret_key = "YOUR_SECRET_KEY" + +# Zone and Resources +zone = "Zone1" +zone_id = "zone-uuid-here" +network_offering = "DefaultIsolatedNetworkSourceNatService" +service_offering = "Small Instance" +disk_offering = "Small" +disk_offering_id = "disk-offering-uuid-here" +template = "ubuntu-24.04-lts" diff --git a/tests/comprehensive/variables.tf b/tests/comprehensive/variables.tf new file mode 100644 index 00000000..6aa95908 --- /dev/null +++ b/tests/comprehensive/variables.tf @@ -0,0 +1,63 @@ +# Variables for test infrastructure + +variable "test_prefix" { + description = "Prefix for all test resources" + type = string + default = "tf-test" +} + +variable "api_url" { + description = "CloudStack API URL" + type = string +} + +variable "api_key" { + description = "CloudStack API key" + type = string + sensitive = true +} + +variable "secret_key" { + description = "CloudStack API secret key" + type = string + sensitive = true +} + +variable "zone" { + description = "CloudStack zone name" + type = string +} + +variable "network_offering" { + description = "Network offering name" + type = string + default = "DefaultIsolatedNetworkSourceNatService" +} + +variable "service_offering" { + description = "VM service offering name" + type = string + default = "Small Instance" +} + +variable "disk_offering" { + description = "Disk offering for data volumes" + type = string + default = "Small" +} + +variable "template" { + description = "Template name for VMs" + type = string + default = "ubuntu-24.04-lts" +} + +variable "disk_offering_id" { + description = "Disk offering ID for data volumes (use data source or CloudStack API to find)" + type = string +} + +variable "zone_id" { + description = "Zone ID (use data source or CloudStack API to find)" + type = string +} diff --git a/tests/comprehensive/versions.tf b/tests/comprehensive/versions.tf new file mode 100644 index 00000000..9fbaef1a --- /dev/null +++ b/tests/comprehensive/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + cloudstack = { + source = "local/cloudstack/cloudstack" + version = "0.5.0" + } + } + required_version = ">= 1.0.0" +} diff --git a/tests/networking/main.tf b/tests/networking/main.tf new file mode 100644 index 00000000..c1dd46be --- /dev/null +++ b/tests/networking/main.tf @@ -0,0 +1,154 @@ +# Test configuration for CloudStack networking with egress rules +# This demonstrates the missing piece for public SSH access: EGRESS RULES + +terraform { + required_providers { + cloudstack = { + source = "cloudstack/cloudstack" + version = ">= 0.5.0" + } + } +} + +provider "cloudstack" { + api_url = var.api_url + api_key = var.api_key + secret_key = var.secret_key +} + +# Create a network for the VM +resource "cloudstack_network" "test_network" { + name = "test-egress-network" + display_text = "Test network for egress rules verification" + cidr = "10.10.10.0/24" + network_offering = var.network_offering + zone = var.zone +} + +# Create security group with BOTH ingress and egress rules +# CloudStack is secure-by-default and blocks egress +# Without egress rules, VMs cannot reach the internet +resource "cloudstack_security_group" "test_sg" { + name = "test-egress-sg" + description = "Security group with SSH ingress and full egress access" +} + +# SSH ingress rule - allows incoming SSH connections +resource "cloudstack_security_group_rule" "ssh_ingress" { + security_group_id = cloudstack_security_group.test_sg.id + + rule { + cidr_list = ["0.0.0.0/0"] + protocol = "tcp" + ports = ["22"] + traffic_type = "ingress" + } +} + +# Egress rules for internet access +# Without these, the VM cannot reach the internet (apt update, curl, etc.) +resource "cloudstack_security_group_rule" "egress_all" { + security_group_id = cloudstack_security_group.test_sg.id + + rule { + cidr_list = ["0.0.0.0/0"] + protocol = "tcp" + ports = ["80", "443"] + traffic_type = "egress" + } +} + +# Also allow DNS egress (UDP) +resource "cloudstack_security_group_rule" "dns_egress" { + security_group_id = cloudstack_security_group.test_sg.id + + rule { + cidr_list = ["0.0.0.0/0"] + protocol = "udp" + ports = ["53"] + traffic_type = "egress" + } +} + +# Acquire a public IP address +resource "cloudstack_ipaddress" "test_ip" { + network_id = cloudstack_network.test_network.id + zone = var.zone +} + +# Create port forwarding rule for SSH access +resource "cloudstack_port_forward" "ssh_forward" { + ip_address_id = cloudstack_ipaddress.test_ip.id + + forward { + protocol = "tcp" + private_port = 22 + public_port = 22 + virtual_machine_id = cloudstack_instance.test_vm.id + vm_guest_ip = cloudstack_instance.test_vm.ip_address + } +} + +# Create the VM instance with security group +resource "cloudstack_instance" "test_vm" { + name = "test-egress-vm" + display_name = "Test VM for egress verification" + service_offering = var.service_offering + template = var.template + zone = var.zone + network_id = cloudstack_network.test_network.id + + # Attach security group with egress rules + security_group_names = [cloudstack_security_group.test_sg.name] + + # Use a small root disk (20GB is sufficient for testing) + root_disk_size = var.root_disk_size + + # SSH keypair for access + keypair = var.keypair + + # Cloud-init user data for initial setup + user_data = base64encode(<<-EOT + #cloud-config + package_update: true + packages: + - curl + runcmd: + - echo "Egress test VM initialized at $(date)" >> /var/log/egress-test.log + EOT + ) + + # Ensure VM starts + start_vm = true +} + +# Outputs for verification +output "public_ip" { + value = cloudstack_ipaddress.test_ip.ip_address + description = "Public IP address for SSH access" +} + +output "ssh_command" { + value = "ssh -i ~/.ssh/${var.keypair} ubuntu@${cloudstack_ipaddress.test_ip.ip_address}" + description = "SSH command to connect to the VM" +} + +output "vm_private_ip" { + value = cloudstack_instance.test_vm.ip_address + description = "VM's private IP address" +} + +output "network_id" { + value = cloudstack_network.test_network.id + description = "Network ID" +} + +output "security_group_id" { + value = cloudstack_security_group.test_sg.id + description = "Security group ID with egress rules" +} + +output "egress_test_command" { + value = "Run: curl -s https://ifconfig.me (should return public IP if egress works)" + description = "Command to test egress connectivity from inside VM" +} diff --git a/tests/networking/terraform.tfvars.example b/tests/networking/terraform.tfvars.example new file mode 100644 index 00000000..25351557 --- /dev/null +++ b/tests/networking/terraform.tfvars.example @@ -0,0 +1,12 @@ +# CloudStack API Configuration +api_url = "https://cloudstack.example.com/client/api" +api_key = "YOUR_API_KEY" +secret_key = "YOUR_SECRET_KEY" + +# Zone and Resources +zone = "Zone1" +network_offering = "DefaultIsolatedNetworkOfferingWithSourceNatService" +service_offering = "Small Instance" +template = "ubuntu-24.04-lts" +keypair = "my-keypair" +root_disk_size = 20 diff --git a/tests/networking/variables.tf b/tests/networking/variables.tf new file mode 100644 index 00000000..a83812e8 --- /dev/null +++ b/tests/networking/variables.tf @@ -0,0 +1,52 @@ +# Variables for CloudStack networking test configuration + +variable "api_url" { + type = string + description = "CloudStack API URL" +} + +variable "api_key" { + type = string + description = "CloudStack API key" + sensitive = true +} + +variable "secret_key" { + type = string + description = "CloudStack secret key" + sensitive = true +} + +variable "zone" { + type = string + description = "CloudStack zone name" +} + +variable "network_offering" { + type = string + description = "Network offering for isolated networks" + default = "DefaultIsolatedNetworkOfferingWithSourceNatService" +} + +variable "service_offering" { + type = string + description = "Service offering (instance size)" + default = "Small Instance" +} + +variable "template" { + type = string + description = "Template name for the VM" + default = "ubuntu-24.04-lts" +} + +variable "keypair" { + type = string + description = "SSH keypair name registered in CloudStack" +} + +variable "root_disk_size" { + type = number + description = "Root disk size in GB" + default = 20 +} From 744dbe863707b879cf9aab7f880b6285d5addeb4 Mon Sep 17 00:00:00 2001 From: kristofer-atlas Date: Tue, 10 Mar 2026 22:04:45 +0000 Subject: [PATCH 3/3] fix: address PR review feedback and CI failures - Domain: add Computed+ForceNew to domain_id and parent_domain_id to fix perpetual diffs causing 19 acceptance test failures - Account: remove unnecessary ForceNew from email/first_name/last_name/ password; implement user-level updates via updateUser API; add Sensitive to password field - Disk offering: mark disk_size as ForceNew since updateDiskOffering does not support size changes - User: mark account and username as ForceNew to prevent silent drift; add Sensitive to password; wrap Update error with resource context - Remove tests/ directory (manual TF configs, not Go acceptance tests) which also fixes Apache RAT check failures from missing headers --- cloudstack/resource_cloudstack_account.go | 74 ++-- .../resource_cloudstack_disk_offering.go | 1 + cloudstack/resource_cloudstack_domain.go | 4 + cloudstack/resource_cloudstack_user.go | 9 +- tests/README.md | 42 --- tests/comprehensive/README.md | 45 --- tests/comprehensive/main.tf | 330 ------------------ tests/comprehensive/terraform.tfvars.example | 13 - tests/comprehensive/variables.tf | 63 ---- tests/comprehensive/versions.tf | 9 - tests/networking/main.tf | 154 -------- tests/networking/terraform.tfvars.example | 12 - tests/networking/variables.tf | 52 --- 13 files changed, 65 insertions(+), 743 deletions(-) delete mode 100644 tests/README.md delete mode 100644 tests/comprehensive/README.md delete mode 100644 tests/comprehensive/main.tf delete mode 100644 tests/comprehensive/terraform.tfvars.example delete mode 100644 tests/comprehensive/variables.tf delete mode 100644 tests/comprehensive/versions.tf delete mode 100644 tests/networking/main.tf delete mode 100644 tests/networking/terraform.tfvars.example delete mode 100644 tests/networking/variables.tf diff --git a/cloudstack/resource_cloudstack_account.go b/cloudstack/resource_cloudstack_account.go index 0eca882e..3149cb21 100644 --- a/cloudstack/resource_cloudstack_account.go +++ b/cloudstack/resource_cloudstack_account.go @@ -37,22 +37,19 @@ func resourceCloudStackAccount() *schema.Resource { "email": { Type: schema.TypeString, Required: true, - ForceNew: true, }, "first_name": { Type: schema.TypeString, Required: true, - ForceNew: true, }, "last_name": { Type: schema.TypeString, Required: true, - ForceNew: true, }, "password": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Required: true, + Sensitive: true, }, "username": { Type: schema.TypeString, @@ -159,22 +156,59 @@ func resourceCloudStackAccountUpdate(d *schema.ResourceData, meta interface{}) e log.Printf("[DEBUG] Updating Account %s", d.Id()) - p := cs.Account.NewUpdateAccountParams() - p.SetId(d.Id()) - - if d.HasChange("role_id") { - p.SetRoleid(d.Get("role_id").(string)) - } - if d.HasChange("account") { - p.SetNewname(d.Get("account").(string)) - } - if d.HasChange("domain_id") { - p.SetDomainid(d.Get("domain_id").(string)) + // Handle account-level changes + if d.HasChange("role_id") || d.HasChange("account") || d.HasChange("domain_id") { + p := cs.Account.NewUpdateAccountParams() + p.SetId(d.Id()) + + if d.HasChange("role_id") { + p.SetRoleid(d.Get("role_id").(string)) + } + if d.HasChange("account") { + p.SetNewname(d.Get("account").(string)) + } + if d.HasChange("domain_id") { + p.SetDomainid(d.Get("domain_id").(string)) + } + + _, err := cs.Account.UpdateAccount(p) + if err != nil { + return fmt.Errorf("Error updating Account %s: %s", d.Id(), err) + } } - _, err := cs.Account.UpdateAccount(p) - if err != nil { - return fmt.Errorf("Error updating Account %s: %s", d.Id(), err) + // Handle user-level changes via updateUser API + if d.HasChange("email") || d.HasChange("first_name") || d.HasChange("last_name") || d.HasChange("password") { + lp := cs.Account.NewListAccountsParams() + lp.SetId(d.Id()) + accounts, err := cs.Account.ListAccounts(lp) + if err != nil { + return fmt.Errorf("Error retrieving Account %s for user update: %s", d.Id(), err) + } + if accounts.Count == 0 || len(accounts.Accounts[0].User) == 0 { + return fmt.Errorf("Account %s has no users to update", d.Id()) + } + + userID := accounts.Accounts[0].User[0].Id + up := cs.User.NewUpdateUserParams(userID) + + if d.HasChange("email") { + up.SetEmail(d.Get("email").(string)) + } + if d.HasChange("first_name") { + up.SetFirstname(d.Get("first_name").(string)) + } + if d.HasChange("last_name") { + up.SetLastname(d.Get("last_name").(string)) + } + if d.HasChange("password") { + up.SetPassword(d.Get("password").(string)) + } + + _, err = cs.User.UpdateUser(up) + if err != nil { + return fmt.Errorf("Error updating user for Account %s: %s", d.Id(), err) + } } log.Printf("[DEBUG] Account %s successfully updated", d.Id()) diff --git a/cloudstack/resource_cloudstack_disk_offering.go b/cloudstack/resource_cloudstack_disk_offering.go index 5470a873..d82fd6e0 100644 --- a/cloudstack/resource_cloudstack_disk_offering.go +++ b/cloudstack/resource_cloudstack_disk_offering.go @@ -45,6 +45,7 @@ func resourceCloudStackDiskOffering() *schema.Resource { "disk_size": { Type: schema.TypeInt, Required: true, + ForceNew: true, }, }, } diff --git a/cloudstack/resource_cloudstack_domain.go b/cloudstack/resource_cloudstack_domain.go index 3a21dce9..1c6b35a1 100644 --- a/cloudstack/resource_cloudstack_domain.go +++ b/cloudstack/resource_cloudstack_domain.go @@ -41,6 +41,8 @@ func resourceCloudStackDomain() *schema.Resource { "domain_id": { Type: schema.TypeString, Optional: true, + Computed: true, + ForceNew: true, }, "network_domain": { Type: schema.TypeString, @@ -49,6 +51,8 @@ func resourceCloudStackDomain() *schema.Resource { "parent_domain_id": { Type: schema.TypeString, Optional: true, + Computed: true, + ForceNew: true, }, }, } diff --git a/cloudstack/resource_cloudstack_user.go b/cloudstack/resource_cloudstack_user.go index a14f1d1b..5bef82a2 100644 --- a/cloudstack/resource_cloudstack_user.go +++ b/cloudstack/resource_cloudstack_user.go @@ -37,6 +37,7 @@ func resourceCloudStackUser() *schema.Resource { "account": { Type: schema.TypeString, Optional: true, + ForceNew: true, }, "email": { Type: schema.TypeString, @@ -51,12 +52,14 @@ func resourceCloudStackUser() *schema.Resource { Required: true, }, "password": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Required: true, + Sensitive: true, }, "username": { Type: schema.TypeString, Required: true, + ForceNew: true, }, }, } @@ -107,7 +110,7 @@ func resourceCloudStackUserUpdate(d *schema.ResourceData, meta interface{}) erro _, err := cs.User.UpdateUser(p) if err != nil { - return err + return fmt.Errorf("Error updating User %s: %s", d.Id(), err) } return resourceCloudStackUserRead(d, meta) diff --git a/tests/README.md b/tests/README.md deleted file mode 100644 index 6908221f..00000000 --- a/tests/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# CloudStack Terraform Provider Test Suites - -This directory contains test configurations for validating the CloudStack Terraform Provider. - -## Test Suites - -### comprehensive/ -Full provider feature test covering VMs, networks, volumes, firewall rules, load balancers, and more. - -### networking/ -Focused test for networking configuration with egress rules. - -## Prerequisites - -1. CloudStack API credentials: - ```bash - export CLOUDSTACK_API_URL="your-api-url" - export CLOUDSTACK_API_KEY="your-api-key" - export CLOUDSTACK_SECRET_KEY="your-secret-key" - ``` - -2. Terraform 1.0+ - -3. Built provider (for local testing): - ```bash - make install - ``` - -## Running Tests - -```bash -cd tests/comprehensive # or tests/networking -terraform init -terraform plan -terraform apply -``` - -## Cleanup - -```bash -terraform destroy -``` diff --git a/tests/comprehensive/README.md b/tests/comprehensive/README.md deleted file mode 100644 index 30407f15..00000000 --- a/tests/comprehensive/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Comprehensive Provider Test Suite - -Tests the CloudStack Terraform Provider capabilities and identifies limitations. - -## Test Coverage - -1. SSH Keypair -2. VPC and Networking -3. Isolated Network with Source NAT -4. IP Address allocation -5. Security Groups and Firewall rules -6. Network ACL -7. VM Deployment (2 VMs with cloud-init, SSH keys, affinity groups, tags) -8. Affinity Groups -9. Volumes -10. Volume Attachments -11. Port Forwarding -12. Load Balancer -13. Static NAT -14. Secondary IP - -## Usage - -```bash -terraform init -terraform plan -terraform apply -terraform destroy -``` - -## Configuration - -Copy terraform.tfvars.example to terraform.tfvars and configure: - -- api_url - CloudStack API endpoint -- api_key - API key -- secret_key - API secret -- zone - Zone name -- network_offering - Network offering name -- service_offering - VM service offering -- template - Template name - -## Known Limitations - -Some resources have stub implementations or missing update functions. See provider documentation for details. diff --git a/tests/comprehensive/main.tf b/tests/comprehensive/main.tf deleted file mode 100644 index e5fb70b6..00000000 --- a/tests/comprehensive/main.tf +++ /dev/null @@ -1,330 +0,0 @@ -# Test Infrastructure for CloudStack Terraform Provider Verification -# This tests all claimed provider capabilities and limitations - -provider "cloudstack" { - # Using environment variables: CLOUDSTACK_API_URL, CLOUDSTACK_API_KEY, CLOUDSTACK_SECRET_KEY -} - -# Generate unique test identifier -locals { - test_prefix = "tf-test" -} - -# ============================================ -# TEST 1: SSH Keypair (Tests: missing Update function) -# ============================================ - -resource "cloudstack_ssh_keypair" "test" { - name = "${local.test_prefix}-keypair" - public_key = file("~/.ssh/id_rsa.pub") -} - -# ============================================ -# TEST 2: VPC and Networking -# ============================================ - -resource "cloudstack_vpc" "test" { - name = "${local.test_prefix}-vpc" - cidr = "10.10.0.0/16" - vpc_offering = "Default VPC offering" - zone = var.zone -} - -# ============================================ -# TEST 3: Network (Isolated Network with Source NAT) -# ============================================ - -resource "cloudstack_network" "test" { - name = "${local.test_prefix}-network" - cidr = "10.10.1.0/24" - network_offering = "DefaultIsolatedNetworkOfferingForVpcNetworks" - vpc_id = cloudstack_vpc.test.id - zone = var.zone -} - -# ============================================ -# TEST 4: IP Address (Tests: missing Update function) -# ============================================ - -resource "cloudstack_ipaddress" "test" { - vpc_id = cloudstack_vpc.test.id - zone = var.zone -} - -# ============================================ -# TEST 5: Security Group and Firewall Rules -# ============================================ - -resource "cloudstack_security_group" "test" { - name = "${local.test_prefix}-sg" - description = "Test security group for provider verification" -} - -resource "cloudstack_firewall" "test" { - ip_address_id = cloudstack_ipaddress.test.id - - rule { - cidr_list = ["0.0.0.0/0"] - protocol = "tcp" - ports = ["22", "80", "443"] - } - - rule { - cidr_list = ["0.0.0.0/0"] - protocol = "icmp" - icmp_code = "-1" - icmp_type = "-1" - } - - managed = true # Workaround for firewall rule issues -} - -# ============================================ -# TEST 6: Network ACL (Tests: missing Update function) -# ============================================ - -resource "cloudstack_network_acl" "test" { - name = "${local.test_prefix}-acl" - description = "Test ACL for provider verification" - vpc_id = cloudstack_vpc.test.id -} - -resource "cloudstack_network_acl_rule" "test" { - acl_id = cloudstack_network_acl.test.id - - rule { - action = "allow" - cidr_list = ["0.0.0.0/0"] - protocol = "tcp" - port = 22 - traffic_type = "ingress" - rule_number = 100 - } - - rule { - action = "allow" - cidr_list = ["0.0.0.0/0"] - protocol = "tcp" - port = 80 - traffic_type = "ingress" - rule_number = 101 - } -} - -# ============================================ -# TEST 7: VM Deployment - Frontend (Web Server) -# ============================================ - -resource "cloudstack_instance" "frontend" { - name = "${local.test_prefix}-frontend" - service_offering = var.service_offering # Smallest offering - template = var.template - zone = var.zone - network_id = cloudstack_network.test.id - - # Small disk override - root_disk_size = 10 - - # SSH keypair - keypair = cloudstack_ssh_keypair.test.name - - # User data for simple web server - user_data = base64encode(<<-EOF - #!/bin/bash - apt-get update - apt-get install -y nginx - systemctl start nginx - systemctl enable nginx - echo "

Frontend VM - Test Infrastructure

" > /var/www/html/index.html - EOF - ) - - tags = { - role = "frontend" - test_run = "tf-test" - component = "web" - } -} - -# ============================================ -# TEST 8: VM Deployment - Backend (Database/API) -# ============================================ - -resource "cloudstack_instance" "backend" { - name = "${local.test_prefix}-backend" - service_offering = var.service_offering # Smallest offering - template = var.template - zone = var.zone - network_id = cloudstack_network.test.id - - # Small disk override - root_disk_size = 10 - - # SSH keypair - keypair = cloudstack_ssh_keypair.test.name - - # User data for simple backend - user_data = base64encode(<<-EOF - #!/bin/bash - apt-get update - apt-get install -y python3 python3-pip - cat > /tmp/simple_api.py << 'PYEOF' - from http.server import HTTPServer, SimpleHTTPRequestHandler - import json - - class APIHandler(SimpleHTTPRequestHandler): - def do_GET(self): - self.send_response(200) - self.send_header('Content-type', 'application/json') - self.end_headers() - response = {"status": "ok", "role": "backend", "message": "Backend API running"} - self.wfile.write(json.dumps(response).encode()) - - if __name__ == "__main__": - server = HTTPServer(('', 8080), APIHandler) - print("Backend API running on port 8080") - server.serve_forever() - PYEOF - nohup python3 /tmp/simple_api.py > /tmp/api.log 2>&1 & - EOF - ) - - tags = { - role = "backend" - test_run = "tf-test" - component = "api" - } -} - -# ============================================ -# TEST 9: Affinity Group (Tests: missing Update function) -# DISABLED: Domain admin cannot create affinity groups -# ============================================ - -# resource "cloudstack_affinity_group" "test" { -# name = "${local.test_prefix}-affinity" -# type = "host anti-affinity" -# } - -# ============================================ -# TEST 10: Volume (Tests: missing Update function) -# ============================================ - -resource "cloudstack_volume" "test" { - name = "${local.test_prefix}-data-volume" - disk_offering_id = var.disk_offering_id - zone_id = var.zone_id -} - -# ============================================ -# TEST 11: Volume Attachment -# ============================================ - -resource "cloudstack_attach_volume" "test" { - volume_id = cloudstack_volume.test.id - virtual_machine_id = cloudstack_instance.backend.id -} - -# ============================================ -# TEST 12: Port Forwarding -# ============================================ - -resource "cloudstack_port_forward" "frontend_http" { - ip_address_id = cloudstack_ipaddress.test.id - - forward { - protocol = "tcp" - private_port = 80 - public_port = 80 - virtual_machine_id = cloudstack_instance.frontend.id - } -} - -resource "cloudstack_port_forward" "frontend_https" { - ip_address_id = cloudstack_ipaddress.test.id - - forward { - protocol = "tcp" - private_port = 443 - public_port = 443 - virtual_machine_id = cloudstack_instance.frontend.id - } -} - -resource "cloudstack_port_forward" "backend_api" { - ip_address_id = cloudstack_ipaddress.test.id - - forward { - protocol = "tcp" - private_port = 8080 - public_port = 8080 - virtual_machine_id = cloudstack_instance.backend.id - } -} - -resource "cloudstack_port_forward" "ssh" { - ip_address_id = cloudstack_ipaddress.test.id - - forward { - protocol = "tcp" - private_port = 22 - public_port = 22 - virtual_machine_id = cloudstack_instance.frontend.id - } -} - -# ============================================ -# TEST 13: Load Balancer Rule -# ============================================ - -resource "cloudstack_loadbalancer_rule" "test" { - name = "${local.test_prefix}-lb" - description = "Test load balancer rule" - ip_address_id = cloudstack_ipaddress.test.id - algorithm = "roundrobin" - private_port = 80 - public_port = 8888 - member_ids = [cloudstack_instance.frontend.id] -} - -# ============================================ -# TEST 14: Static NAT (Tests: missing Update function) -# ============================================ - -resource "cloudstack_static_nat" "test" { - ip_address_id = cloudstack_ipaddress.test.id - virtual_machine_id = cloudstack_instance.frontend.id -} - -# ============================================ -# TEST 15: Secondary IP Address (Tests: missing Update function) -# ============================================ - -resource "cloudstack_secondary_ipaddress" "test" { - virtual_machine_id = cloudstack_instance.backend.id -} - -# ============================================ -# Outputs for Verification -# ============================================ - -output "frontend_ip" { - value = cloudstack_ipaddress.test.ip_address - description = "Public IP address for accessing frontend" -} - -output "frontend_vm_id" { - value = cloudstack_instance.frontend.id -} - -output "backend_vm_id" { - value = cloudstack_instance.backend.id -} - -output "test_urls" { - value = { - frontend = "http://${cloudstack_ipaddress.test.ip_address}" - backend = "http://${cloudstack_ipaddress.test.ip_address}:8080" - } - description = "URLs to verify deployment" -} diff --git a/tests/comprehensive/terraform.tfvars.example b/tests/comprehensive/terraform.tfvars.example deleted file mode 100644 index 19df2c7b..00000000 --- a/tests/comprehensive/terraform.tfvars.example +++ /dev/null @@ -1,13 +0,0 @@ -# CloudStack API Configuration -api_url = "https://cloudstack.example.com/client/api" -api_key = "YOUR_API_KEY" -secret_key = "YOUR_SECRET_KEY" - -# Zone and Resources -zone = "Zone1" -zone_id = "zone-uuid-here" -network_offering = "DefaultIsolatedNetworkSourceNatService" -service_offering = "Small Instance" -disk_offering = "Small" -disk_offering_id = "disk-offering-uuid-here" -template = "ubuntu-24.04-lts" diff --git a/tests/comprehensive/variables.tf b/tests/comprehensive/variables.tf deleted file mode 100644 index 6aa95908..00000000 --- a/tests/comprehensive/variables.tf +++ /dev/null @@ -1,63 +0,0 @@ -# Variables for test infrastructure - -variable "test_prefix" { - description = "Prefix for all test resources" - type = string - default = "tf-test" -} - -variable "api_url" { - description = "CloudStack API URL" - type = string -} - -variable "api_key" { - description = "CloudStack API key" - type = string - sensitive = true -} - -variable "secret_key" { - description = "CloudStack API secret key" - type = string - sensitive = true -} - -variable "zone" { - description = "CloudStack zone name" - type = string -} - -variable "network_offering" { - description = "Network offering name" - type = string - default = "DefaultIsolatedNetworkSourceNatService" -} - -variable "service_offering" { - description = "VM service offering name" - type = string - default = "Small Instance" -} - -variable "disk_offering" { - description = "Disk offering for data volumes" - type = string - default = "Small" -} - -variable "template" { - description = "Template name for VMs" - type = string - default = "ubuntu-24.04-lts" -} - -variable "disk_offering_id" { - description = "Disk offering ID for data volumes (use data source or CloudStack API to find)" - type = string -} - -variable "zone_id" { - description = "Zone ID (use data source or CloudStack API to find)" - type = string -} diff --git a/tests/comprehensive/versions.tf b/tests/comprehensive/versions.tf deleted file mode 100644 index 9fbaef1a..00000000 --- a/tests/comprehensive/versions.tf +++ /dev/null @@ -1,9 +0,0 @@ -terraform { - required_providers { - cloudstack = { - source = "local/cloudstack/cloudstack" - version = "0.5.0" - } - } - required_version = ">= 1.0.0" -} diff --git a/tests/networking/main.tf b/tests/networking/main.tf deleted file mode 100644 index c1dd46be..00000000 --- a/tests/networking/main.tf +++ /dev/null @@ -1,154 +0,0 @@ -# Test configuration for CloudStack networking with egress rules -# This demonstrates the missing piece for public SSH access: EGRESS RULES - -terraform { - required_providers { - cloudstack = { - source = "cloudstack/cloudstack" - version = ">= 0.5.0" - } - } -} - -provider "cloudstack" { - api_url = var.api_url - api_key = var.api_key - secret_key = var.secret_key -} - -# Create a network for the VM -resource "cloudstack_network" "test_network" { - name = "test-egress-network" - display_text = "Test network for egress rules verification" - cidr = "10.10.10.0/24" - network_offering = var.network_offering - zone = var.zone -} - -# Create security group with BOTH ingress and egress rules -# CloudStack is secure-by-default and blocks egress -# Without egress rules, VMs cannot reach the internet -resource "cloudstack_security_group" "test_sg" { - name = "test-egress-sg" - description = "Security group with SSH ingress and full egress access" -} - -# SSH ingress rule - allows incoming SSH connections -resource "cloudstack_security_group_rule" "ssh_ingress" { - security_group_id = cloudstack_security_group.test_sg.id - - rule { - cidr_list = ["0.0.0.0/0"] - protocol = "tcp" - ports = ["22"] - traffic_type = "ingress" - } -} - -# Egress rules for internet access -# Without these, the VM cannot reach the internet (apt update, curl, etc.) -resource "cloudstack_security_group_rule" "egress_all" { - security_group_id = cloudstack_security_group.test_sg.id - - rule { - cidr_list = ["0.0.0.0/0"] - protocol = "tcp" - ports = ["80", "443"] - traffic_type = "egress" - } -} - -# Also allow DNS egress (UDP) -resource "cloudstack_security_group_rule" "dns_egress" { - security_group_id = cloudstack_security_group.test_sg.id - - rule { - cidr_list = ["0.0.0.0/0"] - protocol = "udp" - ports = ["53"] - traffic_type = "egress" - } -} - -# Acquire a public IP address -resource "cloudstack_ipaddress" "test_ip" { - network_id = cloudstack_network.test_network.id - zone = var.zone -} - -# Create port forwarding rule for SSH access -resource "cloudstack_port_forward" "ssh_forward" { - ip_address_id = cloudstack_ipaddress.test_ip.id - - forward { - protocol = "tcp" - private_port = 22 - public_port = 22 - virtual_machine_id = cloudstack_instance.test_vm.id - vm_guest_ip = cloudstack_instance.test_vm.ip_address - } -} - -# Create the VM instance with security group -resource "cloudstack_instance" "test_vm" { - name = "test-egress-vm" - display_name = "Test VM for egress verification" - service_offering = var.service_offering - template = var.template - zone = var.zone - network_id = cloudstack_network.test_network.id - - # Attach security group with egress rules - security_group_names = [cloudstack_security_group.test_sg.name] - - # Use a small root disk (20GB is sufficient for testing) - root_disk_size = var.root_disk_size - - # SSH keypair for access - keypair = var.keypair - - # Cloud-init user data for initial setup - user_data = base64encode(<<-EOT - #cloud-config - package_update: true - packages: - - curl - runcmd: - - echo "Egress test VM initialized at $(date)" >> /var/log/egress-test.log - EOT - ) - - # Ensure VM starts - start_vm = true -} - -# Outputs for verification -output "public_ip" { - value = cloudstack_ipaddress.test_ip.ip_address - description = "Public IP address for SSH access" -} - -output "ssh_command" { - value = "ssh -i ~/.ssh/${var.keypair} ubuntu@${cloudstack_ipaddress.test_ip.ip_address}" - description = "SSH command to connect to the VM" -} - -output "vm_private_ip" { - value = cloudstack_instance.test_vm.ip_address - description = "VM's private IP address" -} - -output "network_id" { - value = cloudstack_network.test_network.id - description = "Network ID" -} - -output "security_group_id" { - value = cloudstack_security_group.test_sg.id - description = "Security group ID with egress rules" -} - -output "egress_test_command" { - value = "Run: curl -s https://ifconfig.me (should return public IP if egress works)" - description = "Command to test egress connectivity from inside VM" -} diff --git a/tests/networking/terraform.tfvars.example b/tests/networking/terraform.tfvars.example deleted file mode 100644 index 25351557..00000000 --- a/tests/networking/terraform.tfvars.example +++ /dev/null @@ -1,12 +0,0 @@ -# CloudStack API Configuration -api_url = "https://cloudstack.example.com/client/api" -api_key = "YOUR_API_KEY" -secret_key = "YOUR_SECRET_KEY" - -# Zone and Resources -zone = "Zone1" -network_offering = "DefaultIsolatedNetworkOfferingWithSourceNatService" -service_offering = "Small Instance" -template = "ubuntu-24.04-lts" -keypair = "my-keypair" -root_disk_size = 20 diff --git a/tests/networking/variables.tf b/tests/networking/variables.tf deleted file mode 100644 index a83812e8..00000000 --- a/tests/networking/variables.tf +++ /dev/null @@ -1,52 +0,0 @@ -# Variables for CloudStack networking test configuration - -variable "api_url" { - type = string - description = "CloudStack API URL" -} - -variable "api_key" { - type = string - description = "CloudStack API key" - sensitive = true -} - -variable "secret_key" { - type = string - description = "CloudStack secret key" - sensitive = true -} - -variable "zone" { - type = string - description = "CloudStack zone name" -} - -variable "network_offering" { - type = string - description = "Network offering for isolated networks" - default = "DefaultIsolatedNetworkOfferingWithSourceNatService" -} - -variable "service_offering" { - type = string - description = "Service offering (instance size)" - default = "Small Instance" -} - -variable "template" { - type = string - description = "Template name for the VM" - default = "ubuntu-24.04-lts" -} - -variable "keypair" { - type = string - description = "SSH keypair name registered in CloudStack" -} - -variable "root_disk_size" { - type = number - description = "Root disk size in GB" - default = 20 -}