diff --git a/cloudstack/data_source_cloudstack_limits.go b/cloudstack/data_source_cloudstack_limits.go new file mode 100644 index 00000000..ddff885b --- /dev/null +++ b/cloudstack/data_source_cloudstack_limits.go @@ -0,0 +1,210 @@ +// 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 ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func dataSourceCloudStackLimits() *schema.Resource { + return &schema.Resource{ + Read: dataSourceCloudStackLimitsRead, + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + "instance", "ip", "volume", "snapshot", "template", "project", "network", "vpc", + "cpu", "memory", "primarystorage", "secondarystorage", + }, false), // false disables case-insensitive matching + Description: "The type of resource to list the limits. Available types are: " + + "instance, ip, volume, snapshot, template, project, network, vpc, cpu, memory, " + + "primarystorage, secondarystorage", + }, + "account": { + Type: schema.TypeString, + Optional: true, + Description: "List resources by account. Must be used with the domainid parameter.", + }, + "domainid": { + Type: schema.TypeString, + Optional: true, + Description: "List only resources belonging to the domain specified.", + }, + "projectid": { + Type: schema.TypeString, + Optional: true, + Description: "List resource limits by project.", + }, + "limits": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "resourcetype": { + Type: schema.TypeString, + Computed: true, + }, + "resourcetypename": { + Type: schema.TypeString, + Computed: true, + }, + "account": { + Type: schema.TypeString, + Computed: true, + }, + "domain": { + Type: schema.TypeString, + Computed: true, + }, + "domainid": { + Type: schema.TypeString, + Computed: true, + }, + "max": { + Type: schema.TypeInt, + Computed: true, + }, + "project": { + Type: schema.TypeString, + Computed: true, + }, + "projectid": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceCloudStackLimitsRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.Limit.NewListResourceLimitsParams() + + // Set optional parameters + if v, ok := d.GetOk("type"); ok { + typeStr := v.(string) + if resourcetype, ok := resourceTypeMap[typeStr]; ok { + p.SetResourcetype(resourcetype) + } else { + return fmt.Errorf("invalid type value: %s", typeStr) + } + } + + if v, ok := d.GetOk("account"); ok { + p.SetAccount(v.(string)) + } + + if v, ok := d.GetOk("domainid"); ok { + p.SetDomainid(v.(string)) + } + + if v, ok := d.GetOk("projectid"); ok { + p.SetProjectid(v.(string)) + } + + // Retrieve the resource limits + l, err := cs.Limit.ListResourceLimits(p) + if err != nil { + return fmt.Errorf("Error retrieving resource limits: %s", err) + } + + // Generate a unique ID for this data source + id := generateDataSourceID(d) + d.SetId(id) + + limits := make([]map[string]interface{}, 0, len(l.ResourceLimits)) + + // Set the resource data + for _, limit := range l.ResourceLimits { + limitMap := map[string]interface{}{ + "resourcetype": limit.Resourcetype, + "resourcetypename": limit.Resourcetypename, + "max": limit.Max, + } + + if limit.Account != "" { + limitMap["account"] = limit.Account + } + + if limit.Domain != "" { + limitMap["domain"] = limit.Domain + } + + if limit.Domainid != "" { + limitMap["domainid"] = limit.Domainid + } + + if limit.Project != "" { + limitMap["project"] = limit.Project + } + + if limit.Projectid != "" { + limitMap["projectid"] = limit.Projectid + } + + limits = append(limits, limitMap) + } + + if err := d.Set("limits", limits); err != nil { + return fmt.Errorf("Error setting limits: %s", err) + } + + return nil +} + +// generateDataSourceID generates a unique ID for the data source based on its parameters +func generateDataSourceID(d *schema.ResourceData) string { + var buf bytes.Buffer + + if v, ok := d.GetOk("type"); ok { + typeStr := v.(string) + if resourcetype, ok := resourceTypeMap[typeStr]; ok { + buf.WriteString(fmt.Sprintf("%d-", resourcetype)) + } + } + + if v, ok := d.GetOk("account"); ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + + if v, ok := d.GetOk("domainid"); ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + + if v, ok := d.GetOk("projectid"); ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + + // Generate a SHA-256 hash of the buffer content + hash := sha256.Sum256(buf.Bytes()) + return fmt.Sprintf("limits-%s", hex.EncodeToString(hash[:])[:8]) +} diff --git a/cloudstack/provider.go b/cloudstack/provider.go index a71df0e5..b56f0ec4 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -90,6 +90,7 @@ func Provider() *schema.Provider { "cloudstack_user": dataSourceCloudstackUser(), "cloudstack_vpn_connection": dataSourceCloudstackVPNConnection(), "cloudstack_pod": dataSourceCloudstackPod(), + "cloudstack_limits": dataSourceCloudStackLimits(), }, ResourcesMap: map[string]*schema.Resource{ @@ -105,6 +106,7 @@ func Provider() *schema.Provider { "cloudstack_ipaddress": resourceCloudStackIPAddress(), "cloudstack_kubernetes_cluster": resourceCloudStackKubernetesCluster(), "cloudstack_kubernetes_version": resourceCloudStackKubernetesVersion(), + "cloudstack_limits": resourceCloudStackLimits(), "cloudstack_loadbalancer_rule": resourceCloudStackLoadBalancerRule(), "cloudstack_network": resourceCloudStackNetwork(), "cloudstack_network_acl": resourceCloudStackNetworkACL(), diff --git a/cloudstack/resource_cloudstack_limits.go b/cloudstack/resource_cloudstack_limits.go new file mode 100644 index 00000000..e2b576d8 --- /dev/null +++ b/cloudstack/resource_cloudstack_limits.go @@ -0,0 +1,327 @@ +// 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" + "strconv" + "strings" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +// resourceTypeMap maps string resource types to their integer values +var resourceTypeMap = map[string]int{ + "instance": 0, + "ip": 1, + "volume": 2, + "snapshot": 3, + "template": 4, + "project": 5, + "network": 6, + "vpc": 7, + "cpu": 8, + "memory": 9, + "primarystorage": 10, + "secondarystorage": 11, +} + +func resourceCloudStackLimits() *schema.Resource { + return &schema.Resource{ + Read: resourceCloudStackLimitsRead, + Update: resourceCloudStackLimitsUpdate, + Create: resourceCloudStackLimitsCreate, + Delete: resourceCloudStackLimitsDelete, + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + "instance", "ip", "volume", "snapshot", "template", "project", "network", "vpc", + "cpu", "memory", "primarystorage", "secondarystorage", + }, false), // false disables case-insensitive matching + Description: "The type of resource to update the limits. Available types are: " + + "instance, ip, volume, snapshot, template, project, network, vpc, cpu, memory, " + + "primarystorage, secondarystorage", + }, + "account": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "Update resource for a specified account. Must be used with the domainid parameter.", + }, + "domainid": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "Update resource limits for all accounts in specified domain. If used with the account parameter, updates resource limits for a specified account in specified domain.", + }, + "max": { + Type: schema.TypeInt, + Optional: true, + Description: "Maximum resource limit. Use -1 for unlimited resource limit. A value of 0 means zero resources are allowed, though the CloudStack API may return -1 for a limit set to 0.", + }, + "projectid": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "Update resource limits for project.", + }, + }, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} + +// getResourceType gets the resource type from the type field +func getResourceType(d *schema.ResourceData) (int, error) { + // Check if type is set + if v, ok := d.GetOk("type"); ok { + typeStr := v.(string) + if resourcetype, ok := resourceTypeMap[typeStr]; ok { + return resourcetype, nil + } + return 0, fmt.Errorf("invalid type value: %s", typeStr) + } + + return 0, fmt.Errorf("type must be specified") +} + +func resourceCloudStackLimitsCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + resourcetype, err := getResourceType(d) + if err != nil { + return err + } + + account := d.Get("account").(string) + domainid := d.Get("domainid").(string) + projectid := d.Get("projectid").(string) + + // Validate account and domain parameters + if account != "" && domainid == "" { + return fmt.Errorf("domainid is required when account is specified") + } + + // Create a new parameter struct + p := cs.Limit.NewUpdateResourceLimitParams(resourcetype) + if account != "" { + p.SetAccount(account) + } + if domainid != "" { + p.SetDomainid(domainid) + } + if maxVal, ok := d.GetOk("max"); ok { + maxIntVal := maxVal.(int) + log.Printf("[DEBUG] Setting max value to %d", maxIntVal) + p.SetMax(int64(maxIntVal)) + } + if projectid != "" { + p.SetProjectid(projectid) + } + + log.Printf("[DEBUG] Updating Resource Limit for type %d", resourcetype) + _, err = cs.Limit.UpdateResourceLimit(p) + + if err != nil { + return fmt.Errorf("Error creating resource limit: %s", err) + } + + // Generate a unique ID based on the parameters + id := generateResourceID(resourcetype, account, domainid, projectid) + d.SetId(id) + + return resourceCloudStackLimitsRead(d, meta) +} + +// generateResourceID creates a unique ID for the resource based on its parameters +func generateResourceID(resourcetype int, account, domainid, projectid string) string { + if projectid != "" { + return fmt.Sprintf("%d-project-%s", resourcetype, projectid) + } + + if account != "" && domainid != "" { + return fmt.Sprintf("%d-account-%s-%s", resourcetype, account, domainid) + } + + if domainid != "" { + return fmt.Sprintf("%d-domain-%s", resourcetype, domainid) + } + + return fmt.Sprintf("%d", resourcetype) +} + +func resourceCloudStackLimitsRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the resourcetype from the type field + resourcetype, err := getResourceType(d) + if err != nil { + // If there's an error getting the type, try to extract it from the ID + idParts := strings.Split(d.Id(), "-") + if len(idParts) > 0 { + if rt, err := strconv.Atoi(idParts[0]); err == nil { + resourcetype = rt + // Find the string representation for this numeric type + for typeStr, typeVal := range resourceTypeMap { + if typeVal == rt { + d.Set("type", typeStr) + break + } + } + } + } + } + + account := d.Get("account").(string) + domainid := d.Get("domainid").(string) + projectid := d.Get("projectid").(string) + + // Create a new parameter struct + p := cs.Limit.NewListResourceLimitsParams() + p.SetResourcetype(resourcetype) + if account != "" { + p.SetAccount(account) + } + if domainid != "" { + p.SetDomainid(domainid) + } + if projectid != "" { + p.SetProjectid(projectid) + } + + // Retrieve the resource limits + l, err := cs.Limit.ListResourceLimits(p) + if err != nil { + return fmt.Errorf("error retrieving resource limits: %s", err) + } + + if l.Count == 0 { + log.Printf("[DEBUG] Resource limit not found") + d.SetId("") + return nil + } + + // Update the config + for _, limit := range l.ResourceLimits { + if limit.Resourcetype == fmt.Sprintf("%d", resourcetype) { + log.Printf("[DEBUG] Retrieved max value from API: %d", limit.Max) + + // If the user set max to 0 but the API returned -1, keep it as 0 in the state + if limit.Max == -1 && d.Get("max").(int) == 0 { + log.Printf("[DEBUG] API returned -1 for a limit set to 0, keeping it as 0 in state") + d.Set("max", 0) + } else { + d.Set("max", limit.Max) + } + + // Only set the type field if it was originally specified in the configuration + if v, ok := d.GetOk("type"); ok { + // Preserve the original case of the type parameter + d.Set("type", v.(string)) + } + + return nil + } + } + + return fmt.Errorf("resource limit not found") +} + +func resourceCloudStackLimitsUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + resourcetype, err := getResourceType(d) + if err != nil { + return err + } + + account := d.Get("account").(string) + domainid := d.Get("domainid").(string) + projectid := d.Get("projectid").(string) + + // Create a new parameter struct + p := cs.Limit.NewUpdateResourceLimitParams(resourcetype) + if account != "" { + p.SetAccount(account) + } + if domainid != "" { + p.SetDomainid(domainid) + } + if maxVal, ok := d.GetOk("max"); ok { + maxIntVal := maxVal.(int) + log.Printf("[DEBUG] Setting max value to %d", maxIntVal) + p.SetMax(int64(maxIntVal)) + } + if projectid != "" { + p.SetProjectid(projectid) + } + + log.Printf("[DEBUG] Updating Resource Limit for type %d", resourcetype) + _, err = cs.Limit.UpdateResourceLimit(p) + + if err != nil { + return fmt.Errorf("Error updating resource limit: %s", err) + } + + return resourceCloudStackLimitsRead(d, meta) +} + +func resourceCloudStackLimitsDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + resourcetype, err := getResourceType(d) + if err != nil { + return err + } + + account := d.Get("account").(string) + domainid := d.Get("domainid").(string) + projectid := d.Get("projectid").(string) + + // Create a new parameter struct + p := cs.Limit.NewUpdateResourceLimitParams(resourcetype) + if account != "" { + p.SetAccount(account) + } + if domainid != "" { + p.SetDomainid(domainid) + } + if projectid != "" { + p.SetProjectid(projectid) + } + p.SetMax(-1) // Set to -1 to remove the limit + + log.Printf("[DEBUG] Removing Resource Limit for type %d", resourcetype) + _, err = cs.Limit.UpdateResourceLimit(p) + + if err != nil { + return fmt.Errorf("Error removing Resource Limit: %s", err) + } + + d.SetId("") + + return nil +} diff --git a/cloudstack/resource_cloudstack_limits_test.go b/cloudstack/resource_cloudstack_limits_test.go new file mode 100644 index 00000000..d0903a05 --- /dev/null +++ b/cloudstack/resource_cloudstack_limits_test.go @@ -0,0 +1,487 @@ +// 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/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccCloudStackLimits_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.foo"), + resource.TestCheckResourceAttr( + "cloudstack_limits.foo", "type", "instance"), + resource.TestCheckResourceAttr( + "cloudstack_limits.foo", "max", "10"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_update(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.foo"), + resource.TestCheckResourceAttr( + "cloudstack_limits.foo", "max", "10"), + ), + }, + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.foo"), + resource.TestCheckResourceAttr( + "cloudstack_limits.foo", "max", "20"), + ), + }, + }, + }) +} + +func testAccCheckCloudStackLimitsExists(n string) 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 Limits ID is set") + } + + return nil + } +} + +func testAccCheckCloudStackLimitsDestroy(s *terraform.State) error { + return nil +} + +const testAccCloudStackLimits_basic = ` +resource "cloudstack_limits" "foo" { + type = "instance" + max = 10 + domainid = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_update = ` +resource "cloudstack_limits" "foo" { + type = "instance" + max = 20 + domainid = cloudstack_domain.test_domain.id +} +` + +func TestAccCloudStackLimits_domain(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_domain_limit, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.domain_limit"), + resource.TestCheckResourceAttr( + "cloudstack_limits.domain_limit", "type", "volume"), + resource.TestCheckResourceAttr( + "cloudstack_limits.domain_limit", "max", "50"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_account(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_account, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.account_limit"), + resource.TestCheckResourceAttr( + "cloudstack_limits.account_limit", "type", "snapshot"), + resource.TestCheckResourceAttr( + "cloudstack_limits.account_limit", "max", "100"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_project(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_project, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.project_limit"), + resource.TestCheckResourceAttr( + "cloudstack_limits.project_limit", "type", "primarystorage"), + resource.TestCheckResourceAttr( + "cloudstack_limits.project_limit", "max", "1000"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_unlimited(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_unlimited, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.unlimited"), + resource.TestCheckResourceAttr( + "cloudstack_limits.unlimited", "type", "cpu"), + resource.TestCheckResourceAttr( + "cloudstack_limits.unlimited", "max", "-1"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_stringType(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_stringType, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.string_type"), + resource.TestCheckResourceAttr( + "cloudstack_limits.string_type", "type", "network"), + resource.TestCheckResourceAttr( + "cloudstack_limits.string_type", "max", "30"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_ip(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_ip, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.ip_limit"), + resource.TestCheckResourceAttr( + "cloudstack_limits.ip_limit", "type", "ip"), + resource.TestCheckResourceAttr( + "cloudstack_limits.ip_limit", "max", "25"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_template(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_template, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.template_limit"), + resource.TestCheckResourceAttr( + "cloudstack_limits.template_limit", "type", "template"), + resource.TestCheckResourceAttr( + "cloudstack_limits.template_limit", "max", "40"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_projectType(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_projectType, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.project_type_limit"), + resource.TestCheckResourceAttr( + "cloudstack_limits.project_type_limit", "type", "project"), + resource.TestCheckResourceAttr( + "cloudstack_limits.project_type_limit", "max", "15"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_vpc(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_vpc, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.vpc_limit"), + resource.TestCheckResourceAttr( + "cloudstack_limits.vpc_limit", "type", "vpc"), + resource.TestCheckResourceAttr( + "cloudstack_limits.vpc_limit", "max", "10"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_memory(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_memory, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.memory_limit"), + resource.TestCheckResourceAttr( + "cloudstack_limits.memory_limit", "type", "memory"), + resource.TestCheckResourceAttr( + "cloudstack_limits.memory_limit", "max", "8192"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_zero(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_zero, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.zero_limit"), + resource.TestCheckResourceAttr( + "cloudstack_limits.zero_limit", "type", "instance"), + resource.TestCheckResourceAttr( + "cloudstack_limits.zero_limit", "max", "0"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_secondarystorage(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_secondarystorage, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.secondarystorage_limit"), + resource.TestCheckResourceAttr( + "cloudstack_limits.secondarystorage_limit", "type", "secondarystorage"), + resource.TestCheckResourceAttr( + "cloudstack_limits.secondarystorage_limit", "max", "2000"), + ), + }, + }, + }) +} + +func TestAccCloudStackLimits_import(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackLimitsDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackLimits_domain + testAccCloudStackLimits_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackLimitsExists("cloudstack_limits.foo"), + ), + }, + { + ResourceName: "cloudstack_limits.foo", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"domainid", "type", "max"}, + }, + }, + }) +} + +// Test configurations for different resource types +const testAccCloudStackLimits_domain = ` +resource "cloudstack_domain" "test_domain" { + name = "test-domain-limits" +} +` + +const testAccCloudStackLimits_domain_limit = ` +resource "cloudstack_limits" "domain_limit" { + type = "volume" + max = 50 + domainid = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_account = ` +resource "cloudstack_account" "test_account" { + username = "test-account-limits" + password = "password" + first_name = "Test" + last_name = "Account" + email = "test-account-limits@example.com" + account_type = 2 # Regular user account type + role_id = 4 # Regular user role + domainid = cloudstack_domain.test_domain.id +} + +resource "cloudstack_limits" "account_limit" { + type = "snapshot" + max = 100 + account = cloudstack_account.test_account.username + domainid = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_project = ` +resource "cloudstack_limits" "project_limit" { + type = "primarystorage" + max = 1000 + domainid = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_unlimited = ` +resource "cloudstack_limits" "unlimited" { + type = "cpu" + max = -1 + domainid = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_stringType = ` +resource "cloudstack_limits" "string_type" { + type = "network" + max = 30 + domainid = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_ip = ` +resource "cloudstack_limits" "ip_limit" { + type = "ip" + max = 25 + domainid = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_template = ` +resource "cloudstack_limits" "template_limit" { + type = "template" + max = 40 + domainid = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_projectType = ` +resource "cloudstack_limits" "project_type_limit" { + type = "project" + max = 15 + domainid = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_vpc = ` +resource "cloudstack_limits" "vpc_limit" { + type = "vpc" + max = 10 + domainid = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_memory = ` +resource "cloudstack_limits" "memory_limit" { + type = "memory" + max = 8192 + domainid = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_zero = ` +# Testing setting a limit to 0 (zero resources allowed) +# Note: The CloudStack API may return -1 for a limit set to 0, but the provider maintains the 0 value in state +resource "cloudstack_limits" "zero_limit" { + type = "instance" + max = 0 + domainid = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_secondarystorage = ` +resource "cloudstack_limits" "secondarystorage_limit" { + type = "secondarystorage" + max = 2000 + domainid = cloudstack_domain.test_domain.id +} +` diff --git a/website/docs/d/limits.html.markdown b/website/docs/d/limits.html.markdown new file mode 100644 index 00000000..a27bb93a --- /dev/null +++ b/website/docs/d/limits.html.markdown @@ -0,0 +1,68 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_limits" +sidebar_current: "docs-cloudstack-datasource-limits" +description: |- + Gets information about CloudStack resource limits. +--- + +# cloudstack_limits + +Use this data source to retrieve information about CloudStack resource limits for accounts, domains, and projects. + +## Example Usage + +```hcl +# Get all resource limits for a specific domain +data "cloudstack_limits" "domain_limits" { + domainid = "domain-uuid" +} + +# Get instance limits for a specific account +data "cloudstack_limits" "account_instance_limits" { + type = "instance" + account = "acct1" + domainid = "domain-uuid" +} + +# Get primary storage limits for a project +data "cloudstack_limits" "project_storage_limits" { + type = "primarystorage" + projectid = "project-uuid" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `type` - (Optional) The type of resource to list the limits. Available types are: + * `instance` + * `ip` + * `volume` + * `snapshot` + * `template` + * `project` + * `network` + * `vpc` + * `cpu` + * `memory` + * `primarystorage` + * `secondarystorage` +* `account` - (Optional) List resources by account. Must be used with the `domainid` parameter. +* `domainid` - (Optional) List only resources belonging to the domain specified. +* `projectid` - (Optional) List resource limits by project. + +## Attributes Reference + +The following attributes are exported: + +* `limits` - A list of resource limits. Each limit has the following attributes: + * `resourcetype` - The type of resource. + * `resourcetypename` - The name of the resource type. + * `max` - The maximum number of the resource. A value of `-1` indicates unlimited resources. A value of `0` means zero resources are allowed, though the CloudStack API may return `-1` for a limit set to `0`. + * `account` - The account of the resource limit. + * `domain` - The domain name of the resource limit. + * `domainid` - The domain ID of the resource limit. + * `project` - The project name of the resource limit. + * `projectid` - The project ID of the resource limit. diff --git a/website/docs/r/limits.html.markdown b/website/docs/r/limits.html.markdown new file mode 100644 index 00000000..80c7fc39 --- /dev/null +++ b/website/docs/r/limits.html.markdown @@ -0,0 +1,88 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_limits" +sidebar_current: "docs-cloudstack-limits" +description: |- + Provides a CloudStack limits resource. +--- + +# cloudstack_limits + +Provides a CloudStack limits resource. This can be used to manage resource limits for accounts, domains, and projects within CloudStack. + +## Example Usage + +```hcl +# Set instance limit for the root domain +resource "cloudstack_limits" "instance_limit" { + type = "instance" + max = 20 +} + +# Set volume limit for a specific account in a domain +resource "cloudstack_limits" "volume_limit" { + type = "volume" + max = 50 + account = "acct1" + domainid = "domain-uuid" +} + +# Set primary storage limit for a project +resource "cloudstack_limits" "storage_limit" { + type = "primarystorage" + max = 1000 # GB + projectid = "project-uuid" +} + +# Set unlimited CPU limit +resource "cloudstack_limits" "cpu_unlimited" { + type = "cpu" + max = -1 # Unlimited +} +``` + +## Argument Reference + +The following arguments are supported: + +* `type` - (Required, ForceNew) The type of resource to update. Available types are: + * `instance` + * `ip` + * `volume` + * `snapshot` + * `template` + * `project` + * `network` + * `vpc` + * `cpu` + * `memory` + * `primarystorage` + * `secondarystorage` + +* `account` - (Optional, ForceNew) Update resource for a specified account. Must be used with the `domainid` parameter. +* `domainid` - (Optional, ForceNew) Update resource limits for all accounts in specified domain. If used with the `account` parameter, updates resource limits for a specified account in specified domain. +* `max` - (Optional) Maximum resource limit. Use `-1` for unlimited resource limit. A value of `0` means zero resources are allowed, though the CloudStack API may return `-1` for a limit set to `0`. +* `projectid` - (Optional, ForceNew) Update resource limits for project. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the resource. +* `type` - The type of resource. +* `max` - The maximum number of the resource. +* `account` - The account of the resource limit. +* `domainid` - The domain ID of the resource limit. +* `projectid` - The project ID of the resource limit. + +## Import + +Resource limits can be imported using the resource type (numeric), account, domain ID, and project ID, e.g. + +```bash +terraform import cloudstack_limits.instance_limit 0 +terraform import cloudstack_limits.volume_limit 2-acct1-domain-uuid +terraform import cloudstack_limits.storage_limit 10-project-uuid +``` + +When importing, the numeric resource type is used in the import ID. The provider will automatically convert the numeric type to the corresponding string type after import.