Skip to content

feat: Update resource_cloudstack_security_group to allow domainid or projectid#262

Open
ianc769 wants to merge 2 commits intoapache:mainfrom
ianc769:feature/security-group-domain
Open

feat: Update resource_cloudstack_security_group to allow domainid or projectid#262
ianc769 wants to merge 2 commits intoapache:mainfrom
ianc769:feature/security-group-domain

Conversation

@ianc769
Copy link
Contributor

@ianc769 ianc769 commented Nov 6, 2025

Update resource_cloudstack_security_group to allow domainid or projectid

https://cloudstack.apache.org/api/apidocs-4.21/apis/createSecurityGroup.html

This is breaking since I'm renaming project to projectid (just more descriptive as to what is expected in this field and matches the actual api call)

With the code:

data "cloudstack_domain" "root" {
  filter {
    name  = "name"
    value = "ROOT"
  }
}

resource "cloudstack_security_group" "foo" {
  name        = "terraform-security-group-account"
  description = "terraform-security-group-account-text"
  account     = "admin"
  domainid    = "ROOT"
}

resource "cloudstack_security_group" "foo2" {
  name        = "terraform-security-group-project"
  description = "terraform-security-group-project-text"
  projectid   = "fdaa5a01-bc8c-4e50-a0ea-9135a1c54fb3"
}
12:15:41.534 STDOUT terraform: Terraform used the selected providers to generate the following execution
12:15:41.534 STDOUT terraform: plan. Resource actions are indicated with the following symbols:
12:15:41.535 STDOUT terraform:   + create
12:15:41.535 STDOUT terraform:   ~ update in-place
12:15:41.535 STDOUT terraform: Terraform will perform the following actions:
12:15:41.535 STDOUT terraform:   # cloudstack_security_group.foo will be created
12:15:41.535 STDOUT terraform:   + resource "cloudstack_security_group" "foo" {
12:15:41.535 STDOUT terraform:       + account     = "admin"
12:15:41.535 STDOUT terraform:       + description = "terraform-security-group-account-text"
12:15:41.535 STDOUT terraform:       + domainid    = "03ff694f-a2d5-11f0-95ca-52540047021e"
12:15:41.535 STDOUT terraform:       + id          = (known after apply)
12:15:41.535 STDOUT terraform:       + name        = "terraform-security-group-account"
12:15:41.535 STDOUT terraform:       + projectid   = (known after apply)
12:15:41.535 STDOUT terraform:     }
12:15:41.535 STDOUT terraform:   # cloudstack_security_group.foo2 will be created
12:15:41.535 STDOUT terraform:   + resource "cloudstack_security_group" "foo2" {
12:15:41.535 STDOUT terraform:       + description = "terraform-security-group-project-text"
12:15:41.535 STDOUT terraform:       + domainid    = (known after apply)
12:15:41.535 STDOUT terraform:       + id          = (known after apply)
12:15:41.535 STDOUT terraform:       + name        = "terraform-security-group-project"
12:15:41.535 STDOUT terraform:       + projectid   = "fdaa5a01-bc8c-4e50-a0ea-9135a1c54fb3"
12:15:41.535 STDOUT terraform:     }

12:15:57.912 STDOUT terraform: Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

ForceNew: true,
},

"projectid": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"projectid": {
"project": {

to maintain consistency with other resources.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the cloudstack_security_group Terraform resource to support creating security groups either in an account/domain context (account + domainid) or in a project context (projectid), aligning with the CloudStack createSecurityGroup API parameters.

Changes:

  • Rename project argument to projectid, and add account + domainid support in the resource implementation.
  • Extend docs with examples for account/domain and project usage, and update argument reference accordingly.
  • Add new acceptance tests for account- and project-scoped security group creation.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
cloudstack/resource_cloudstack_security_group.go Adds account, domainid, projectid schema/behavior and updates read/delete logic to use projectid.
cloudstack/resources.go Adds a setDomainid helper and retains generic import logic (project-prefix parsing).
cloudstack/resource_cloudstack_security_group_test.go Adds acceptance tests for project and account variants.
website/docs/r/security_group.html.markdown Adds usage examples and updates argument docs to account/domainid/projectid.
Comments suppressed due to low confidence (3)

cloudstack/resource_cloudstack_security_group_test.go:107

  • The docs explicitly describe importing security groups with a project prefix (<project-name>/<id>), but the acceptance tests only cover non-project import. Adding an import-in-project acceptance test (similar to TestAccCloudStackInstance_importProject) would prevent regressions around the project -> projectid rename and importer behavior.
func TestAccCloudStackSecurityGroup_import(t *testing.T) {
	resource.Test(t, resource.TestCase{
		PreCheck:     func() { testAccPreCheck(t) },
		Providers:    testAccProviders,
		CheckDestroy: testAccCheckCloudStackSecurityGroupDestroy,
		Steps: []resource.TestStep{
			{
				Config: testAccCloudStackSecurityGroup_basic,
			},

			{
				ResourceName:      "cloudstack_security_group.foo",
				ImportState:       true,
				ImportStateVerify: true,
			},
		},
	})

cloudstack/resources.go:184

  • importStatePassthrough sets the project prefix into the project attribute. After renaming resources to use projectid (e.g., security group), imports with a project/ID format will no longer populate the correct field. Consider either: (1) adding a new importer for resources that use projectid, or (2) setting both project and projectid here (ignoring Set() errors when the key doesn't exist).
func importStatePassthrough(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
	// Try to split the ID to extract the optional project name.
	s := strings.SplitN(d.Id(), "/", 2)
	if len(s) == 2 {
		d.Set("project", s[0])
	}

cloudstack/resource_cloudstack_security_group.go:72

  • Schema-level validation is missing for the new argument combinations. Since account and projectid are mutually exclusive and account requires domainid, consider encoding this via schema constraints (e.g., ConflictsWith on account vs projectid, and RequiredWith/CustomizeDiff for account -> domainid). This will surface errors at plan time instead of failing during apply.
			"account": {
				Type:     schema.TypeString,
				Optional: true,
				ForceNew: true,
			},

			"domainid": {
				Type:     schema.TypeString,
				Optional: true,
				Computed: true,
				ForceNew: true,
			},

			"projectid": {
				Type:     schema.TypeString,
				Optional: true,
				Computed: true,
				ForceNew: true,
			},

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +165 to +176
// If there is a domain supplied, we retrieve and set the domain id
func setDomainid(p cloudstack.DomainIDSetter, cs *cloudstack.CloudStackClient, d *schema.ResourceData) error {
if domain, ok := d.GetOk("domain"); ok {
domainid, e := retrieveID(cs, "domain", domain.(string))
if e != nil {
return e.Error()
}
p.SetDomainid(domainid)
}

return nil
}
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setDomainid is introduced but not used anywhere in the current codebase (search only finds its definition). If this helper is intended for upcoming changes, consider adding the call sites in this PR; otherwise, it’s dead code that increases maintenance surface and can be removed until needed.

Copilot uses AI. Check for mistakes.
Comment on lines 140 to 144
// Get the security group details
sg, count, err := cs.SecurityGroup.GetSecurityGroupByID(
d.Id(),
cloudstack.WithProject(d.Get("project").(string)),
cloudstack.WithProject(d.Get("projectid").(string)),
)
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Import uses importStatePassthrough, which populates the state attribute project, but this resource now reads project context from projectid (WithProject(d.Get("projectid"))). Importing with a project prefix (e.g., my-project/<id>) will therefore lose the project scope and may fail to read/delete the SG. Consider switching this resource to a dedicated importer that sets projectid from the prefix (and optionally also supports legacy project for backward compatibility).

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +92
if account, ok := d.GetOk("account"); ok {
if _, domainOk := d.GetOk("domainid"); !domainOk {
return fmt.Errorf("account parameter requires domainid to be set")
}
// Account and projectid are mutually exclusive
if _, projectOk := d.GetOk("projectid"); projectOk {
return fmt.Errorf("account and projectid parameters are mutually exclusive")
}
log.Printf("[DEBUG] Creating security group %s for account %s", name, account)
}
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

account from d.GetOk("account") is an interface{}; logging it with %s will produce %!s(interface {}=...) in logs. Cast to string (or use %v) before logging.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants