diff --git a/cloudstack/data_source_cloudstack_security_group.go b/cloudstack/data_source_cloudstack_security_group.go new file mode 100644 index 00000000..9a034b79 --- /dev/null +++ b/cloudstack/data_source_cloudstack_security_group.go @@ -0,0 +1,142 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "encoding/json" + "fmt" + "log" + "regexp" + "strings" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceCloudstackSecurityGroup() *schema.Resource { + return &schema.Resource{ + Read: dataSourceCloudstackSecurityGroupRead, + Schema: map[string]*schema.Schema{ + "filter": dataSourceFiltersSchema(), + + //Computed values + "name": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "project": { + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + }, + } +} + +func dataSourceCloudstackSecurityGroupRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + p := cs.SecurityGroup.NewListSecurityGroupsParams() + + // If there is a project supplied, we retrieve and set the project id + if err := setProjectid(p, cs, d); err != nil { + return err + } + + securityGroups, err := cs.SecurityGroup.ListSecurityGroups(p) + if err != nil { + return fmt.Errorf("failed to list security groups: %s", err) + } + + filters := d.Get("filter").(*schema.Set) + var matches []*cloudstack.SecurityGroup + + for _, securityGroup := range securityGroups.SecurityGroups { + match, err := applySecurityGroupFilters(securityGroup, filters) + if err != nil { + return err + } + if match { + matches = append(matches, securityGroup) + } + } + + if len(matches) == 0 { + return fmt.Errorf("no security group matches the specified filters") + } + if len(matches) > 1 { + return fmt.Errorf("multiple security groups match the specified filters") + } + + securityGroup := matches[0] + log.Printf("[DEBUG] Selected security group: %s", securityGroup.Name) + + return securityGroupDescriptionAttributes(d, securityGroup) +} + +func securityGroupDescriptionAttributes(d *schema.ResourceData, securityGroup *cloudstack.SecurityGroup) error { + d.SetId(securityGroup.Id) + + if err := d.Set("name", securityGroup.Name); err != nil { + return fmt.Errorf("failed to set security group name: %s", err) + } + if err := d.Set("description", securityGroup.Description); err != nil { + return fmt.Errorf("failed to set security group description: %s", err) + } + + setValueOrID(d, "project", securityGroup.Project, securityGroup.Projectid) + + return nil +} + +func applySecurityGroupFilters(securityGroup *cloudstack.SecurityGroup, filters *schema.Set) (bool, error) { + securityGroupJSON, err := json.Marshal(securityGroup) + if err != nil { + return false, fmt.Errorf("failed to encode security group: %s", err) + } + + var fields map[string]interface{} + if err := json.Unmarshal(securityGroupJSON, &fields); err != nil { + return false, fmt.Errorf("failed to decode security group: %s", err) + } + + for _, filter := range filters.List() { + values := filter.(map[string]interface{}) + pattern, err := regexp.Compile(values["value"].(string)) + if err != nil { + return false, fmt.Errorf("invalid regex: %s", err) + } + + name := strings.ReplaceAll(values["name"].(string), "_", "") + value, ok := fields[name] + if !ok { + return false, fmt.Errorf("field %q does not exist in security group", values["name"].(string)) + } + + if !pattern.MatchString(fmt.Sprint(value)) { + return false, nil + } + } + + return true, nil +} diff --git a/cloudstack/data_source_cloudstack_security_group_test.go b/cloudstack/data_source_cloudstack_security_group_test.go new file mode 100644 index 00000000..6979578a --- /dev/null +++ b/cloudstack/data_source_cloudstack_security_group_test.go @@ -0,0 +1,68 @@ +// +// 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-sdk/v2/helper/id" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccSecurityGroupDataSource_basic(t *testing.T) { + resourceName := "cloudstack_security_group.security-group-resource" + dataSourceName := "data.cloudstack_security_group.security-group-data-source" + securityGroupName := "terraform-security-group-data-source-" + id.UniqueId() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testSecurityGroupDataSourceConfig(securityGroupName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "id", resourceName, "id"), + resource.TestCheckResourceAttrPair(dataSourceName, "name", resourceName, "name"), + resource.TestCheckResourceAttrPair(dataSourceName, "description", resourceName, "description"), + ), + }, + }, + }) +} + +func testSecurityGroupDataSourceConfig(name string) string { + return fmt.Sprintf(` +resource "cloudstack_security_group" "security-group-resource" { + name = "%[1]s" + description = "Security group data source acceptance test" +} + +data "cloudstack_security_group" "security-group-data-source" { + filter { + name = "name" + value = "^%[1]s$" + } + depends_on = [cloudstack_security_group.security-group-resource] +} +`, + name, + ) +} diff --git a/cloudstack/provider.go b/cloudstack/provider.go index 88756f69..dffb67af 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -107,6 +107,7 @@ func Provider() *schema.Provider { "cloudstack_quota_tariff": dataSourceCloudStackQuotaTariff(), "cloudstack_user_data": dataSourceCloudstackUserData(), "cloudstack_kubernetes_cluster_config": dataSourceCloudstackKubernetesClusterConfig(), + "cloudstack_security_group": dataSourceCloudstackSecurityGroup(), }, ResourcesMap: map[string]*schema.Resource{ diff --git a/website/docs/README.md b/website/docs/README.md index 2fe8dd3f..c99234a2 100644 --- a/website/docs/README.md +++ b/website/docs/README.md @@ -62,6 +62,7 @@ The following arguments are supported: - [instance](./d/instance.html.markdown) - [ipaddress](./d/ipaddress.html.markdown) - [network_offering](./d/network_offering.html.markdown) +- [security_group](./d/security_group.html.markdown) - [service_offering](./d/service_offering.html.markdown) - [ssh_keypair](./d/ssh_keypair.html.markdown) - [template](./d/template.html.markdown) diff --git a/website/docs/d/security_group.html.markdown b/website/docs/d/security_group.html.markdown new file mode 100644 index 00000000..a31d11a1 --- /dev/null +++ b/website/docs/d/security_group.html.markdown @@ -0,0 +1,41 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_security_group" +description: |- + Gets information about a security group. +--- + +# cloudstack_security_group + +Use this data source to get information about a security group for use in other resources. + +## Example Usage + +```hcl +data "cloudstack_security_group" "web" { + project = "my-project" + + filter { + name = "name" + value = "^web-servers$" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `filter` - (Required) One or more name/regular-expression pairs used to select the security group. Filter names use snake case, for example `name`, `description`, or `project`. +* `project` - (Optional) The name or ID of the project containing the security group. + +The filters must identify exactly one security group. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the security group. +* `name` - The name of the security group. +* `description` - The description of the security group. +* `project` - The name of the project containing the security group, or its ID when the data source was configured with a project ID.