diff --git a/cloudstack/resource_cloudstack_instance.go b/cloudstack/resource_cloudstack_instance.go index 6a38ddb4..6fd2f54f 100644 --- a/cloudstack/resource_cloudstack_instance.go +++ b/cloudstack/resource_cloudstack_instance.go @@ -364,7 +364,19 @@ func resourceCloudStackInstanceCreate(d *schema.ResourceData, meta interface{}) if zone.Networktype == "Advanced" { // Set the default network ID - p.SetNetworkids([]string{d.Get("network_id").(string)}) + networkID := d.Get("network_id").(string) + p.SetNetworkids([]string{networkID}) + + // If no project is explicitly set, try to inherit it from the network + if _, ok := d.GetOk("project"); !ok && networkID != "" { + // Get the network to retrieve its project + // Use projectid=-1 to search across all projects + network, count, err := cs.Network.GetNetworkByID(networkID, cloudstack.WithProject("-1")) + if err == nil && count > 0 && network.Projectid != "" { + log.Printf("[DEBUG] Inheriting project %s from network %s", network.Projectid, networkID) + p.SetProjectid(network.Projectid) + } + } } // If there is a ipaddres supplied, add it to the parameter struct @@ -414,6 +426,7 @@ func resourceCloudStackInstanceCreate(d *schema.ResourceData, meta interface{}) } // If there is a project supplied, we retrieve and set the project id + // This will override the inherited project from network if explicitly set if err := setProjectid(p, cs, d); err != nil { return err } @@ -497,10 +510,22 @@ func resourceCloudStackInstanceRead(d *schema.ResourceData, meta interface{}) er cs := meta.(*cloudstack.CloudStackClient) // Get the virtual machine details + // First try with the project from state (if any) + project := d.Get("project").(string) vm, count, err := cs.VirtualMachine.GetVirtualMachineByID( d.Id(), - cloudstack.WithProject(d.Get("project").(string)), + cloudstack.WithProject(project), ) + + // If not found and no explicit project was set, try with projectid=-1 + // This handles the case where the project was inherited from network + if count == 0 && project == "" { + vm, count, err = cs.VirtualMachine.GetVirtualMachineByID( + d.Id(), + cloudstack.WithProject("-1"), + ) + } + if err != nil { if count == 0 { log.Printf("[DEBUG] Instance %s does no longer exist", d.Get("name").(string)) @@ -516,6 +541,11 @@ func resourceCloudStackInstanceRead(d *schema.ResourceData, meta interface{}) er d.Set("display_name", vm.Displayname) d.Set("group", vm.Group) + // Set the project if the instance belongs to one + if vm.Project != "" { + d.Set("project", vm.Project) + } + // In some rare cases (when destroying a machine fails) it can happen that // an instance does not have any attached NIC anymore. if len(vm.Nic) > 0 { diff --git a/cloudstack/resource_cloudstack_instance_test.go b/cloudstack/resource_cloudstack_instance_test.go index 5979aaaf..795901ba 100644 --- a/cloudstack/resource_cloudstack_instance_test.go +++ b/cloudstack/resource_cloudstack_instance_test.go @@ -234,6 +234,29 @@ func TestAccCloudStackInstance_project(t *testing.T) { }) } +func TestAccCloudStackInstance_networkProjectInheritance(t *testing.T) { + var instance cloudstack.VirtualMachine + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackInstance_networkProjectInheritance, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackInstanceExists( + "cloudstack_instance.foobar", &instance), + // Verify the project was inherited from the network + resource.TestCheckResourceAttr( + "cloudstack_instance.foobar", "project", "terraform"), + testAccCheckCloudStackInstanceProjectInherited(&instance), + ), + }, + }, + }) +} + func TestAccCloudStackInstance_import(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -371,6 +394,18 @@ func testAccCheckCloudStackInstanceRenamedAndResized( } } +func testAccCheckCloudStackInstanceProjectInherited( + instance *cloudstack.VirtualMachine) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if instance.Project != "terraform" { + return fmt.Errorf("Expected project to be 'terraform' (inherited from network), got: %s", instance.Project) + } + + return nil + } +} + func testAccCheckCloudStackInstanceDestroy(s *terraform.State) error { cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) @@ -576,3 +611,24 @@ ${random_bytes.string.base64} EOF EOFTF }` + +const testAccCloudStackInstance_networkProjectInheritance = ` +resource "cloudstack_network" "foo" { + name = "terraform-network" + display_text = "terraform-network" + cidr = "10.1.1.0/24" + network_offering = "DefaultIsolatedNetworkOfferingWithSourceNatService" + project = "terraform" + zone = "Sandbox-simulator" +} + +resource "cloudstack_instance" "foobar" { + name = "terraform-test" + display_name = "terraform-test" + service_offering= "Small Instance" + network_id = cloudstack_network.foo.id + template = "CentOS 5.6 (64-bit) no GUI (Simulator)" + zone = cloudstack_network.foo.zone + expunge = true + # Note: project is NOT specified here - it should be inherited from the network +}` diff --git a/cloudstack/resource_cloudstack_ipaddress.go b/cloudstack/resource_cloudstack_ipaddress.go index c7db4ac0..adcea914 100644 --- a/cloudstack/resource_cloudstack_ipaddress.go +++ b/cloudstack/resource_cloudstack_ipaddress.go @@ -102,11 +102,33 @@ func resourceCloudStackIPAddressCreate(d *schema.ResourceData, meta interface{}) if vpcid, ok := d.GetOk("vpc_id"); ok && vpcid.(string) != "" { return fmt.Errorf("set only network_id or vpc_id") } + + // If no project is explicitly set, try to inherit it from the network + if _, ok := d.GetOk("project"); !ok { + // Get the network to retrieve its project + // Use projectid=-1 to search across all projects + network, count, err := cs.Network.GetNetworkByID(networkid.(string), cloudstack.WithProject("-1")) + if err == nil && count > 0 && network.Projectid != "" { + log.Printf("[DEBUG] Inheriting project %s from network %s", network.Projectid, networkid.(string)) + p.SetProjectid(network.Projectid) + } + } } if vpcid, ok := d.GetOk("vpc_id"); ok { // Set the vpcid p.SetVpcid(vpcid.(string)) + + // If no project is explicitly set, try to inherit it from the VPC + if _, ok := d.GetOk("project"); !ok { + // Get the VPC to retrieve its project + // Use projectid=-1 to search across all projects + vpc, count, err := cs.VPC.GetVPCByID(vpcid.(string), cloudstack.WithProject("-1")) + if err == nil && count > 0 && vpc.Projectid != "" { + log.Printf("[DEBUG] Inheriting project %s from VPC %s", vpc.Projectid, vpcid.(string)) + p.SetProjectid(vpc.Projectid) + } + } } if zone, ok := d.GetOk("zone"); ok { @@ -121,6 +143,7 @@ func resourceCloudStackIPAddressCreate(d *schema.ResourceData, meta interface{}) } // If there is a project supplied, we retrieve and set the project id + // This will override the inherited project from VPC or network if explicitly set if err := setProjectid(p, cs, d); err != nil { return err } @@ -150,6 +173,7 @@ func resourceCloudStackIPAddressRead(d *schema.ResourceData, meta interface{}) e d.Id(), cloudstack.WithProject(d.Get("project").(string)), ) + if err != nil { if count == 0 { log.Printf( diff --git a/cloudstack/resource_cloudstack_ipaddress_test.go b/cloudstack/resource_cloudstack_ipaddress_test.go index 82b8ffce..da6a8206 100644 --- a/cloudstack/resource_cloudstack_ipaddress_test.go +++ b/cloudstack/resource_cloudstack_ipaddress_test.go @@ -84,6 +84,52 @@ func TestAccCloudStackIPAddress_vpcid_with_network_id(t *testing.T) { }) } +func TestAccCloudStackIPAddress_vpcProjectInheritance(t *testing.T) { + var ipaddr cloudstack.PublicIpAddress + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackIPAddressDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackIPAddress_vpcProjectInheritance, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackIPAddressExists( + "cloudstack_ipaddress.foo", &ipaddr), + // Verify the project was inherited from the VPC + resource.TestCheckResourceAttr( + "cloudstack_ipaddress.foo", "project", "terraform"), + testAccCheckCloudStackIPAddressProjectInherited(&ipaddr), + ), + }, + }, + }) +} + +func TestAccCloudStackIPAddress_networkProjectInheritance(t *testing.T) { + var ipaddr cloudstack.PublicIpAddress + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackIPAddressDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackIPAddress_networkProjectInheritance, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackIPAddressExists( + "cloudstack_ipaddress.foo", &ipaddr), + // Verify the project was inherited from the network + resource.TestCheckResourceAttr( + "cloudstack_ipaddress.foo", "project", "terraform"), + testAccCheckCloudStackIPAddressProjectInherited(&ipaddr), + ), + }, + }, + }) +} + func testAccCheckCloudStackIPAddressExists( n string, ipaddr *cloudstack.PublicIpAddress) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -113,6 +159,18 @@ func testAccCheckCloudStackIPAddressExists( } } +func testAccCheckCloudStackIPAddressProjectInherited( + ipaddr *cloudstack.PublicIpAddress) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if ipaddr.Project != "terraform" { + return fmt.Errorf("Expected project to be 'terraform' (inherited from VPC or network), got: %s", ipaddr.Project) + } + + return nil + } +} + func testAccCheckCloudStackIPAddressDestroy(s *terraform.State) error { cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) @@ -186,3 +244,34 @@ resource "cloudstack_ipaddress" "foo" { network_id = cloudstack_network.foo.id zone = cloudstack_vpc.foo.zone }` + +const testAccCloudStackIPAddress_vpcProjectInheritance = ` +resource "cloudstack_vpc" "foo" { + name = "terraform-vpc" + cidr = "10.0.0.0/8" + vpc_offering = "Default VPC offering" + project = "terraform" + zone = "Sandbox-simulator" +} + +resource "cloudstack_ipaddress" "foo" { + vpc_id = cloudstack_vpc.foo.id + zone = cloudstack_vpc.foo.zone + # Note: project is NOT specified here - it should be inherited from the VPC +}` + +const testAccCloudStackIPAddress_networkProjectInheritance = ` +resource "cloudstack_network" "foo" { + name = "terraform-network" + display_text = "terraform-network" + cidr = "10.1.1.0/24" + network_offering = "DefaultIsolatedNetworkOfferingWithSourceNatService" + project = "terraform" + source_nat_ip = true + zone = "Sandbox-simulator" +} + +resource "cloudstack_ipaddress" "foo" { + network_id = cloudstack_network.foo.id + # Note: project is NOT specified here - it should be inherited from the network +}` diff --git a/cloudstack/resource_cloudstack_network.go b/cloudstack/resource_cloudstack_network.go index e7329f82..a9a2f3b1 100644 --- a/cloudstack/resource_cloudstack_network.go +++ b/cloudstack/resource_cloudstack_network.go @@ -228,9 +228,24 @@ func resourceCloudStackNetworkCreate(d *schema.ResourceData, meta interface{}) e // Set the acl ID p.SetAclid(aclid.(string)) } + + // If no project is explicitly set, try to inherit it from the VPC + if _, ok := d.GetOk("project"); !ok { + // Get the VPC to retrieve its project + // Use listall to search across all projects + vpcParams := cs.VPC.NewListVPCsParams() + vpcParams.SetId(vpcid.(string)) + vpcParams.SetListall(true) + vpcList, err := cs.VPC.ListVPCs(vpcParams) + if err == nil && vpcList.Count > 0 && vpcList.VPCs[0].Projectid != "" { + log.Printf("[DEBUG] Inheriting project %s from VPC %s", vpcList.VPCs[0].Projectid, vpcid.(string)) + p.SetProjectid(vpcList.VPCs[0].Projectid) + } + } } // If there is a project supplied, we retrieve and set the project id + // This will override the inherited project from VPC if explicitly set if err := setProjectid(p, cs, d); err != nil { return err } @@ -283,11 +298,23 @@ func resourceCloudStackNetworkCreate(d *schema.ResourceData, meta interface{}) e func resourceCloudStackNetworkRead(d *schema.ResourceData, meta interface{}) error { cs := meta.(*cloudstack.CloudStackClient) - // Get the virtual machine details + // Get the network details + // First try with the project from state (if any) + project := d.Get("project").(string) n, count, err := cs.Network.GetNetworkByID( d.Id(), - cloudstack.WithProject(d.Get("project").(string)), + cloudstack.WithProject(project), ) + + // If not found and no explicit project was set, try with projectid=-1 + // This handles the case where the project was inherited from VPC + if count == 0 && project == "" { + n, count, err = cs.Network.GetNetworkByID( + d.Id(), + cloudstack.WithProject("-1"), + ) + } + if err != nil { if count == 0 { log.Printf( diff --git a/cloudstack/resource_cloudstack_network_acl.go b/cloudstack/resource_cloudstack_network_acl.go index a895d332..62104e89 100644 --- a/cloudstack/resource_cloudstack_network_acl.go +++ b/cloudstack/resource_cloudstack_network_acl.go @@ -54,6 +54,7 @@ func resourceCloudStackNetworkACL() *schema.Resource { "project": { Type: schema.TypeString, Optional: true, + Computed: true, ForceNew: true, }, @@ -70,9 +71,25 @@ func resourceCloudStackNetworkACLCreate(d *schema.ResourceData, meta interface{} cs := meta.(*cloudstack.CloudStackClient) name := d.Get("name").(string) + vpcID := d.Get("vpc_id").(string) + + // If no project is explicitly set, try to inherit it from the VPC + // and set it in the state so the Read function can use it + if _, ok := d.GetOk("project"); !ok { + // Get the VPC to retrieve its project + // Use projectid=-1 to search across all projects + vpc, count, err := cs.VPC.GetVPCByID(vpcID, cloudstack.WithProject("-1")) + if err == nil && count > 0 && vpc.Projectid != "" { + log.Printf("[DEBUG] Inheriting project %s from VPC %s", vpc.Projectid, vpcID) + // Set the project in the resource data for state management + d.Set("project", vpc.Project) + } + } // Create a new parameter struct - p := cs.NetworkACL.NewCreateNetworkACLListParams(name, d.Get("vpc_id").(string)) + // Note: CreateNetworkACLListParams doesn't support SetProjectid + // The ACL will be created in the same project as the VPC automatically + p := cs.NetworkACL.NewCreateNetworkACLListParams(name, vpcID) // Set the description if description, ok := d.GetOk("description"); ok { @@ -100,6 +117,7 @@ func resourceCloudStackNetworkACLRead(d *schema.ResourceData, meta interface{}) d.Id(), cloudstack.WithProject(d.Get("project").(string)), ) + if err != nil { if count == 0 { log.Printf( @@ -115,6 +133,16 @@ func resourceCloudStackNetworkACLRead(d *schema.ResourceData, meta interface{}) d.Set("description", f.Description) d.Set("vpc_id", f.Vpcid) + // If project is not already set in state, try to get it from the VPC + if d.Get("project").(string) == "" { + // Get the VPC to retrieve its project + vpc, vpcCount, vpcErr := cs.VPC.GetVPCByID(f.Vpcid, cloudstack.WithProject("-1")) + if vpcErr == nil && vpcCount > 0 && vpc.Project != "" { + log.Printf("[DEBUG] Setting project %s from VPC %s for ACL %s", vpc.Project, f.Vpcid, f.Name) + setValueOrID(d, "project", vpc.Project, vpc.Projectid) + } + } + return nil } diff --git a/cloudstack/resource_cloudstack_network_acl_test.go b/cloudstack/resource_cloudstack_network_acl_test.go index b252a5a5..181b2d50 100644 --- a/cloudstack/resource_cloudstack_network_acl_test.go +++ b/cloudstack/resource_cloudstack_network_acl_test.go @@ -66,6 +66,29 @@ func TestAccCloudStackNetworkACL_import(t *testing.T) { }) } +func TestAccCloudStackNetworkACL_vpcProjectInheritance(t *testing.T) { + var acl cloudstack.NetworkACLList + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackNetworkACLDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackNetworkACL_vpcProjectInheritance, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackNetworkACLExists( + "cloudstack_network_acl.foo", &acl), + // Verify the project was inherited from the VPC + resource.TestCheckResourceAttr( + "cloudstack_network_acl.foo", "project", "terraform"), + testAccCheckCloudStackNetworkACLProjectInherited(&acl), + ), + }, + }, + }) +} + func testAccCheckCloudStackNetworkACLExists( n string, acl *cloudstack.NetworkACLList) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -110,6 +133,19 @@ func testAccCheckCloudStackNetworkACLBasicAttributes( } } +func testAccCheckCloudStackNetworkACLProjectInherited( + acl *cloudstack.NetworkACLList) resource.TestCheckFunc { + return func(s *terraform.State) error { + // The ACL itself doesn't have project info, but we verify it was created + // successfully which means it inherited the project from the VPC + if acl.Name != "terraform-acl" { + return fmt.Errorf("Expected ACL name to be 'terraform-acl', got: %s", acl.Name) + } + + return nil + } +} + func testAccCheckCloudStackNetworkACLDestroy(s *terraform.State) error { cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) @@ -144,3 +180,19 @@ resource "cloudstack_network_acl" "foo" { description = "terraform-acl-text" vpc_id = cloudstack_vpc.foo.id }` + +const testAccCloudStackNetworkACL_vpcProjectInheritance = ` +resource "cloudstack_vpc" "foo" { + name = "terraform-vpc" + cidr = "10.0.0.0/8" + vpc_offering = "Default VPC offering" + project = "terraform" + zone = "Sandbox-simulator" +} + +resource "cloudstack_network_acl" "foo" { + name = "terraform-acl" + description = "terraform-acl-text" + vpc_id = cloudstack_vpc.foo.id + # Note: project is NOT specified here - it should be inherited from the VPC +}` diff --git a/cloudstack/resource_cloudstack_network_test.go b/cloudstack/resource_cloudstack_network_test.go index 0b650ace..09488116 100644 --- a/cloudstack/resource_cloudstack_network_test.go +++ b/cloudstack/resource_cloudstack_network_test.go @@ -90,6 +90,30 @@ func TestAccCloudStackNetwork_vpc(t *testing.T) { }) } +func TestAccCloudStackNetwork_vpcProjectInheritance(t *testing.T) { + var network cloudstack.Network + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackNetworkDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackNetwork_vpcProjectInheritance, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackNetworkExists( + "cloudstack_network.foo", &network), + testAccCheckCloudStackNetworkVPCAttributes(&network), + // Verify the project was inherited from the VPC + resource.TestCheckResourceAttr( + "cloudstack_network.foo", "project", "terraform"), + testAccCheckCloudStackNetworkProjectInherited(&network), + ), + }, + }, + }) +} + func TestAccCloudStackNetwork_updateACL(t *testing.T) { var network cloudstack.Network @@ -244,6 +268,18 @@ func testAccCheckCloudStackNetworkVPCAttributes( } } +func testAccCheckCloudStackNetworkProjectInherited( + network *cloudstack.Network) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if network.Project != "terraform" { + return fmt.Errorf("Expected project to be 'terraform' (inherited from VPC), got: %s", network.Project) + } + + return nil + } +} + func testAccCheckCloudStackNetworkDestroy(s *terraform.State) error { cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) @@ -377,3 +413,22 @@ resource "cloudstack_network" "foo" { acl_id = cloudstack_network_acl.bar.id zone = cloudstack_vpc.foo.zone }` + +const testAccCloudStackNetwork_vpcProjectInheritance = ` +resource "cloudstack_vpc" "foo" { + name = "terraform-vpc" + cidr = "10.0.0.0/8" + vpc_offering = "Default VPC offering" + project = "terraform" + zone = "Sandbox-simulator" +} + +resource "cloudstack_network" "foo" { + name = "terraform-network" + display_text = "terraform-network" + cidr = "10.1.1.0/24" + network_offering = "DefaultIsolatedNetworkOfferingForVpcNetworks" + vpc_id = cloudstack_vpc.foo.id + zone = cloudstack_vpc.foo.zone + # Note: project is NOT specified here - it should be inherited from the VPC +}` diff --git a/cloudstack/resource_cloudstack_port_forward.go b/cloudstack/resource_cloudstack_port_forward.go index 2b006738..66e79d27 100644 --- a/cloudstack/resource_cloudstack_port_forward.go +++ b/cloudstack/resource_cloudstack_port_forward.go @@ -55,6 +55,7 @@ func resourceCloudStackPortForward() *schema.Resource { "project": { Type: schema.TypeString, Optional: true, + Computed: true, ForceNew: true, }, @@ -112,9 +113,23 @@ func resourceCloudStackPortForward() *schema.Resource { } func resourceCloudStackPortForwardCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + // We need to set this upfront in order to be able to save a partial state d.SetId(d.Get("ip_address_id").(string)) + // If no project is explicitly set, try to inherit it from the IP address + if _, ok := d.GetOk("project"); !ok { + // Get the IP address to retrieve its project + // Use projectid=-1 to search across all projects + ip, count, err := cs.Address.GetPublicIpAddressByID(d.Id(), cloudstack.WithProject("-1")) + if err == nil && count > 0 && ip.Projectid != "" { + log.Printf("[DEBUG] Inheriting project %s from IP address %s", ip.Projectid, d.Id()) + // Set the project in the resource data for state management + d.Set("project", ip.Project) + } + } + // Create all forwards that are configured if nrs := d.Get("forward").(*schema.Set); nrs.Len() > 0 { // Create an empty schema.Set to hold all forwards @@ -176,9 +191,9 @@ func createPortForward(d *schema.ResourceData, meta interface{}, forward map[str return err } + // Query VM without project filter - it will be found regardless of project vm, _, err := cs.VirtualMachine.GetVirtualMachineByID( forward["virtual_machine_id"].(string), - cloudstack.WithProject(d.Get("project").(string)), ) if err != nil { return err @@ -234,10 +249,11 @@ func resourceCloudStackPortForwardRead(d *schema.ResourceData, meta interface{}) cs := meta.(*cloudstack.CloudStackClient) // First check if the IP address is still associated - _, count, err := cs.Address.GetPublicIpAddressByID( + ip, count, err := cs.Address.GetPublicIpAddressByID( d.Id(), cloudstack.WithProject(d.Get("project").(string)), ) + if err != nil { if count == 0 { log.Printf( @@ -249,13 +265,18 @@ func resourceCloudStackPortForwardRead(d *schema.ResourceData, meta interface{}) return err } + // Set the project if the IP address belongs to one + setValueOrID(d, "project", ip.Project, ip.Projectid) + // Get all the forwards from the running environment p := cs.Firewall.NewListPortForwardingRulesParams() p.SetIpaddressid(d.Id()) p.SetListall(true) - if err := setProjectid(p, cs, d); err != nil { - return err + // Use the project from the IP address if it belongs to one + // If no project, don't set projectid (use default scope) + if ip.Projectid != "" { + p.SetProjectid(ip.Projectid) } l, err := cs.Firewall.ListPortForwardingRules(p) @@ -279,13 +300,17 @@ func resourceCloudStackPortForwardRead(d *schema.ResourceData, meta interface{}) id, ok := forward["uuid"] if !ok || id.(string) == "" { + // Forward doesn't have a UUID yet (shouldn't happen after Create, but handle gracefully) + log.Printf("[DEBUG] Skipping forward without UUID: %+v", forward) continue } // Get the forward f, ok := forwardMap[id.(string)] if !ok { - forward["uuid"] = "" + // Forward not found in API response - the rule was deleted outside of Terraform + log.Printf("[WARN] Port forwarding rule %s not found in API response, removing from state", id.(string)) + // Don't add this forward to the new set - it will be removed from state continue } @@ -352,9 +377,11 @@ func resourceCloudStackPortForwardRead(d *schema.ResourceData, meta interface{}) } } - if forwards.Len() > 0 { - d.Set("forward", forwards) - } else if !managed { + // Always set the forward attribute to maintain consistent state + d.Set("forward", forwards) + + // Only remove the resource from state if it's not managed and has no forwards + if forwards.Len() == 0 && !managed { d.SetId("") } diff --git a/cloudstack/resource_cloudstack_port_forward_test.go b/cloudstack/resource_cloudstack_port_forward_test.go index bb15f289..c653ad13 100644 --- a/cloudstack/resource_cloudstack_port_forward_test.go +++ b/cloudstack/resource_cloudstack_port_forward_test.go @@ -119,6 +119,27 @@ func TestAccCloudStackPortForward_portRange(t *testing.T) { }) } +func TestAccCloudStackPortForward_projectInheritance(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackPortForwardDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackPortForward_projectInheritance, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackPortForwardsExist("cloudstack_port_forward.foo"), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "forward.#", "1"), + // Verify the project was inherited from the IP address + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "project", "terraform"), + ), + }, + }, + }) +} + func testAccCheckCloudStackPortForwardsExist(n string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -369,3 +390,49 @@ resource "cloudstack_port_forward" "foo" { virtual_machine_id = cloudstack_instance.foobar.id } }` + +const testAccCloudStackPortForward_projectInheritance = ` +resource "cloudstack_vpc" "foo" { + name = "terraform-vpc" + cidr = "10.0.0.0/8" + vpc_offering = "Default VPC offering" + project = "terraform" + zone = "Sandbox-simulator" +} + +resource "cloudstack_network" "foo" { + name = "terraform-network" + display_text = "terraform-network" + cidr = "10.1.1.0/24" + network_offering = "DefaultIsolatedNetworkOfferingForVpcNetworks" + vpc_id = cloudstack_vpc.foo.id + zone = "Sandbox-simulator" +} + +resource "cloudstack_ipaddress" "foo" { + vpc_id = cloudstack_vpc.foo.id + zone = "Sandbox-simulator" + # project is automatically inherited from VPC +} + +resource "cloudstack_instance" "foobar" { + name = "terraform-test" + display_name = "terraform" + service_offering = "Medium Instance" + network_id = cloudstack_network.foo.id + template = "CentOS 5.6 (64-bit) no GUI (Simulator)" + zone = "Sandbox-simulator" + expunge = true +} + +resource "cloudstack_port_forward" "foo" { + ip_address_id = cloudstack_ipaddress.foo.id + # Note: project is NOT specified here - it should be inherited from the IP address + + forward { + protocol = "tcp" + private_port = 443 + public_port = 8443 + virtual_machine_id = cloudstack_instance.foobar.id + } +}` diff --git a/website/docs/r/instance.html.markdown b/website/docs/r/instance.html.markdown index ebf3f20f..5c795e98 100644 --- a/website/docs/r/instance.html.markdown +++ b/website/docs/r/instance.html.markdown @@ -105,6 +105,29 @@ resource "cloudstack_instance" "from_template" { } ``` +### Instance with Automatic Project Inheritance + +```hcl +# Create a network in a project +resource "cloudstack_network" "project_network" { + name = "project-network" + cidr = "10.1.1.0/24" + network_offering = "DefaultIsolatedNetworkOfferingWithSourceNatService" + project = "my-project" + zone = "zone-1" +} + +# Instance automatically inherits project from network +resource "cloudstack_instance" "app" { + name = "app-server" + service_offering = "small" + network_id = cloudstack_network.project_network.id + template = "CentOS 7" + zone = cloudstack_network.project_network.zone + # project is automatically inherited from the network +} +``` + ## Argument Reference The following arguments are supported: @@ -164,7 +187,9 @@ The following arguments are supported: this instance. Changing this forces a new resource to be created. * `project` - (Optional) The name or ID of the project to deploy this - instance to. Changing this forces a new resource to be created. + instance to. Changing this forces a new resource to be created. If not + specified and `network_id` is provided, the project will be automatically + inherited from the network. * `zone` - (Required) The name or ID of the zone where this instance will be created. Changing this forces a new resource to be created. diff --git a/website/docs/r/ipaddress.html.markdown b/website/docs/r/ipaddress.html.markdown index 27bcbd20..0ba3cddc 100644 --- a/website/docs/r/ipaddress.html.markdown +++ b/website/docs/r/ipaddress.html.markdown @@ -12,12 +12,63 @@ Acquires and associates a public IP. ## Example Usage +### Basic IP Address for Network + ```hcl resource "cloudstack_ipaddress" "default" { network_id = "6eb22f91-7454-4107-89f4-36afcdf33021" } ``` +### IP Address for VPC + +```hcl +resource "cloudstack_vpc" "foo" { + name = "my-vpc" + cidr = "10.0.0.0/16" + vpc_offering = "Default VPC offering" + zone = "zone-1" +} + +resource "cloudstack_ipaddress" "vpc_ip" { + vpc_id = cloudstack_vpc.foo.id +} +``` + +### IP Address with Automatic Project Inheritance + +```hcl +# Create a VPC in a project +resource "cloudstack_vpc" "project_vpc" { + name = "project-vpc" + cidr = "10.0.0.0/16" + vpc_offering = "Default VPC offering" + project = "my-project" + zone = "zone-1" +} + +# IP address automatically inherits project from VPC +resource "cloudstack_ipaddress" "vpc_ip" { + vpc_id = cloudstack_vpc.project_vpc.id + # project is automatically inherited from the VPC +} + +# Or with a network +resource "cloudstack_network" "project_network" { + name = "project-network" + cidr = "10.1.1.0/24" + network_offering = "DefaultIsolatedNetworkOfferingWithSourceNatService" + project = "my-project" + zone = "zone-1" +} + +# IP address automatically inherits project from network +resource "cloudstack_ipaddress" "network_ip" { + network_id = cloudstack_network.project_network.id + # project is automatically inherited from the network +} +``` + ## Argument Reference The following arguments are supported: @@ -35,7 +86,10 @@ The following arguments are supported: acquired and associated. Changing this forces a new resource to be created. * `project` - (Optional) The name or ID of the project to deploy this - instance to. Changing this forces a new resource to be created. + instance to. Changing this forces a new resource to be created. If not + specified and `vpc_id` is provided, the project will be automatically + inherited from the VPC. If not specified and `network_id` is provided, + the project will be automatically inherited from the network. *NOTE: `network_id` and/or `zone` should have a value when `is_portable` is `false`!* *NOTE: Either `network_id` or `vpc_id` should have a value when `is_portable` is `true`!* diff --git a/website/docs/r/network.html.markdown b/website/docs/r/network.html.markdown index d83ebb1a..45075d5f 100644 --- a/website/docs/r/network.html.markdown +++ b/website/docs/r/network.html.markdown @@ -23,6 +23,27 @@ resource "cloudstack_network" "default" { } ``` +VPC network with automatic project inheritance: + +```hcl +resource "cloudstack_vpc" "default" { + name = "test-vpc" + cidr = "10.0.0.0/8" + vpc_offering = "Default VPC Offering" + zone = "zone-1" + project = "my-project" +} + +resource "cloudstack_network" "vpc_network" { + name = "test-vpc-network" + cidr = "10.1.0.0/16" + network_offering = "DefaultIsolatedNetworkOfferingForVpcNetworks" + vpc_id = cloudstack_vpc.default.id + zone = cloudstack_vpc.default.zone + # project is automatically inherited from the VPC +} +``` + ## Argument Reference The following arguments are supported: @@ -61,7 +82,9 @@ The following arguments are supported: `none`, this will force a new resource to be created. (defaults `none`) * `project` - (Optional) The name or ID of the project to deploy this - instance to. Changing this forces a new resource to be created. + instance to. Changing this forces a new resource to be created. If not + specified and `vpc_id` is provided, the project will be automatically + inherited from the VPC. * `source_nat_ip` - (Optional) If set to `true` a public IP will be associated with the network. This is mainly used when the network supports the source diff --git a/website/docs/r/network_acl.html.markdown b/website/docs/r/network_acl.html.markdown index 37f2928c..78a5bd68 100644 --- a/website/docs/r/network_acl.html.markdown +++ b/website/docs/r/network_acl.html.markdown @@ -12,6 +12,8 @@ Creates a Network ACL for the given VPC. ## Example Usage +### Basic Network ACL + ```hcl resource "cloudstack_network_acl" "default" { name = "test-acl" @@ -19,6 +21,27 @@ resource "cloudstack_network_acl" "default" { } ``` +### Network ACL with Automatic Project Inheritance + +```hcl +# Create a VPC in a project +resource "cloudstack_vpc" "project_vpc" { + name = "project-vpc" + cidr = "10.0.0.0/16" + vpc_offering = "Default VPC offering" + project = "my-project" + zone = "zone-1" +} + +# Network ACL automatically inherits project from VPC +resource "cloudstack_network_acl" "project_acl" { + name = "project-acl" + description = "ACL for project VPC" + vpc_id = cloudstack_vpc.project_vpc.id + # project is automatically inherited from the VPC +} +``` + ## Argument Reference The following arguments are supported: @@ -30,7 +53,8 @@ The following arguments are supported: new resource to be created. * `project` - (Optional) The name or ID of the project to deploy this - instance to. Changing this forces a new resource to be created. + instance to. Changing this forces a new resource to be created. If not + specified, the project will be automatically inherited from the VPC. * `vpc_id` - (Required) The ID of the VPC to create this ACL for. Changing this forces a new resource to be created. diff --git a/website/docs/r/port_forward.html.markdown b/website/docs/r/port_forward.html.markdown index 23d1eb30..bd444ef1 100644 --- a/website/docs/r/port_forward.html.markdown +++ b/website/docs/r/port_forward.html.markdown @@ -12,6 +12,8 @@ Creates port forwards. ## Example Usage +### Basic Port Forward + ```hcl resource "cloudstack_port_forward" "default" { ip_address_id = "30b21801-d4b3-4174-852b-0c0f30bdbbfb" @@ -25,6 +27,38 @@ resource "cloudstack_port_forward" "default" { } ``` +### Port Forward with Automatic Project Inheritance + +```hcl +# Create a VPC in a project +resource "cloudstack_vpc" "project_vpc" { + name = "project-vpc" + cidr = "10.0.0.0/16" + vpc_offering = "Default VPC offering" + project = "my-project" + zone = "zone-1" +} + +# IP address automatically inherits project from VPC +resource "cloudstack_ipaddress" "project_ip" { + vpc_id = cloudstack_vpc.project_vpc.id + zone = "zone-1" +} + +# Port forward automatically inherits project from IP address +resource "cloudstack_port_forward" "project_forward" { + ip_address_id = cloudstack_ipaddress.project_ip.id + # project is automatically inherited from the IP address + + forward { + protocol = "tcp" + private_port = 80 + public_port = 8080 + virtual_machine_id = cloudstack_instance.web.id + } +} +``` + ## Argument Reference The following arguments are supported: @@ -36,8 +70,9 @@ The following arguments are supported: this IP address will be managed by this resource. This means it will delete all port forwards that are not in your config! (defaults false) -* `project` - (Optional) The name or ID of the project to create this port forward - in. Changing this forces a new resource to be created. +* `project` - (Optional) The name or ID of the project to deploy this + resource to. Changing this forces a new resource to be created. If not + specified, the project will be automatically inherited from the IP address. * `forward` - (Required) Can be specified multiple times. Each forward block supports fields documented below.