Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 76 additions & 9 deletions cloudstack/resource_cloudstack_security_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,20 @@ func resourceCloudStackSecurityGroup() *schema.Resource {
ForceNew: true,
},

"project": {
"account": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},

"domainid": {
Type: schema.TypeString,
Optional: true,
Computed: true,
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.

Type: schema.TypeString,
Optional: true,
Computed: true,
Expand All @@ -66,6 +79,18 @@ func resourceCloudStackSecurityGroupCreate(d *schema.ResourceData, meta interfac

name := d.Get("name").(string)

// Validate that account is used with domainid
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)
}
Comment on lines +83 to +92
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.

// Create a new parameter struct
p := cs.SecurityGroup.NewCreateSecurityGroupParams(name)

Expand All @@ -76,9 +101,27 @@ func resourceCloudStackSecurityGroupCreate(d *schema.ResourceData, meta interfac
p.SetDescription(name)
}

// If there is a project supplied, we retrieve and set the project id
if err := setProjectid(p, cs, d); err != nil {
return err
// Set the account if provided
if account, ok := d.GetOk("account"); ok {
p.SetAccount(account.(string))
}

// If there is a domainid supplied, retrieve and set the domain id (supports both names and IDs)
if domain, ok := d.GetOk("domainid"); ok {
domainID, err := retrieveID(cs, "domain", domain.(string))
if err != nil {
return err.Error()
}
p.SetDomainid(domainID)
}

// If there is a projectid supplied, retrieve and set the project id (supports both names and IDs)
if project, ok := d.GetOk("projectid"); ok {
projectID, err := retrieveID(cs, "project", project.(string))
if err != nil {
return err.Error()
}
p.SetProjectid(projectID)
}

r, err := cs.SecurityGroup.CreateSecurityGroup(p)
Expand All @@ -97,7 +140,7 @@ func resourceCloudStackSecurityGroupRead(d *schema.ResourceData, meta interface{
// 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)),
)
Comment on lines 140 to 144
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.
if err != nil {
if count == 0 {
Expand All @@ -113,7 +156,13 @@ func resourceCloudStackSecurityGroupRead(d *schema.ResourceData, meta interface{
d.Set("name", sg.Name)
d.Set("description", sg.Description)

setValueOrID(d, "project", sg.Project, sg.Projectid)
// Only set account if it was explicitly configured
if _, ok := d.GetOk("account"); ok {
d.Set("account", sg.Account)
}

setValueOrID(d, "domainid", sg.Domain, sg.Domainid)
setValueOrID(d, "projectid", sg.Project, sg.Projectid)

return nil
}
Expand All @@ -125,9 +174,27 @@ func resourceCloudStackSecurityGroupDelete(d *schema.ResourceData, meta interfac
p := cs.SecurityGroup.NewDeleteSecurityGroupParams()
p.SetId(d.Id())

// If there is a project supplied, we retrieve and set the project id
if err := setProjectid(p, cs, d); err != nil {
return err
// Set the account if provided
if account, ok := d.GetOk("account"); ok {
p.SetAccount(account.(string))
}

// If there is a domainid supplied, retrieve and set the domain id (supports both names and IDs)
if domain, ok := d.GetOk("domainid"); ok {
domainID, err := retrieveID(cs, "domain", domain.(string))
if err != nil {
return err.Error()
}
p.SetDomainid(domainID)
}

// If there is a projectid supplied, retrieve and set the project id (supports both names and IDs)
if project, ok := d.GetOk("projectid"); ok {
projectID, err := retrieveID(cs, "project", project.(string))
if err != nil {
return err.Error()
}
p.SetProjectid(projectID)
}

// Delete the security group
Expand Down
68 changes: 68 additions & 0 deletions cloudstack/resource_cloudstack_security_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,47 @@ func TestAccCloudStackSecurityGroup_basic(t *testing.T) {
})
}

func TestAccCloudStackSecurityGroup_project(t *testing.T) {
var sg cloudstack.SecurityGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckCloudStackSecurityGroupDestroy,
Steps: []resource.TestStep{
{
Config: testAccCloudStackSecurityGroup_project,
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudStackSecurityGroupExists(
"cloudstack_security_group.foo", &sg),
resource.TestCheckResourceAttrPair(
"cloudstack_security_group.foo", "projectid",
"cloudstack_project.test", "id"),
),
},
},
})
}

func TestAccCloudStackSecurityGroup_account(t *testing.T) {
var sg cloudstack.SecurityGroup
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckCloudStackSecurityGroupDestroy,
Steps: []resource.TestStep{
{
Config: testAccCloudStackSecurityGroup_account,
Check: resource.ComposeTestCheckFunc(
testAccCheckCloudStackSecurityGroupExists(
"cloudstack_security_group.foo", &sg),
resource.TestCheckResourceAttr(
"cloudstack_security_group.foo", "account", "admin"),
),
},
},
})
}

func TestAccCloudStackSecurityGroup_import(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Expand Down Expand Up @@ -136,3 +177,30 @@ resource "cloudstack_security_group" "foo" {
name = "terraform-security-group"
description = "terraform-security-group-text"
}`

const testAccCloudStackSecurityGroup_project = `
resource "cloudstack_project" "test" {
name = "terraform-security-group-test-project"
displaytext = "Terraform Security Group Test Project"
}

resource "cloudstack_security_group" "foo" {
name = "terraform-security-group-project"
description = "terraform-security-group-project-text"
projectid = cloudstack_project.test.id
}`

const testAccCloudStackSecurityGroup_account = `
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 = data.cloudstack_domain.root.id
}`
13 changes: 13 additions & 0 deletions cloudstack/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,19 @@ func setProjectid(p cloudstack.ProjectIDSetter, cs *cloudstack.CloudStackClient,
return nil
}

// 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
}
Comment on lines +165 to +176
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.

// importStatePassthrough is a generic importer with project support.
func importStatePassthrough(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
// Try to split the ID to extract the optional project name.
Expand Down
43 changes: 42 additions & 1 deletion website/docs/r/security_group.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,39 @@ resource "cloudstack_security_group" "default" {
}
```

### With Account and Domain

```hcl
data "cloudstack_domain" "my_domain" {
filter {
name = "name"
value = "ROOT"
}
}

resource "cloudstack_security_group" "account_sg" {
name = "allow_web"
description = "Allow access to HTTP and HTTPS"
account = "my-account"
domainid = data.cloudstack_domain.my_domain.id
}
```

### With Project

```hcl
resource "cloudstack_project" "my_project" {
name = "my-project"
displaytext = "My Project"
}

resource "cloudstack_security_group" "project_sg" {
name = "allow_web"
description = "Allow access to HTTP and HTTPS"
projectid = cloudstack_project.my_project.id
}
```

## Argument Reference

The following arguments are supported:
Expand All @@ -29,9 +62,17 @@ The following arguments are supported:
* `description` - (Optional) The description of the security group. Changing
this forces a new resource to be created.

* `project` - (Optional) The name or ID of the project to create this security
* `account` - (Optional) The account name to create the security group for.
Must be used with `domainid`. Cannot be used with `projectid`. Changing this
forces a new resource to be created.

* `domainid` - (Optional) The name or ID of the domain to create this security
group in. Changing this forces a new resource to be created.

* `projectid` - (Optional) The name or ID of the project to create this security
group in. Cannot be used with `account`. Changing this forces a new
resource to be created.

## Attributes Reference

The following attributes are exported:
Expand Down
Loading