This is an automated email from the ASF dual-hosted git repository. Pearl1594 pushed a commit to branch vpc-offering-suppot in repository https://gitbox.apache.org/repos/asf/cloudstack-terraform-provider.git
commit 5715e4c9317bff4ca2a8ff22685ff866a4c4d389 Author: Pearl Dsilva <[email protected]> AuthorDate: Thu Jul 23 14:57:19 2026 -0400 Add support for VPC offering resource --- cloudstack/data_source_cloudstack_vpc_offering.go | 233 ++++++++++++ .../data_source_cloudstack_vpc_offering_test.go | 123 +++++++ cloudstack/provider.go | 2 + cloudstack/resource_cloudstack_vpc_offering.go | 406 +++++++++++++++++++++ .../resource_cloudstack_vpc_offering_test.go | 198 ++++++++++ website/docs/d/vpc_offering.html.markdown | 43 +++ website/docs/r/vpc_offering.html.markdown | 71 ++++ 7 files changed, 1076 insertions(+) diff --git a/cloudstack/data_source_cloudstack_vpc_offering.go b/cloudstack/data_source_cloudstack_vpc_offering.go new file mode 100644 index 0000000..82d3c8d --- /dev/null +++ b/cloudstack/data_source_cloudstack_vpc_offering.go @@ -0,0 +1,233 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "encoding/json" + "fmt" + "log" + "regexp" + "strings" + "time" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceCloudstackVPCOffering() *schema.Resource { + return &schema.Resource{ + Read: datasourceCloudStackVPCOfferingRead, + Schema: map[string]*schema.Schema{ + "filter": dataSourceFiltersSchema(), + + //Computed values + "name": { + Type: schema.TypeString, + Computed: true, + }, + "display_text": { + Type: schema.TypeString, + Computed: true, + }, + "enable": { + Type: schema.TypeBool, + Computed: true, + }, + "for_nsx": { + Type: schema.TypeBool, + Computed: true, + }, + "internet_protocol": { + Type: schema.TypeString, + Computed: true, + }, + "network_mode": { + Type: schema.TypeString, + Computed: true, + }, + "routing_mode": { + Type: schema.TypeString, + Computed: true, + }, + "specify_as_number": { + Type: schema.TypeBool, + Computed: true, + }, + "is_default": { + Type: schema.TypeBool, + Computed: true, + }, + "supported_services": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "service_provider_list": { + Type: schema.TypeMap, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "service_capability_list": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "service": { + Type: schema.TypeString, + Computed: true, + }, + "capability_type": { + Type: schema.TypeString, + Computed: true, + }, + "capability_value": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func datasourceCloudStackVPCOfferingRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + p := cs.VPC.NewListVPCOfferingsParams() + csVPCOfferings, err := cs.VPC.ListVPCOfferings(p) + + if err != nil { + return fmt.Errorf("Failed to list VPC offerings: %s", err) + } + + filters := d.Get("filter") + var vpcOfferings []*cloudstack.VPCOffering + + for _, o := range csVPCOfferings.VPCOfferings { + match, err := applyVPCOfferingFilters(o, filters.(*schema.Set)) + if err != nil { + return err + } + if match { + vpcOfferings = append(vpcOfferings, o) + } + } + + if len(vpcOfferings) == 0 { + return fmt.Errorf("No VPC offering is matching with the specified regex") + } + //return the latest VPC offering from the list of filtered VPC offerings according + //to its creation date + vpcOffering, err := latestVPCOffering(vpcOfferings) + if err != nil { + return err + } + log.Printf("[DEBUG] Selected VPC offering: %s\n", vpcOffering.Displaytext) + + fullVPCOffering, _, err := cs.VPC.GetVPCOfferingByID(vpcOffering.Id) + if err != nil { + return fmt.Errorf("Error retrieving full VPC offering details: %s", err) + } + + return vpcOfferingDescriptionAttributes(d, fullVPCOffering) +} + +func vpcOfferingDescriptionAttributes(d *schema.ResourceData, vpcOffering *cloudstack.VPCOffering) error { + d.SetId(vpcOffering.Id) + d.Set("name", vpcOffering.Name) + d.Set("display_text", vpcOffering.Displaytext) + d.Set("enable", vpcOffering.State == "Enabled") + d.Set("for_nsx", vpcOffering.Fornsx) + d.Set("internet_protocol", vpcOffering.Internetprotocol) + d.Set("network_mode", vpcOffering.Networkmode) + d.Set("routing_mode", vpcOffering.Routingmode) + d.Set("specify_as_number", vpcOffering.Specifyasnumber) + d.Set("is_default", vpcOffering.Isdefault) + + if len(vpcOffering.Service) > 0 { + services := make([]string, len(vpcOffering.Service)) + serviceProviders := make(map[string]string) + var serviceCapabilities []map[string]interface{} + for i, service := range vpcOffering.Service { + services[i] = service.Name + if len(service.Provider) > 0 { + serviceProviders[service.Name] = service.Provider[0].Name + } + for _, capability := range service.Capability { + serviceCapabilities = append(serviceCapabilities, map[string]interface{}{ + "service": service.Name, + "capability_type": capability.Name, + "capability_value": capability.Value, + }) + } + } + d.Set("supported_services", services) + d.Set("service_provider_list", serviceProviders) + d.Set("service_capability_list", serviceCapabilities) + } + + return nil +} + +func latestVPCOffering(vpcOfferings []*cloudstack.VPCOffering) (*cloudstack.VPCOffering, error) { + var latest time.Time + var vpcOffering *cloudstack.VPCOffering + + for _, o := range vpcOfferings { + created, err := time.Parse("2006-01-02T15:04:05-0700", o.Created) + if err != nil { + return nil, fmt.Errorf("Failed to parse creation date of a VPC offering: %s", err) + } + + if created.After(latest) { + latest = created + vpcOffering = o + } + } + + return vpcOffering, nil +} + +func applyVPCOfferingFilters(vpcOffering *cloudstack.VPCOffering, filters *schema.Set) (bool, error) { + var vpcOfferingJSON map[string]interface{} + k, _ := json.Marshal(vpcOffering) + err := json.Unmarshal(k, &vpcOfferingJSON) + if err != nil { + return false, err + } + + for _, f := range filters.List() { + m := f.(map[string]interface{}) + r, err := regexp.Compile(m["value"].(string)) + if err != nil { + return false, fmt.Errorf("Invalid regex: %s", err) + } + updatedName := strings.ReplaceAll(m["name"].(string), "_", "") + vpcOfferingField, ok := vpcOfferingJSON[updatedName].(string) + if !ok { + continue + } + if !r.MatchString(vpcOfferingField) { + return false, nil + } + + } + return true, nil +} diff --git a/cloudstack/data_source_cloudstack_vpc_offering_test.go b/cloudstack/data_source_cloudstack_vpc_offering_test.go new file mode 100644 index 0000000..c02aa43 --- /dev/null +++ b/cloudstack/data_source_cloudstack_vpc_offering_test.go @@ -0,0 +1,123 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccVPCOfferingDataSource_basic(t *testing.T) { + resourceName := "cloudstack_vpc_offering.vpc-off-resource" + datasourceName := "data.cloudstack_vpc_offering.vpc-off-data-source" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testVPCOfferingDataSourceConfig_basic, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(datasourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(datasourceName, "display_text", resourceName, "display_text"), + resource.TestCheckResourceAttrPair(datasourceName, "enable", resourceName, "enable"), + ), + }, + }, + }) +} + +func TestAccVPCOfferingDataSource_withServices(t *testing.T) { + resourceName := "cloudstack_vpc_offering.vpc-off-resource" + datasourceName := "data.cloudstack_vpc_offering.vpc-off-data-source" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testVPCOfferingDataSourceConfig_withServices, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(datasourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(datasourceName, "supported_services.#", resourceName, "supported_services.#"), + resource.TestCheckResourceAttrPair(datasourceName, "service_provider_list.%", resourceName, "service_provider_list.%"), + resource.TestCheckResourceAttrPair(datasourceName, "service_provider_list.Dhcp", resourceName, "service_provider_list.Dhcp"), + resource.TestCheckResourceAttrPair(datasourceName, "enable", resourceName, "enable"), + ), + }, + }, + }) +} + +const testVPCOfferingDataSourceConfig_basic = ` +resource "cloudstack_vpc_offering" "vpc-off-resource"{ + name = "TestVPCOfferingDisplay01" + display_text = "TestVPCOfferingDisplay01" + enable = true + // CloudStack always injects SourceNat and NetworkACL support into a VPC + // offering even if omitted here, so they must be declared explicitly to + // avoid permanent post-apply drift on these ForceNew attributes. + supported_services = ["SourceNat", "NetworkACL"] + service_provider_list = { + SourceNat = "VpcVirtualRouter" + NetworkACL = "VpcVirtualRouter" + } +} + +data "cloudstack_vpc_offering" "vpc-off-data-source"{ + filter{ + name = "name" + value = "TestVPCOfferingDisplay01" + } + depends_on = [ + cloudstack_vpc_offering.vpc-off-resource + ] +} +` + +const testVPCOfferingDataSourceConfig_withServices = ` +resource "cloudstack_vpc_offering" "vpc-off-resource"{ + name = "TestVPCOfferingServices01" + display_text = "TestVPCOfferingServices01" + enable = true + supported_services = ["Dhcp", "Dns", "SourceNat", "PortForwarding", "Lb", "UserData", "StaticNat", "NetworkACL"] + service_provider_list = { + Dhcp = "VpcVirtualRouter" + Dns = "VpcVirtualRouter" + SourceNat = "VpcVirtualRouter" + PortForwarding = "VpcVirtualRouter" + Lb = "VpcVirtualRouter" + UserData = "VpcVirtualRouter" + StaticNat = "VpcVirtualRouter" + NetworkACL = "VpcVirtualRouter" + } +} + +data "cloudstack_vpc_offering" "vpc-off-data-source"{ + filter{ + name = "name" + value = "TestVPCOfferingServices01" + } + depends_on = [ + cloudstack_vpc_offering.vpc-off-resource + ] +} +` diff --git a/cloudstack/provider.go b/cloudstack/provider.go index 7209014..7a1a94d 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -91,6 +91,7 @@ func Provider() *schema.Provider { "cloudstack_service_offering": dataSourceCloudstackServiceOffering(), "cloudstack_volume": dataSourceCloudstackVolume(), "cloudstack_vpc": dataSourceCloudstackVPC(), + "cloudstack_vpc_offering": dataSourceCloudstackVPCOffering(), "cloudstack_ipaddress": dataSourceCloudstackIPAddress(), "cloudstack_user": dataSourceCloudstackUser(), "cloudstack_vpn_connection": dataSourceCloudstackVPNConnection(), @@ -147,6 +148,7 @@ func Provider() *schema.Provider { "cloudstack_template": resourceCloudStackTemplate(), "cloudstack_traffic_type": resourceCloudStackTrafficType(), "cloudstack_vpc": resourceCloudStackVPC(), + "cloudstack_vpc_offering": resourceCloudStackVPCOffering(), "cloudstack_vpn_connection": resourceCloudStackVPNConnection(), "cloudstack_vpn_customer_gateway": resourceCloudStackVPNCustomerGateway(), "cloudstack_vpn_gateway": resourceCloudStackVPNGateway(), diff --git a/cloudstack/resource_cloudstack_vpc_offering.go b/cloudstack/resource_cloudstack_vpc_offering.go new file mode 100644 index 0000000..2f3523d --- /dev/null +++ b/cloudstack/resource_cloudstack_vpc_offering.go @@ -0,0 +1,406 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "log" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceCloudStackVPCOffering() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackVPCOfferingCreate, + Read: resourceCloudStackVPCOfferingRead, + Update: resourceCloudStackVPCOfferingUpdate, + Delete: resourceCloudStackVPCOfferingDelete, + Importer: &schema.ResourceImporter{ + State: importStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "display_text": { + Type: schema.TypeString, + Required: true, + }, + "domain_id": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Description: "the ID of the containing domain(s), null for public offerings", + ForceNew: true, + }, + "zone_id": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Description: "the ID of the containing zone(s), null for public offerings", + ForceNew: true, + }, + "enable": { + Type: schema.TypeBool, + Optional: true, + Description: "set to true if the offering is to be enabled during creation. Default is false", + }, + "for_nsx": { + Type: schema.TypeBool, + Optional: true, + Description: "true if the VPC offering is meant to be used for NSX, false otherwise", + ForceNew: true, + }, + "nsx_support_lb": { + Type: schema.TypeBool, + Optional: true, + Description: "true if the NSX supports Lb service, false otherwise", + ForceNew: true, + }, + "internet_protocol": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The internet protocol of the VPC offering. Options are ipv4 and dualstack. Default is ipv4. dualstack will create a VPC offering that supports both IPv4 and IPv6", + ForceNew: true, + }, + "network_mode": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Indicates the mode with which the VPC will operate. Valid option: NATTED or ROUTED", + ForceNew: true, + }, + "routing_mode": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "the routing mode for the VPC offering. Supported types are: Static or Dynamic.", + ForceNew: true, + }, + "specify_as_number": { + Type: schema.TypeBool, + Optional: true, + Description: "true if the VPC offering supports choosing AS number", + ForceNew: true, + }, + "network_provider": { + Type: schema.TypeString, + Optional: true, + Description: "network provider for the VPC offering", + ForceNew: true, + }, + "service_offering_id": { + Type: schema.TypeString, + Optional: true, + Description: "the ID of the service offering for the VPC router appliance", + ForceNew: true, + }, + "supported_services": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "the list of supported services", + ForceNew: true, + }, + "service_provider_list": { + Type: schema.TypeMap, + Optional: true, + Computed: true, + Description: "provider to service mapping. If not specified, the provider for the service will be mapped to the default provider on the physical network", + ForceNew: true, + }, + "service_capability_list": { + Type: schema.TypeList, + Optional: true, + Computed: true, + ForceNew: true, + Description: "desired service capabilities as part of the VPC offering", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "service": { + Type: schema.TypeString, + Required: true, + Description: "the service the capability applies to, e.g. SourceNat", + }, + "capability_type": { + Type: schema.TypeString, + Required: true, + Description: "the capability type, e.g. RedundantRouter", + }, + "capability_value": { + Type: schema.TypeString, + Required: true, + Description: "the capability value, e.g. true", + }, + }, + }, + }, + "is_default": { + Type: schema.TypeBool, + Computed: true, + Description: "true if this is the default VPC offering", + }, + }, + } +} + +func resourceCloudStackVPCOfferingCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + name := d.Get("name").(string) + displayText := d.Get("display_text").(string) + + // Create a new parameter struct + p := cs.VPC.NewCreateVPCOfferingParams(displayText, name) + + if v, ok := d.GetOk("domain_id"); ok { + p.SetDomainid(expandStringList(v.([]interface{}))) + } + + if v, ok := d.GetOk("zone_id"); ok { + p.SetZoneid(expandStringList(v.([]interface{}))) + } + + if v, ok := d.GetOk("enable"); ok { + p.SetEnable(v.(bool)) + } + + if v, ok := d.GetOk("for_nsx"); ok { + p.SetFornsx(v.(bool)) + } + + if v, ok := d.GetOk("nsx_support_lb"); ok { + p.SetNsxsupportlb(v.(bool)) + } + + if v, ok := d.GetOk("internet_protocol"); ok { + p.SetInternetprotocol(v.(string)) + } + + if v, ok := d.GetOk("network_mode"); ok { + p.SetNetworkmode(v.(string)) + } + + if v, ok := d.GetOk("routing_mode"); ok { + p.SetRoutingmode(v.(string)) + } + + if v, ok := d.GetOk("specify_as_number"); ok { + p.SetSpecifyasnumber(v.(bool)) + } + + if v, ok := d.GetOk("network_provider"); ok { + p.SetProvider(v.(string)) + } + + if v, ok := d.GetOk("service_offering_id"); ok { + serviceOfferingID, e := retrieveID(cs, "service_offering", v.(string)) + if e != nil { + return e.Error() + } + p.SetServiceofferingid(serviceOfferingID) + } + + if v, ok := d.GetOk("supported_services"); ok { + var supportedServices []string + for _, service := range v.(*schema.Set).List() { + supportedServices = append(supportedServices, service.(string)) + } + p.SetSupportedservices(supportedServices) + } + + if v, ok := d.GetOk("service_provider_list"); ok { + m := make(map[string]string) + for key, value := range v.(map[string]interface{}) { + m[key] = value.(string) + } + p.SetServiceproviderlist(m) + } + + if v, ok := d.GetOk("service_capability_list"); ok { + var capabilities []map[string]string + for _, item := range v.([]interface{}) { + c := item.(map[string]interface{}) + capabilities = append(capabilities, map[string]string{ + "service": c["service"].(string), + "capabilitytype": c["capability_type"].(string), + "capabilityvalue": c["capability_value"].(string), + }) + } + p.SetServicecapabilitylist(capabilities) + } + + log.Printf("[DEBUG] Creating VPC Offering %s", name) + o, err := cs.VPC.CreateVPCOffering(p) + if err != nil { + return fmt.Errorf("Error creating VPC Offering %s: %s", name, err) + } + + d.SetId(o.Id) + + log.Printf("[DEBUG] VPC Offering %s successfully created", name) + return resourceCloudStackVPCOfferingRead(d, meta) +} + +func resourceCloudStackVPCOfferingRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + log.Printf("[DEBUG] Retrieving VPC Offering %s", d.Id()) + + // Get the VPC Offering details + o, count, err := cs.VPC.GetVPCOfferingByID(d.Id()) + if err != nil { + if count == 0 { + log.Printf("[DEBUG] VPC Offering %s does no longer exist", d.Id()) + d.SetId("") + return nil + } + + return err + } + + d.SetId(o.Id) + d.Set("name", o.Name) + d.Set("display_text", o.Displaytext) + d.Set("enable", o.State == "Enabled") + d.Set("is_default", o.Isdefault) + + d.Set("for_nsx", o.Fornsx) + d.Set("specify_as_number", o.Specifyasnumber) + + if o.Internetprotocol != "" { + d.Set("internet_protocol", o.Internetprotocol) + } + + if o.Networkmode != "" { + d.Set("network_mode", o.Networkmode) + } + + if o.Routingmode != "" { + d.Set("routing_mode", o.Routingmode) + } + + if o.Domainid != "" { + d.Set("domain_id", []string{o.Domainid}) + } + + if o.Zoneid != "" { + d.Set("zone_id", []string{o.Zoneid}) + } + + if len(o.Service) > 0 { + services := make([]string, len(o.Service)) + serviceProviders := make(map[string]string) + var capabilities []map[string]interface{} + for i, service := range o.Service { + services[i] = service.Name + if len(service.Provider) > 0 { + serviceProviders[service.Name] = service.Provider[0].Name + } + for _, capability := range service.Capability { + capabilities = append(capabilities, map[string]interface{}{ + "service": service.Name, + "capability_type": capability.Name, + "capability_value": capability.Value, + }) + } + } + d.Set("supported_services", services) + d.Set("service_provider_list", serviceProviders) + if len(capabilities) > 0 { + d.Set("service_capability_list", capabilities) + } + } + + return nil +} + +func resourceCloudStackVPCOfferingUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + name := d.Get("name").(string) + + // Check if the name is changed and if so, update the VPC offering + if d.HasChange("name") { + log.Printf("[DEBUG] Name changed for %s, starting update", name) + + p := cs.VPC.NewUpdateVPCOfferingParams(d.Id()) + p.SetName(name) + + _, err := cs.VPC.UpdateVPCOffering(p) + if err != nil { + return fmt.Errorf("Error updating the name for VPC offering %s: %s", name, err) + } + } + + // Check if the display text is changed and if so, update the VPC offering + if d.HasChange("display_text") { + log.Printf("[DEBUG] Display text changed for %s, starting update", name) + + p := cs.VPC.NewUpdateVPCOfferingParams(d.Id()) + p.SetDisplaytext(d.Get("display_text").(string)) + + _, err := cs.VPC.UpdateVPCOffering(p) + if err != nil { + return fmt.Errorf("Error updating the display text for VPC offering %s: %s", name, err) + } + } + + // Check if the enabled state is changed and if so, update the VPC offering + if d.HasChange("enable") { + log.Printf("[DEBUG] State changed for %s, starting update", name) + + state := "Disabled" + if d.Get("enable").(bool) { + state = "Enabled" + } + + p := cs.VPC.NewUpdateVPCOfferingParams(d.Id()) + p.SetState(state) + + _, err := cs.VPC.UpdateVPCOffering(p) + if err != nil { + return fmt.Errorf("Error updating the state for VPC offering %s: %s", name, err) + } + } + + return resourceCloudStackVPCOfferingRead(d, meta) +} + +func resourceCloudStackVPCOfferingDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.VPC.NewDeleteVPCOfferingParams(d.Id()) + _, err := cs.VPC.DeleteVPCOffering(p) + if err != nil { + return fmt.Errorf("Error deleting VPC Offering: %s", err) + } + + return nil +} + +func expandStringList(list []interface{}) []string { + result := make([]string, len(list)) + for i, v := range list { + result[i] = v.(string) + } + return result +} diff --git a/cloudstack/resource_cloudstack_vpc_offering_test.go b/cloudstack/resource_cloudstack_vpc_offering_test.go new file mode 100644 index 0000000..f0b468f --- /dev/null +++ b/cloudstack/resource_cloudstack_vpc_offering_test.go @@ -0,0 +1,198 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccCloudStackVPCOffering_basic(t *testing.T) { + var vpcOffering cloudstack.VPCOffering + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackVPCOfferingDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackVPCOffering_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackVPCOfferingExists( + "cloudstack_vpc_offering.foo", &vpcOffering), + testAccCheckCloudStackVPCOfferingAttributes(&vpcOffering), + resource.TestCheckResourceAttr( + "cloudstack_vpc_offering.foo", "enable", "true"), + ), + }, + }, + }) +} + +func TestAccCloudStackVPCOffering_serviceCapabilityList(t *testing.T) { + var vpcOffering cloudstack.VPCOffering + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackVPCOfferingDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackVPCOffering_serviceCapabilityList, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackVPCOfferingExists( + "cloudstack_vpc_offering.capability", &vpcOffering), + resource.TestCheckResourceAttr( + "cloudstack_vpc_offering.capability", "service_capability_list.0.service", "SourceNat"), + resource.TestCheckResourceAttr( + "cloudstack_vpc_offering.capability", "service_capability_list.0.capability_type", "RedundantRouter"), + resource.TestCheckResourceAttr( + "cloudstack_vpc_offering.capability", "service_capability_list.0.capability_value", "true"), + ), + }, + }, + }) +} + +func TestAccCloudStackVPCOffering_import(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackVPCOfferingDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackVPCOffering_basic, + }, + + { + ResourceName: "cloudstack_vpc_offering.foo", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckCloudStackVPCOfferingExists( + n string, vpcOffering *cloudstack.VPCOffering) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No VPC offering ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + o, _, err := cs.VPC.GetVPCOfferingByID(rs.Primary.ID) + + if err != nil { + return err + } + + if o.Id != rs.Primary.ID { + return fmt.Errorf("VPC offering not found") + } + + *vpcOffering = *o + + return nil + } +} + +func testAccCheckCloudStackVPCOfferingAttributes( + vpcOffering *cloudstack.VPCOffering) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if vpcOffering.Name != "terraform-vpc-offering" { + return fmt.Errorf("Bad name: %s", vpcOffering.Name) + } + + if vpcOffering.Displaytext != "terraform-vpc-offering-text" { + return fmt.Errorf("Bad display text: %s", vpcOffering.Displaytext) + } + + return nil + } +} + +func testAccCheckCloudStackVPCOfferingDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_vpc_offering" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No VPC offering ID is set") + } + + _, _, err := cs.VPC.GetVPCOfferingByID(rs.Primary.ID) + if err == nil { + return fmt.Errorf("VPC offering %s still exists", rs.Primary.ID) + } + } + + return nil +} + +const testAccCloudStackVPCOffering_basic = ` +resource "cloudstack_vpc_offering" "foo" { + name = "terraform-vpc-offering" + display_text = "terraform-vpc-offering-text" + enable = true + supported_services = ["Dhcp", "Dns", "SourceNat", "PortForwarding", "Lb", "UserData", "StaticNat", "NetworkACL"] + service_provider_list = { + Dhcp = "VpcVirtualRouter" + Dns = "VpcVirtualRouter" + SourceNat = "VpcVirtualRouter" + PortForwarding = "VpcVirtualRouter" + Lb = "VpcVirtualRouter" + UserData = "VpcVirtualRouter" + StaticNat = "VpcVirtualRouter" + NetworkACL = "VpcVirtualRouter" + } +}` + +const testAccCloudStackVPCOffering_serviceCapabilityList = ` +resource "cloudstack_vpc_offering" "capability" { + name = "terraform-vpc-offering-capability" + display_text = "terraform-vpc-offering-capability-text" + // CloudStack always injects SourceNat and NetworkACL support into a VPC + // offering even if omitted here, so they must be declared explicitly to + // avoid permanent post-apply drift on these ForceNew attributes. + supported_services = ["SourceNat", "NetworkACL"] + service_provider_list = { + SourceNat = "VpcVirtualRouter" + NetworkACL = "VpcVirtualRouter" + } + service_capability_list { + service = "SourceNat" + capability_type = "RedundantRouter" + capability_value = "true" + } +}` diff --git a/website/docs/d/vpc_offering.html.markdown b/website/docs/d/vpc_offering.html.markdown new file mode 100644 index 0000000..59dd5e3 --- /dev/null +++ b/website/docs/d/vpc_offering.html.markdown @@ -0,0 +1,43 @@ +--- +layout: "cloudstack" +page_title: "Cloudstack: cloudstack_vpc_offering" +sidebar_current: "docs-cloudstack-cloudstack_vpc_offering" +description: |- + Gets information about cloudstack VPC offering. +--- + +# cloudstack_vpc_offering + +Use this datasource to get information about a VPC offering for use in other resources. + +### Example Usage + +```hcl + data "cloudstack_vpc_offering" "vpc-off-data-source"{ + filter{ + name = "name" + value="Default VPC offering" + } + } +``` + +### Argument Reference + +* `filter` - (Required) One or more name/value pairs to filter off of. You can apply filters on any exported attributes. + +## Attributes Reference + +The following attributes are exported: + +* `name` - The name of the VPC offering. +* `display_text` - An alternate display text of the VPC offering. +* `enable` - Whether the VPC offering is enabled. +* `for_nsx` - Whether this VPC offering is meant to be used for NSX. +* `internet_protocol` - The internet protocol supported by the VPC offering. +* `network_mode` - The mode with which the VPC will operate. +* `routing_mode` - The routing mode for the VPC offering. +* `specify_as_number` - Whether the VPC offering supports choosing an AS number. +* `is_default` - `true` if this is the default VPC offering. +* `supported_services` - The list of supported services for this VPC offering. +* `service_provider_list` - The map of service providers for the supported services. +* `service_capability_list` - The map of service capabilities for this VPC offering. diff --git a/website/docs/r/vpc_offering.html.markdown b/website/docs/r/vpc_offering.html.markdown new file mode 100644 index 0000000..bf8998d --- /dev/null +++ b/website/docs/r/vpc_offering.html.markdown @@ -0,0 +1,71 @@ +--- +layout: default +page_title: "CloudStack: cloudstack_vpc_offering" +sidebar_current: "docs-cloudstack-resource-vpc_offering" +description: |- + Creates a VPC Offering +--- + +# CloudStack: cloudstack_vpc_offering + +A `cloudstack_vpc_offering` resource manages a VPC offering within CloudStack. + +## Example Usage + +```hcl +resource "cloudstack_vpc_offering" "example" { + name = "example-vpc-offering" + display_text = "Example VPC Offering" + enable = true + supported_services = ["Dhcp", "Dns", "SourceNat", "PortForwarding", "Lb", "UserData", "StaticNat", "NetworkACL"] + service_provider_list = { + Dhcp = "VpcVirtualRouter" + Dns = "VpcVirtualRouter" + SourceNat = "VpcVirtualRouter" + PortForwarding = "VpcVirtualRouter" + Lb = "VpcVirtualRouter" + UserData = "VpcVirtualRouter" + StaticNat = "VpcVirtualRouter" + NetworkACL = "VpcVirtualRouter" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the VPC offering. +* `display_text` - (Required) The display text of the VPC offering. +* `domain_id` - (Optional) The ID of the containing domain(s), null for public offerings. +* `zone_id` - (Optional) The ID of the containing zone(s), null for offerings available in all zones. +* `enable` - (Optional) Whether to enable the VPC offering. Defaults to `false`. +* `for_nsx` - (Optional) Whether this VPC offering is meant to be used for NSX. Defaults to `false`. +* `nsx_support_lb` - (Optional) Whether the NSX supports the Lb service. Defaults to `false`. +* `internet_protocol` - (Optional) The internet protocol. Possible values are "IPv4" or "dualstack". Defaults to "IPv4". +* `network_mode` - (Optional) Indicates the mode with which the VPC will operate. Possible values are "NATTED" or "ROUTED". +* `routing_mode` - (Optional) The routing mode for the VPC offering. Possible values are "Static" or "Dynamic". +* `specify_as_number` - (Optional) Whether to allow specifying an AS number. Defaults to `false`. +* `network_provider` - (Optional) The network provider for the VPC offering. +* `service_offering_id` - (Optional) The ID or name of the service offering for the VPC router appliance. +* `supported_services` - (Required) A list of supported services for this VPC offering. CloudStack always requires (and, if omitted, will silently add) `NetworkACL` support, and adds `SourceNat` support unless `network_mode` is `ROUTED`. Since this attribute forces recreation on change, omitting either of these services from your configuration will cause `terraform plan` to show a permanent diff wanting to recreate the resource - always list them explicitly. +* `service_provider_list` - (Optional) A map of service providers for the supported services. +* `service_capability_list` - (Optional) A list of desired service capabilities for this VPC offering. Each block supports: + * `service` - (Required) The service the capability applies to, e.g. `SourceNat`. + * `capability_type` - (Required) The capability type, e.g. `RedundantRouter`. + * `capability_value` - (Required) The capability value, e.g. `true`. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the VPC offering. +* `is_default` - `true` if this is the default VPC offering. + +## Import + +VPC offerings can be imported; use `<VPCOFFERINGID>` as the import ID. For example: + +```shell +$ terraform import cloudstack_vpc_offering.example <VPCOFFERINGID> +```
