From 3494af66b55ed8aae76fdf751fb498a6c2241660 Mon Sep 17 00:00:00 2001 From: Ian Crouch Date: Mon, 14 Jul 2025 14:24:25 -0400 Subject: [PATCH 1/6] feat: add cloudstack_limits data source and resource management - Implemented data source for retrieving CloudStack resource limits. - Added resource management for setting and updating resource limits for accounts, domains, and projects. - Updated documentation for cloudstack_limits with usage examples and argument references. --- cloudstack/data_source_cloudstack_limits.go | 210 ++++++++++++ cloudstack/provider.go | 2 + cloudstack/resource_cloudstack_limits.go | 320 ++++++++++++++++++ cloudstack/resource_cloudstack_limits_test.go | 289 ++++++++++++++++ website/docs/d/limits.html.markdown | 71 ++++ website/docs/r/limits.html.markdown | 91 +++++ 6 files changed, 983 insertions(+) create mode 100644 cloudstack/data_source_cloudstack_limits.go create mode 100644 cloudstack/resource_cloudstack_limits.go create mode 100644 cloudstack/resource_cloudstack_limits_test.go create mode 100644 website/docs/d/limits.html.markdown create mode 100644 website/docs/r/limits.html.markdown diff --git a/cloudstack/data_source_cloudstack_limits.go b/cloudstack/data_source_cloudstack_limits.go new file mode 100644 index 00000000..12069328 --- /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", "publicip", "eip", "autoscalevmgroup", + }, 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, publicip, eip, autoscalevmgroup", + }, + "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..3363c06d --- /dev/null +++ b/cloudstack/resource_cloudstack_limits.go @@ -0,0 +1,320 @@ +// 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, + "publicip": 12, + "eip": 13, + "autoscalevmgroup": 14, +} + +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", "publicip", "eip", "autoscalevmgroup", + }, 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, publicip, eip, autoscalevmgroup", + }, + "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.", + }, + "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) + max := d.Get("max").(int) + 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 max != 0 { + p.SetMax(int64(max)) + } + 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) { + 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) + max := d.Get("max").(int) + 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 max != 0 { + p.SetMax(int64(max)) + } + 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..e61eeb02 --- /dev/null +++ b/cloudstack/resource_cloudstack_limits_test.go @@ -0,0 +1,289 @@ +// 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_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", "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 +} +` diff --git a/website/docs/d/limits.html.markdown b/website/docs/d/limits.html.markdown new file mode 100644 index 00000000..7c75e854 --- /dev/null +++ b/website/docs/d/limits.html.markdown @@ -0,0 +1,71 @@ +--- +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` + * `publicip` + * `eip` + * `autoscalevmgroup` +* `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. + * `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..f63a3049 --- /dev/null +++ b/website/docs/r/limits.html.markdown @@ -0,0 +1,91 @@ +--- +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` + * `publicip` + * `eip` + * `autoscalevmgroup` + +* `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. +* `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. From e3b3d903c39aa5be1c0420aec69aabdd284527d6 Mon Sep 17 00:00:00 2001 From: Ian Crouch Date: Tue, 22 Jul 2025 14:54:53 -0400 Subject: [PATCH 2/6] feat: add tests for additional CloudStack limits resource types --- cloudstack/resource_cloudstack_limits.go | 7 +- cloudstack/resource_cloudstack_limits_test.go | 168 ++++++++++++++++++ 2 files changed, 170 insertions(+), 5 deletions(-) diff --git a/cloudstack/resource_cloudstack_limits.go b/cloudstack/resource_cloudstack_limits.go index 3363c06d..532e7d1d 100644 --- a/cloudstack/resource_cloudstack_limits.go +++ b/cloudstack/resource_cloudstack_limits.go @@ -43,9 +43,6 @@ var resourceTypeMap = map[string]int{ "memory": 9, "primarystorage": 10, "secondarystorage": 11, - "publicip": 12, - "eip": 13, - "autoscalevmgroup": 14, } func resourceCloudStackLimits() *schema.Resource { @@ -61,11 +58,11 @@ func resourceCloudStackLimits() *schema.Resource { ForceNew: true, ValidateFunc: validation.StringInSlice([]string{ "instance", "ip", "volume", "snapshot", "template", "project", "network", "vpc", - "cpu", "memory", "primarystorage", "secondarystorage", "publicip", "eip", "autoscalevmgroup", + "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, publicip, eip, autoscalevmgroup", + "primarystorage, secondarystorage", }, "account": { Type: schema.TypeString, diff --git a/cloudstack/resource_cloudstack_limits_test.go b/cloudstack/resource_cloudstack_limits_test.go index e61eeb02..01a35b14 100644 --- a/cloudstack/resource_cloudstack_limits_test.go +++ b/cloudstack/resource_cloudstack_limits_test.go @@ -207,6 +207,126 @@ func TestAccCloudStackLimits_stringType(t *testing.T) { }) } +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_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) }, @@ -287,3 +407,51 @@ resource "cloudstack_limits" "string_type" { 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_secondarystorage = ` +resource "cloudstack_limits" "secondarystorage_limit" { + type = "secondarystorage" + max = 2000 + domainid = cloudstack_domain.test_domain.id +} +` From 0b9d7fcfda0b769fdc4c049d0f1266a0cb706e23 Mon Sep 17 00:00:00 2001 From: Ian Crouch Date: Thu, 7 Aug 2025 14:17:24 -0400 Subject: [PATCH 3/6] feat: update CloudStack limits handling for zero resources and unlimited resources Implimenting copilot suggestions for d.GetOk --- cloudstack/data_source_cloudstack_limits.go | 4 +-- cloudstack/resource_cloudstack_limits.go | 26 ++++++++++----- cloudstack/resource_cloudstack_limits_test.go | 32 ++++++++++++++++++- website/docs/d/limits.html.markdown | 5 +-- website/docs/r/limits.html.markdown | 5 +-- 5 files changed, 53 insertions(+), 19 deletions(-) diff --git a/cloudstack/data_source_cloudstack_limits.go b/cloudstack/data_source_cloudstack_limits.go index 12069328..ddff885b 100644 --- a/cloudstack/data_source_cloudstack_limits.go +++ b/cloudstack/data_source_cloudstack_limits.go @@ -38,11 +38,11 @@ func dataSourceCloudStackLimits() *schema.Resource { Optional: true, ValidateFunc: validation.StringInSlice([]string{ "instance", "ip", "volume", "snapshot", "template", "project", "network", "vpc", - "cpu", "memory", "primarystorage", "secondarystorage", "publicip", "eip", "autoscalevmgroup", + "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, publicip, eip, autoscalevmgroup", + "primarystorage, secondarystorage", }, "account": { Type: schema.TypeString, diff --git a/cloudstack/resource_cloudstack_limits.go b/cloudstack/resource_cloudstack_limits.go index 532e7d1d..e2b576d8 100644 --- a/cloudstack/resource_cloudstack_limits.go +++ b/cloudstack/resource_cloudstack_limits.go @@ -79,7 +79,7 @@ func resourceCloudStackLimits() *schema.Resource { "max": { Type: schema.TypeInt, Optional: true, - Description: "Maximum resource limit. Use -1 for unlimited resource limit.", + 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, @@ -118,7 +118,6 @@ func resourceCloudStackLimitsCreate(d *schema.ResourceData, meta interface{}) er account := d.Get("account").(string) domainid := d.Get("domainid").(string) - max := d.Get("max").(int) projectid := d.Get("projectid").(string) // Validate account and domain parameters @@ -134,8 +133,10 @@ func resourceCloudStackLimitsCreate(d *schema.ResourceData, meta interface{}) er if domainid != "" { p.SetDomainid(domainid) } - if max != 0 { - p.SetMax(int64(max)) + 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) @@ -226,7 +227,15 @@ func resourceCloudStackLimitsRead(d *schema.ResourceData, meta interface{}) erro // Update the config for _, limit := range l.ResourceLimits { if limit.Resourcetype == fmt.Sprintf("%d", resourcetype) { - d.Set("max", limit.Max) + 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 { @@ -251,7 +260,6 @@ func resourceCloudStackLimitsUpdate(d *schema.ResourceData, meta interface{}) er account := d.Get("account").(string) domainid := d.Get("domainid").(string) - max := d.Get("max").(int) projectid := d.Get("projectid").(string) // Create a new parameter struct @@ -262,8 +270,10 @@ func resourceCloudStackLimitsUpdate(d *schema.ResourceData, meta interface{}) er if domainid != "" { p.SetDomainid(domainid) } - if max != 0 { - p.SetMax(int64(max)) + 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) diff --git a/cloudstack/resource_cloudstack_limits_test.go b/cloudstack/resource_cloudstack_limits_test.go index 01a35b14..09b078d6 100644 --- a/cloudstack/resource_cloudstack_limits_test.go +++ b/cloudstack/resource_cloudstack_limits_test.go @@ -307,6 +307,26 @@ func TestAccCloudStackLimits_memory(t *testing.T) { }) } +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) }, @@ -343,7 +363,7 @@ func TestAccCloudStackLimits_import(t *testing.T) { ResourceName: "cloudstack_limits.foo", ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"domainid", "type", "type", "max"}, + ImportStateVerifyIgnore: []string{"domainid", "type", "max"}, }, }, }) @@ -448,6 +468,16 @@ resource "cloudstack_limits" "memory_limit" { } ` +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" diff --git a/website/docs/d/limits.html.markdown b/website/docs/d/limits.html.markdown index 7c75e854..a27bb93a 100644 --- a/website/docs/d/limits.html.markdown +++ b/website/docs/d/limits.html.markdown @@ -49,9 +49,6 @@ The following arguments are supported: * `memory` * `primarystorage` * `secondarystorage` - * `publicip` - * `eip` - * `autoscalevmgroup` * `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. @@ -63,7 +60,7 @@ 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. + * `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. diff --git a/website/docs/r/limits.html.markdown b/website/docs/r/limits.html.markdown index f63a3049..80c7fc39 100644 --- a/website/docs/r/limits.html.markdown +++ b/website/docs/r/limits.html.markdown @@ -58,13 +58,10 @@ The following arguments are supported: * `memory` * `primarystorage` * `secondarystorage` - * `publicip` - * `eip` - * `autoscalevmgroup` * `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. +* `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 From 9be4bbae0e81cf2667e21fe53fd448cb20c6a6c7 Mon Sep 17 00:00:00 2001 From: Ian Date: Thu, 7 Aug 2025 11:20:04 -0700 Subject: [PATCH 4/6] tabs to spaces Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cloudstack/resource_cloudstack_limits_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudstack/resource_cloudstack_limits_test.go b/cloudstack/resource_cloudstack_limits_test.go index 09b078d6..d0903a05 100644 --- a/cloudstack/resource_cloudstack_limits_test.go +++ b/cloudstack/resource_cloudstack_limits_test.go @@ -378,7 +378,7 @@ resource "cloudstack_domain" "test_domain" { const testAccCloudStackLimits_domain_limit = ` resource "cloudstack_limits" "domain_limit" { - type = "volume" + type = "volume" max = 50 domainid = cloudstack_domain.test_domain.id } From 0a72f69768ffb04f41f646af890ac5940eb8422d Mon Sep 17 00:00:00 2001 From: Ian Crouch Date: Thu, 14 Aug 2025 13:43:12 -0400 Subject: [PATCH 5/6] - Comment project related limit testing - update resourceCloudStackLimitsRead to handle different ID formats - rewrite resourceCloudStackLimitsImport to handle different ID formats - Support -1 (Unlimited) and 0 (zero) limits --- cloudstack/resource_cloudstack_limits.go | 115 +++++++++++++- cloudstack/resource_cloudstack_limits_test.go | 148 ++++++++++++++---- 2 files changed, 230 insertions(+), 33 deletions(-) diff --git a/cloudstack/resource_cloudstack_limits.go b/cloudstack/resource_cloudstack_limits.go index e2b576d8..f5c44f53 100644 --- a/cloudstack/resource_cloudstack_limits.go +++ b/cloudstack/resource_cloudstack_limits.go @@ -19,6 +19,7 @@ package cloudstack import ( + "context" "fmt" "log" "strconv" @@ -89,11 +90,92 @@ func resourceCloudStackLimits() *schema.Resource { }, }, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: resourceCloudStackLimitsImport, }, } } +// resourceCloudStackLimitsImport parses composite import IDs and sets resource fields accordingly. +func resourceCloudStackLimitsImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + // Expected formats: + // - type-account-accountname-domainid (for account-specific limits) + // - type-project-projectid (for project-specific limits) + // - type-domain-domainid (for domain-specific limits) + + log.Printf("[DEBUG] Importing resource with ID: %s", d.Id()) + + // First, extract the resource type which is always the first part + idParts := strings.SplitN(d.Id(), "-", 2) + if len(idParts) < 2 { + return nil, fmt.Errorf("unexpected import ID format (%q), expected type-account-accountname-domainid, type-domain-domainid, or type-project-projectid", d.Id()) + } + + // Parse the resource type + typeInt, err := strconv.Atoi(idParts[0]) + if err != nil { + return nil, fmt.Errorf("invalid type value in import ID: %s", idParts[0]) + } + + // Find the string representation for this numeric type + var typeStr string + for k, v := range resourceTypeMap { + if v == typeInt { + typeStr = k + break + } + } + if typeStr == "" { + return nil, fmt.Errorf("unknown type value in import ID: %d", typeInt) + } + if err := d.Set("type", typeStr); err != nil { + return nil, err + } + + // Get the original resource ID from the state + originalID := d.Id() + log.Printf("[DEBUG] Original import ID: %s", originalID) + + // Instead of trying to parse the complex ID, let's create a new resource + // and read it from the API to get the correct values + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct for listing resource limits + p := cs.Limit.NewListResourceLimitsParams() + p.SetResourcetype(typeInt) + + // Try to determine the resource scope from the ID format + remainingID := idParts[1] + + // Extract the resource scope from the ID + if strings.HasPrefix(remainingID, "domain-") { + // It's a domain-specific limit + log.Printf("[DEBUG] Detected domain-specific limit") + // We'll use the Read function to get the domain ID from the state + // after setting a temporary ID + d.SetId(originalID) + return []*schema.ResourceData{d}, nil + } else if strings.HasPrefix(remainingID, "project-") { + // It's a project-specific limit + log.Printf("[DEBUG] Detected project-specific limit") + // We'll use the Read function to get the project ID from the state + // after setting a temporary ID + d.SetId(originalID) + return []*schema.ResourceData{d}, nil + } else if strings.HasPrefix(remainingID, "account-") { + // It's an account-specific limit + log.Printf("[DEBUG] Detected account-specific limit") + // We'll use the Read function to get the account and domain ID from the state + // after setting a temporary ID + d.SetId(originalID) + return []*schema.ResourceData{d}, nil + } else { + // For backward compatibility, assume it's a global limit + log.Printf("[DEBUG] Detected global limit") + d.SetId(originalID) + return []*schema.ResourceData{d}, nil + } +} + // getResourceType gets the resource type from the type field func getResourceType(d *schema.ResourceData) (int, error) { // Check if type is set @@ -191,6 +273,21 @@ func resourceCloudStackLimitsRead(d *schema.ResourceData, meta interface{}) erro break } } + + // Handle different ID formats + if len(idParts) >= 3 { + if idParts[1] == "domain" { + // Format: resourcetype-domain-domainid + d.Set("domainid", idParts[2]) + } else if idParts[1] == "project" { + // Format: resourcetype-project-projectid + d.Set("projectid", idParts[2]) + } else if idParts[1] == "account" && len(idParts) >= 4 { + // Format: resourcetype-account-account-domainid + d.Set("account", idParts[2]) + d.Set("domainid", idParts[3]) + } + } } } } @@ -229,11 +326,19 @@ func resourceCloudStackLimitsRead(d *schema.ResourceData, meta interface{}) erro 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) + // Handle CloudStack's convention where -1 signifies unlimited and 0 signifies zero + if limit.Max == -1 { + // For the zero limit test case, we need to preserve the 0 value + // We'll check if the resource was created with max=0 + if 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 { + // Otherwise, use -1 to represent unlimited + d.Set("max", limit.Max) + } } else { + // For any other value, use it directly d.Set("max", limit.Max) } diff --git a/cloudstack/resource_cloudstack_limits_test.go b/cloudstack/resource_cloudstack_limits_test.go index d0903a05..7a146868 100644 --- a/cloudstack/resource_cloudstack_limits_test.go +++ b/cloudstack/resource_cloudstack_limits_test.go @@ -147,25 +147,25 @@ func TestAccCloudStackLimits_account(t *testing.T) { }) } -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_project(t *testing.T) { #TODO: Need project imported before this will do anything +// 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{ @@ -369,6 +369,53 @@ func TestAccCloudStackLimits_import(t *testing.T) { }) } +// Test importing domain-specific limits +func TestAccCloudStackLimits_importDomain(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"), + ), + }, + { + ResourceName: "cloudstack_limits.domain_limit", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"domainid", "type", "max"}, + }, + }, + }) +} + +// Test importing account-specific limits +// Note: We're not verifying the state here because the account import is complex +// and we just want to make sure the import succeeds +func TestAccCloudStackLimits_importAccount(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"), + ), + }, + { + ResourceName: "cloudstack_limits.account_limit", + ImportState: true, + ImportStateVerify: false, // Don't verify the state + }, + }, + }) +} + // Test configurations for different resource types const testAccCloudStackLimits_domain = ` resource "cloudstack_domain" "test_domain" { @@ -404,13 +451,20 @@ resource "cloudstack_limits" "account_limit" { } ` -const testAccCloudStackLimits_project = ` -resource "cloudstack_limits" "project_limit" { - type = "primarystorage" - max = 1000 - domainid = cloudstack_domain.test_domain.id -} -` +// const testAccCloudStackLimits_project = ` #TODO: Need project imported before this will do anything +// resource "cloudstack_project" "test_project" { +// name = "test-project-limits" +// display_text = "Test Project for Limits" +// domainid = cloudstack_domain.test_domain.id +// } +// +// resource "cloudstack_limits" "project_limit" { +// type = "primarystorage" +// max = 1000 +// domainid = cloudstack_domain.test_domain.id +// projectid = cloudstack_project.test_project.id +// } +// ` const testAccCloudStackLimits_unlimited = ` resource "cloudstack_limits" "unlimited" { @@ -469,8 +523,6 @@ resource "cloudstack_limits" "memory_limit" { ` 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 @@ -485,3 +537,43 @@ resource "cloudstack_limits" "secondarystorage_limit" { domainid = cloudstack_domain.test_domain.id } ` + +const testAccCloudStackLimits_zeroToPositive = ` +resource "cloudstack_limits" "zero_limit" { + type = "instance" + max = 5 + domainid = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_positiveValue = ` +resource "cloudstack_limits" "positive_limit" { + type = "instance" + max = 15 + domainid = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_positiveToZero = ` +resource "cloudstack_limits" "positive_limit" { + type = "instance" + max = 0 + domainid = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_positiveToUnlimited = ` +resource "cloudstack_limits" "positive_limit" { + type = "instance" + max = -1 + domainid = cloudstack_domain.test_domain.id +} +` + +const testAccCloudStackLimits_unlimitedToZero = ` +resource "cloudstack_limits" "unlimited" { + type = "cpu" + max = 0 + domainid = cloudstack_domain.test_domain.id +} +` From a6cbbfa17945b3f1d4835d72c43f96b3c2987431 Mon Sep 17 00:00:00 2001 From: Ian Crouch Date: Thu, 4 Sep 2025 14:51:58 -0400 Subject: [PATCH 6/6] fix: update parameter names from 'domainid' to 'domain_id' for consistency add configured_max to capture intended max value of 0 or -1 and cleanup the related code --- cloudstack/data_source_cloudstack_limits.go | 10 +- cloudstack/resource_cloudstack_limits.go | 157 +++++++++++------- cloudstack/resource_cloudstack_limits_test.go | 46 ++--- 3 files changed, 126 insertions(+), 87 deletions(-) diff --git a/cloudstack/data_source_cloudstack_limits.go b/cloudstack/data_source_cloudstack_limits.go index ddff885b..f9086f51 100644 --- a/cloudstack/data_source_cloudstack_limits.go +++ b/cloudstack/data_source_cloudstack_limits.go @@ -47,9 +47,9 @@ func dataSourceCloudStackLimits() *schema.Resource { "account": { Type: schema.TypeString, Optional: true, - Description: "List resources by account. Must be used with the domainid parameter.", + Description: "List resources by account. Must be used with the domain_id parameter.", }, - "domainid": { + "domain_id": { Type: schema.TypeString, Optional: true, Description: "List only resources belonging to the domain specified.", @@ -80,7 +80,7 @@ func dataSourceCloudStackLimits() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "domainid": { + "domain_id": { Type: schema.TypeString, Computed: true, }, @@ -123,7 +123,7 @@ func dataSourceCloudStackLimitsRead(d *schema.ResourceData, meta interface{}) er p.SetAccount(v.(string)) } - if v, ok := d.GetOk("domainid"); ok { + if v, ok := d.GetOk("domain_id"); ok { p.SetDomainid(v.(string)) } @@ -196,7 +196,7 @@ func generateDataSourceID(d *schema.ResourceData) string { buf.WriteString(fmt.Sprintf("%s-", v.(string))) } - if v, ok := d.GetOk("domainid"); ok { + if v, ok := d.GetOk("domain_id"); ok { buf.WriteString(fmt.Sprintf("%s-", v.(string))) } diff --git a/cloudstack/resource_cloudstack_limits.go b/cloudstack/resource_cloudstack_limits.go index f5c44f53..4b4d98b8 100644 --- a/cloudstack/resource_cloudstack_limits.go +++ b/cloudstack/resource_cloudstack_limits.go @@ -69,9 +69,9 @@ func resourceCloudStackLimits() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, - Description: "Update resource for a specified account. Must be used with the domainid parameter.", + Description: "Update resource for a specified account. Must be used with the domain_id parameter.", }, - "domainid": { + "domain_id": { Type: schema.TypeString, Optional: true, ForceNew: true, @@ -82,6 +82,11 @@ func resourceCloudStackLimits() *schema.Resource { 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.", }, + "configured_max": { + Type: schema.TypeInt, + Computed: true, + Description: "Internal field to track the originally configured max value to distinguish between 0 and -1 when CloudStack returns -1.", + }, "projectid": { Type: schema.TypeString, Optional: true, @@ -98,16 +103,16 @@ func resourceCloudStackLimits() *schema.Resource { // resourceCloudStackLimitsImport parses composite import IDs and sets resource fields accordingly. func resourceCloudStackLimitsImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { // Expected formats: - // - type-account-accountname-domainid (for account-specific limits) + // - type-account-accountname-domain_id (for account-specific limits) // - type-project-projectid (for project-specific limits) - // - type-domain-domainid (for domain-specific limits) + // - type-domain-domain_id (for domain-specific limits) log.Printf("[DEBUG] Importing resource with ID: %s", d.Id()) // First, extract the resource type which is always the first part idParts := strings.SplitN(d.Id(), "-", 2) if len(idParts) < 2 { - return nil, fmt.Errorf("unexpected import ID format (%q), expected type-account-accountname-domainid, type-domain-domainid, or type-project-projectid", d.Id()) + return nil, fmt.Errorf("unexpected import ID format (%q), expected type-account-accountname-domain_id, type-domain-domain_id, or type-project-projectid", d.Id()) } // Parse the resource type @@ -199,12 +204,12 @@ func resourceCloudStackLimitsCreate(d *schema.ResourceData, meta interface{}) er } account := d.Get("account").(string) - domainid := d.Get("domainid").(string) + domain_id := d.Get("domain_id").(string) projectid := d.Get("projectid").(string) // Validate account and domain parameters - if account != "" && domainid == "" { - return fmt.Errorf("domainid is required when account is specified") + if account != "" && domain_id == "" { + return fmt.Errorf("domain_id is required when account is specified") } // Create a new parameter struct @@ -212,13 +217,23 @@ func resourceCloudStackLimitsCreate(d *schema.ResourceData, meta interface{}) er if account != "" { p.SetAccount(account) } - if domainid != "" { - p.SetDomainid(domainid) + if domain_id != "" { + p.SetDomainid(domain_id) } - if maxVal, ok := d.GetOk("max"); ok { + // Check for max value - need to handle zero values explicitly + maxVal := d.Get("max") + if maxVal != nil { maxIntVal := maxVal.(int) log.Printf("[DEBUG] Setting max value to %d", maxIntVal) p.SetMax(int64(maxIntVal)) + + // Store the original configured value for later reference + // This helps the Read function distinguish between 0 and -1 when CloudStack returns -1 + if err := d.Set("configured_max", maxIntVal); err != nil { + return fmt.Errorf("error storing configured max value: %w", err) + } + } else { + log.Printf("[DEBUG] No max value found in configuration during Create") } if projectid != "" { p.SetProjectid(projectid) @@ -232,24 +247,24 @@ func resourceCloudStackLimitsCreate(d *schema.ResourceData, meta interface{}) er } // Generate a unique ID based on the parameters - id := generateResourceID(resourcetype, account, domainid, projectid) + id := generateResourceID(resourcetype, account, domain_id, 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 { +func generateResourceID(resourcetype int, account, domain_id, 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 account != "" && domain_id != "" { + return fmt.Sprintf("%d-account-%s-%s", resourcetype, account, domain_id) } - if domainid != "" { - return fmt.Sprintf("%d-domain-%s", resourcetype, domainid) + if domain_id != "" { + return fmt.Sprintf("%d-domain-%s", resourcetype, domain_id) } return fmt.Sprintf("%d", resourcetype) @@ -269,7 +284,9 @@ func resourceCloudStackLimitsRead(d *schema.ResourceData, meta interface{}) erro // Find the string representation for this numeric type for typeStr, typeVal := range resourceTypeMap { if typeVal == rt { - d.Set("type", typeStr) + if err := d.Set("type", typeStr); err != nil { + return fmt.Errorf("error setting type: %s", err) + } break } } @@ -277,15 +294,23 @@ func resourceCloudStackLimitsRead(d *schema.ResourceData, meta interface{}) erro // Handle different ID formats if len(idParts) >= 3 { if idParts[1] == "domain" { - // Format: resourcetype-domain-domainid - d.Set("domainid", idParts[2]) + // Format: resourcetype-domain-domain_id + if err := d.Set("domain_id", idParts[2]); err != nil { + return fmt.Errorf("error setting domain_id: %s", err) + } } else if idParts[1] == "project" { // Format: resourcetype-project-projectid - d.Set("projectid", idParts[2]) + if err := d.Set("projectid", idParts[2]); err != nil { + return fmt.Errorf("error setting projectid: %s", err) + } } else if idParts[1] == "account" && len(idParts) >= 4 { - // Format: resourcetype-account-account-domainid - d.Set("account", idParts[2]) - d.Set("domainid", idParts[3]) + // Format: resourcetype-account-account-domain_id + if err := d.Set("account", idParts[2]); err != nil { + return fmt.Errorf("error setting account: %s", err) + } + if err := d.Set("domain_id", idParts[3]); err != nil { + return fmt.Errorf("error setting domain_id: %s", err) + } } } } @@ -293,7 +318,7 @@ func resourceCloudStackLimitsRead(d *schema.ResourceData, meta interface{}) erro } account := d.Get("account").(string) - domainid := d.Get("domainid").(string) + domain_id := d.Get("domain_id").(string) projectid := d.Get("projectid").(string) // Create a new parameter struct @@ -302,8 +327,8 @@ func resourceCloudStackLimitsRead(d *schema.ResourceData, meta interface{}) erro if account != "" { p.SetAccount(account) } - if domainid != "" { - p.SetDomainid(domainid) + if domain_id != "" { + p.SetDomainid(domain_id) } if projectid != "" { p.SetProjectid(projectid) @@ -321,38 +346,45 @@ func resourceCloudStackLimitsRead(d *schema.ResourceData, meta interface{}) erro 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) - - // Handle CloudStack's convention where -1 signifies unlimited and 0 signifies zero - if limit.Max == -1 { - // For the zero limit test case, we need to preserve the 0 value - // We'll check if the resource was created with max=0 - if 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 { - // Otherwise, use -1 to represent unlimited - d.Set("max", limit.Max) - } - } else { - // For any other value, use it directly - d.Set("max", limit.Max) + // Get the first (and should be only) limit from the results + limit := l.ResourceLimits[0] + + // Handle the max value - CloudStack may return -1 for both unlimited and zero limits + // We need to preserve the original value from the configuration when possible + log.Printf("[DEBUG] CloudStack returned max value: %d", limit.Max) + if limit.Max == -1 { + // CloudStack returns -1 for both unlimited and zero limits + // Check if we have the originally configured value stored + if configuredMax, hasConfiguredMax := d.GetOk("configured_max"); hasConfiguredMax { + configuredValue := configuredMax.(int) + log.Printf("[DEBUG] Found configured max value: %d, using it", configuredValue) + // Use the originally configured value (0 for zero limit, -1 for unlimited) + if err := d.Set("max", configuredValue); err != nil { + return fmt.Errorf("error setting max to configured value %d: %w", configuredValue, err) } - - // 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)) + } else { + log.Printf("[DEBUG] No configured max value found, treating -1 as unlimited") + // If no configured value is stored, treat -1 as unlimited + if err := d.Set("max", -1); err != nil { + return fmt.Errorf("error setting max to unlimited (-1): %w", err) } + } + } else { + log.Printf("[DEBUG] Using positive max value from API: %d", limit.Max) + // For any positive value, use it directly from the API + if err := d.Set("max", int(limit.Max)); err != nil { + return fmt.Errorf("error setting max: %w", err) + } + } - return nil + // Preserve original type configuration if it exists + if typeValue, ok := d.GetOk("type"); ok { + if err := d.Set("type", typeValue.(string)); err != nil { + return fmt.Errorf("error setting type: %w", err) } } - return fmt.Errorf("resource limit not found") + return nil } func resourceCloudStackLimitsUpdate(d *schema.ResourceData, meta interface{}) error { @@ -364,7 +396,7 @@ func resourceCloudStackLimitsUpdate(d *schema.ResourceData, meta interface{}) er } account := d.Get("account").(string) - domainid := d.Get("domainid").(string) + domain_id := d.Get("domain_id").(string) projectid := d.Get("projectid").(string) // Create a new parameter struct @@ -372,13 +404,20 @@ func resourceCloudStackLimitsUpdate(d *schema.ResourceData, meta interface{}) er if account != "" { p.SetAccount(account) } - if domainid != "" { - p.SetDomainid(domainid) + if domain_id != "" { + p.SetDomainid(domain_id) } if maxVal, ok := d.GetOk("max"); ok { maxIntVal := maxVal.(int) log.Printf("[DEBUG] Setting max value to %d", maxIntVal) p.SetMax(int64(maxIntVal)) + + // Store the original configured value for later reference + // This helps the Read function distinguish between 0 and -1 when CloudStack returns -1 + log.Printf("[DEBUG] Storing configured max value in update: %d", maxIntVal) + if err := d.Set("configured_max", maxIntVal); err != nil { + return fmt.Errorf("error storing configured max value: %w", err) + } } if projectid != "" { p.SetProjectid(projectid) @@ -403,7 +442,7 @@ func resourceCloudStackLimitsDelete(d *schema.ResourceData, meta interface{}) er } account := d.Get("account").(string) - domainid := d.Get("domainid").(string) + domain_id := d.Get("domain_id").(string) projectid := d.Get("projectid").(string) // Create a new parameter struct @@ -411,8 +450,8 @@ func resourceCloudStackLimitsDelete(d *schema.ResourceData, meta interface{}) er if account != "" { p.SetAccount(account) } - if domainid != "" { - p.SetDomainid(domainid) + if domain_id != "" { + p.SetDomainid(domain_id) } if projectid != "" { p.SetProjectid(projectid) diff --git a/cloudstack/resource_cloudstack_limits_test.go b/cloudstack/resource_cloudstack_limits_test.go index 7a146868..be98fb16 100644 --- a/cloudstack/resource_cloudstack_limits_test.go +++ b/cloudstack/resource_cloudstack_limits_test.go @@ -95,7 +95,7 @@ const testAccCloudStackLimits_basic = ` resource "cloudstack_limits" "foo" { type = "instance" max = 10 - domainid = cloudstack_domain.test_domain.id + domain_id = cloudstack_domain.test_domain.id } ` @@ -103,7 +103,7 @@ const testAccCloudStackLimits_update = ` resource "cloudstack_limits" "foo" { type = "instance" max = 20 - domainid = cloudstack_domain.test_domain.id + domain_id = cloudstack_domain.test_domain.id } ` @@ -363,7 +363,7 @@ func TestAccCloudStackLimits_import(t *testing.T) { ResourceName: "cloudstack_limits.foo", ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"domainid", "type", "max"}, + ImportStateVerifyIgnore: []string{"domain_id", "type", "max", "configured_max"}, }, }, }) @@ -386,7 +386,7 @@ func TestAccCloudStackLimits_importDomain(t *testing.T) { ResourceName: "cloudstack_limits.domain_limit", ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"domainid", "type", "max"}, + ImportStateVerifyIgnore: []string{"domain_id", "type", "max", "configured_max"}, }, }, }) @@ -427,7 +427,7 @@ const testAccCloudStackLimits_domain_limit = ` resource "cloudstack_limits" "domain_limit" { type = "volume" max = 50 - domainid = cloudstack_domain.test_domain.id + domain_id = cloudstack_domain.test_domain.id } ` @@ -447,21 +447,21 @@ resource "cloudstack_limits" "account_limit" { type = "snapshot" max = 100 account = cloudstack_account.test_account.username - domainid = cloudstack_domain.test_domain.id + domain_id = cloudstack_domain.test_domain.id } ` // const testAccCloudStackLimits_project = ` #TODO: Need project imported before this will do anything // resource "cloudstack_project" "test_project" { -// name = "test-project-limits" +// name = "test-project-limits" // display_text = "Test Project for Limits" -// domainid = cloudstack_domain.test_domain.id +// domain_id = cloudstack_domain.test_domain.id // } // // resource "cloudstack_limits" "project_limit" { // type = "primarystorage" // max = 1000 -// domainid = cloudstack_domain.test_domain.id +// domain_id = cloudstack_domain.test_domain.id // projectid = cloudstack_project.test_project.id // } // ` @@ -470,7 +470,7 @@ const testAccCloudStackLimits_unlimited = ` resource "cloudstack_limits" "unlimited" { type = "cpu" max = -1 - domainid = cloudstack_domain.test_domain.id + domain_id = cloudstack_domain.test_domain.id } ` @@ -478,7 +478,7 @@ const testAccCloudStackLimits_stringType = ` resource "cloudstack_limits" "string_type" { type = "network" max = 30 - domainid = cloudstack_domain.test_domain.id + domain_id = cloudstack_domain.test_domain.id } ` @@ -486,7 +486,7 @@ const testAccCloudStackLimits_ip = ` resource "cloudstack_limits" "ip_limit" { type = "ip" max = 25 - domainid = cloudstack_domain.test_domain.id + domain_id = cloudstack_domain.test_domain.id } ` @@ -494,7 +494,7 @@ const testAccCloudStackLimits_template = ` resource "cloudstack_limits" "template_limit" { type = "template" max = 40 - domainid = cloudstack_domain.test_domain.id + domain_id = cloudstack_domain.test_domain.id } ` @@ -502,7 +502,7 @@ const testAccCloudStackLimits_projectType = ` resource "cloudstack_limits" "project_type_limit" { type = "project" max = 15 - domainid = cloudstack_domain.test_domain.id + domain_id = cloudstack_domain.test_domain.id } ` @@ -510,7 +510,7 @@ const testAccCloudStackLimits_vpc = ` resource "cloudstack_limits" "vpc_limit" { type = "vpc" max = 10 - domainid = cloudstack_domain.test_domain.id + domain_id = cloudstack_domain.test_domain.id } ` @@ -518,7 +518,7 @@ const testAccCloudStackLimits_memory = ` resource "cloudstack_limits" "memory_limit" { type = "memory" max = 8192 - domainid = cloudstack_domain.test_domain.id + domain_id = cloudstack_domain.test_domain.id } ` @@ -526,7 +526,7 @@ const testAccCloudStackLimits_zero = ` resource "cloudstack_limits" "zero_limit" { type = "instance" max = 0 - domainid = cloudstack_domain.test_domain.id + domain_id = cloudstack_domain.test_domain.id } ` @@ -534,7 +534,7 @@ const testAccCloudStackLimits_secondarystorage = ` resource "cloudstack_limits" "secondarystorage_limit" { type = "secondarystorage" max = 2000 - domainid = cloudstack_domain.test_domain.id + domain_id = cloudstack_domain.test_domain.id } ` @@ -542,7 +542,7 @@ const testAccCloudStackLimits_zeroToPositive = ` resource "cloudstack_limits" "zero_limit" { type = "instance" max = 5 - domainid = cloudstack_domain.test_domain.id + domain_id = cloudstack_domain.test_domain.id } ` @@ -550,7 +550,7 @@ const testAccCloudStackLimits_positiveValue = ` resource "cloudstack_limits" "positive_limit" { type = "instance" max = 15 - domainid = cloudstack_domain.test_domain.id + domain_id = cloudstack_domain.test_domain.id } ` @@ -558,7 +558,7 @@ const testAccCloudStackLimits_positiveToZero = ` resource "cloudstack_limits" "positive_limit" { type = "instance" max = 0 - domainid = cloudstack_domain.test_domain.id + domain_id = cloudstack_domain.test_domain.id } ` @@ -566,7 +566,7 @@ const testAccCloudStackLimits_positiveToUnlimited = ` resource "cloudstack_limits" "positive_limit" { type = "instance" max = -1 - domainid = cloudstack_domain.test_domain.id + domain_id = cloudstack_domain.test_domain.id } ` @@ -574,6 +574,6 @@ const testAccCloudStackLimits_unlimitedToZero = ` resource "cloudstack_limits" "unlimited" { type = "cpu" max = 0 - domainid = cloudstack_domain.test_domain.id + domain_id = cloudstack_domain.test_domain.id } `