From 105802b8e262b21b8db67c18795fd480f4c5cbc2 Mon Sep 17 00:00:00 2001 From: allium Date: Tue, 12 Nov 2024 16:22:48 +0800 Subject: [PATCH] feat(waf): support new datasource to query certificates (#5840) --- docs/data-sources/waf_certificates.md | 83 +++++++ huaweicloud/provider.go | 1 + ...ource_huaweicloud_waf_certificates_test.go | 119 +++++++++ ...ata_source_huaweicloud_waf_certificates.go | 235 ++++++++++++++++++ 4 files changed, 438 insertions(+) create mode 100644 docs/data-sources/waf_certificates.md create mode 100644 huaweicloud/services/acceptance/waf/data_source_huaweicloud_waf_certificates_test.go create mode 100644 huaweicloud/services/waf/data_source_huaweicloud_waf_certificates.go diff --git a/docs/data-sources/waf_certificates.md b/docs/data-sources/waf_certificates.md new file mode 100644 index 0000000000..c5a6a064ac --- /dev/null +++ b/docs/data-sources/waf_certificates.md @@ -0,0 +1,83 @@ +--- +subcategory: "Web Application Firewall (WAF)" +layout: "huaweicloud" +page_title: "HuaweiCloud: huaweicloud_waf_certificates" +description: |- + Use this data source to get a list of WAF certificates within HuaweiCloud. +--- + +# huaweicloud_waf_certificates + +Use this data source to get a list of WAF certificates within HuaweiCloud. + +## Example Usage + +```hcl +variable enterprise_project_id {} + +data "huaweicloud_waf_certificates" "test" { + enterprise_project_id = var.enterprise_project_id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Optional, String) Specifies the region in which to query the data source. If omitted, the provider-level + region will be used. + +* `enterprise_project_id` - (Optional, String) Specifies the enterprise project ID of WAF certificate. + For enterprise users, if omitted, default enterprise project will be used. + +* `name` - (Optional, String) Specifies the name of certificate. The value is case-sensitive and supports fuzzy matching. + +* `host` - (Optional, Bool) Specifies whether to obtain the domain name for which the certificate is used. + + **true**: Obtain the certificates that have been used for domain names. + + **false**: Obtain the certificates that have not been used for any domain names. + + Defaults to **false**. + +* `expiration_status` - (Optional, String) Specifies the certificate expiration status. The options are as follows: + + `0`: Not expired; + + `1`: Expired; + + `2`: Expired soon (The certificate will expire in one month.) + + -> If this field is not configured, all certificates that meet the expired status will be found. + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The data source ID. + +* `certificates` - The certificate list. + The [certificates](#certificates_struct) structure is documented below. + + +The `certificates` block supports: + +* `id` - The certificate ID. + +* `name` - The certificate name. + +* `expiration_status` - The certificate expiration status. + +* `created_at` - The time when the certificate was uploaded, in RFC3339 format. + +* `expired_at` - The time when the certificate expires, in RFC3339 format. + +* `bind_host` - The domain information bound to the certificate. + The [bind_host](#items_bind_host_struct) structure is documented below. + + +The `bind_host` block supports: + +* `id` - The domain ID. + +* `domain` - The domain name. + +* `mode` - The special domain pattern. + +* `waf_type` - The deployment mode of WAF instance that is used for the domain name. The value can be **cloud** for + cloud WAF or **premium** for dedicated WAF instances. diff --git a/huaweicloud/provider.go b/huaweicloud/provider.go index f3ee7cb8dd..f4abf1da12 100644 --- a/huaweicloud/provider.go +++ b/huaweicloud/provider.go @@ -1106,6 +1106,7 @@ func Provider() *schema.Provider { "huaweicloud_waf_rules_precise_protection": waf.DataSourceWafRulesPreciseProtection(), "huaweicloud_waf_rules_web_tamper_protection": waf.DataSourceWafRulesWebTamperProtection(), "huaweicloud_waf_source_ips": waf.DataSourceWafSourceIps(), + "huaweicloud_waf_certificates": waf.DataSourceWafCertificates(), "huaweicloud_dws_alarm_subscriptions": dws.DataSourceAlarmSubscriptions(), "huaweicloud_dws_availability_zones": dws.DataSourceDwsAvailabilityZones(), diff --git a/huaweicloud/services/acceptance/waf/data_source_huaweicloud_waf_certificates_test.go b/huaweicloud/services/acceptance/waf/data_source_huaweicloud_waf_certificates_test.go new file mode 100644 index 0000000000..0c4a35c505 --- /dev/null +++ b/huaweicloud/services/acceptance/waf/data_source_huaweicloud_waf_certificates_test.go @@ -0,0 +1,119 @@ +package waf + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/acceptance" +) + +func TestAccDataSourceWafCertificates_basic(t *testing.T) { + var ( + dataSourceName = acceptance.RandomAccResourceName() + + dataSource = "data.huaweicloud_waf_certificates.test" + dc = acceptance.InitDataSourceCheck(dataSource) + + byName = "data.huaweicloud_waf_certificates.name_filter" + dcByName = acceptance.InitDataSourceCheck(byName) + + byHost = "data.huaweicloud_waf_certificates.host_filter" + dcByHost = acceptance.InitDataSourceCheck(byHost) + + byExpirationStatus = "data.huaweicloud_waf_certificates.expiration_status_filter" + dcByExpirationStatus = acceptance.InitDataSourceCheck(byExpirationStatus) + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acceptance.TestAccPreCheck(t) + }, + ProviderFactories: acceptance.TestAccProviderFactories, + Steps: []resource.TestStep{ + { + Config: testDataSourceDataSourceWafCertificates_basic(dataSourceName), + Check: resource.ComposeTestCheckFunc( + dc.CheckResourceExists(), + resource.TestCheckResourceAttrSet(dataSource, "certificates.0.id"), + resource.TestCheckResourceAttrSet(dataSource, "certificates.0.name"), + resource.TestCheckResourceAttrSet(dataSource, "certificates.0.created_at"), + + dcByName.CheckResourceExists(), + resource.TestCheckOutput("name_filter_is_useful", "true"), + + dcByHost.CheckResourceExists(), + resource.TestCheckOutput("host_filter_is_useful", "true"), + + dcByExpirationStatus.CheckResourceExists(), + resource.TestCheckOutput("expiration_status_filter_is_useful", "true"), + ), + }, + }, + }) +} + +func testDataSourceDataSourceWafCertificates_basic(name string) string { + return fmt.Sprintf(` +%[1]s + +data "huaweicloud_waf_certificates" "test" { + enterprise_project_id = "%[2]s" + + depends_on = [ + huaweicloud_waf_certificate.test + ] +} + +# Filter by name +locals { + name = data.huaweicloud_waf_certificates.test.certificates.0.name +} + +data "huaweicloud_waf_certificates" "name_filter" { + enterprise_project_id = "%[2]s" + name = local.name +} + +locals { + name_filter_result = [ + for v in data.huaweicloud_waf_certificates.name_filter.certificates[*].name : v == local.name + ] +} + +output "name_filter_is_useful" { + value = length(local.name_filter_result) > 0 && alltrue(local.name_filter_result) +} + +# Filter by host +data "huaweicloud_waf_certificates" "host_filter" { + enterprise_project_id = "%[2]s" + host = true +} + +output "host_filter_is_useful" { + value = length(data.huaweicloud_waf_certificates.host_filter.certificates.0.bind_host) > 0 +} + +# Filter by expiration_status +locals { + expiration_status = data.huaweicloud_waf_certificates.test.certificates.0.expiration_status +} + +data "huaweicloud_waf_certificates" "expiration_status_filter" { + enterprise_project_id = "%[2]s" + expiration_status = local.expiration_status +} + +locals { + expiration_status_filter_result = [ + for v in data.huaweicloud_waf_certificates.expiration_status_filter.certificates[*].expiration_status : v == local.expiration_status + ] +} + +output "expiration_status_filter_is_useful" { + value = length(local.expiration_status_filter_result) > 0 && alltrue(local.expiration_status_filter_result) +} +`, testAccWafCertificate_basic(name, generateCertificateBody()), acceptance.HW_ENTERPRISE_PROJECT_ID_TEST) +} diff --git a/huaweicloud/services/waf/data_source_huaweicloud_waf_certificates.go b/huaweicloud/services/waf/data_source_huaweicloud_waf_certificates.go new file mode 100644 index 0000000000..eae00a6c15 --- /dev/null +++ b/huaweicloud/services/waf/data_source_huaweicloud_waf_certificates.go @@ -0,0 +1,235 @@ +package waf + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/chnsz/golangsdk" + + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/utils" +) + +// @API WAF GET /v1/{project_id}/waf/certificate +func DataSourceWafCertificates() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceWafCertificatesRead, + + Schema: map[string]*schema.Schema{ + "region": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: `Specifies the region in which to query the data source. If omitted, the provider-level region will be used.`, + }, + "enterprise_project_id": { + Type: schema.TypeString, + Optional: true, + Description: `Specifies the enterprise project ID of WAF certificate.`, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Description: `Specifies the name of certificate.`, + }, + "host": { + Type: schema.TypeBool, + Optional: true, + Description: `Specifies whether to obtain the domain name for which the certificate is used.`, + }, + "expiration_status": { + Type: schema.TypeString, + Optional: true, + Description: `Specifies the certificate expiration status.`, + }, + "certificates": { + Type: schema.TypeList, + Computed: true, + Description: `The certificate list.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: `The certificate ID.`, + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: `The certificate name.`, + }, + "expiration_status": { + Type: schema.TypeString, + Computed: true, + Description: `The certificate expiration status.`, + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: `The time when the certificate was uploaded.`, + }, + "expired_at": { + Type: schema.TypeString, + Computed: true, + Description: `The time when the certificate expires.`, + }, + "bind_host": { + Type: schema.TypeList, + Computed: true, + Description: `The domain information bound to the certificate.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: `The domain ID.`, + }, + "domain": { + Type: schema.TypeString, + Computed: true, + Description: `The domain name.`, + }, + "mode": { + Type: schema.TypeString, + Computed: true, + Description: `The special domain pattern.`, + }, + "waf_type": { + Type: schema.TypeString, + Computed: true, + Description: `The deployment mode of WAF instance that is used for the domain name.`, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func buildWafCertificatesQueryParams(d *schema.ResourceData, cfg *config.Config) string { + res := "?pagesize=100" + if epsId := cfg.GetEnterpriseProjectID(d); epsId != "" { + res = fmt.Sprintf("%s&enterprise_project_id=%s", res, epsId) + } + if v, ok := d.GetOk("name"); ok { + res = fmt.Sprintf("%s&name=%v", res, v) + } + // The type of this field is `boolean`, the default value is false, the default value is ignored. + if v, ok := d.GetOk("host"); ok { + res = fmt.Sprintf("%s&host=%v", res, v) + } + if v, ok := d.GetOk("expiration_status"); ok { + res = fmt.Sprintf("%s&exp_status=%v", res, v) + } + + return res +} + +func dataSourceWafCertificatesRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + mErr *multierror.Error + httpUrl = "v1/{project_id}/waf/certificate" + product = "waf" + totalCertificates []interface{} + currentPage = 1 + ) + client, err := cfg.NewServiceClient(product, region) + if err != nil { + return diag.Errorf("error creating WAF client: %s", err) + } + + requestPath := client.Endpoint + httpUrl + requestPath = strings.ReplaceAll(requestPath, "{project_id}", client.ProjectID) + requestPath += buildWafCertificatesQueryParams(d, cfg) + requestOpt := golangsdk.RequestOpts{ + MoreHeaders: map[string]string{ + "Content-Type": "application/json;charset=utf8", + }, + KeepResponseBody: true, + } + + for { + requestPathWithPage := fmt.Sprintf("%s&page=%d", requestPath, currentPage) + resp, err := client.Request("GET", requestPathWithPage, &requestOpt) + if err != nil { + return diag.Errorf("error retrieving WAF certificates: %s", err) + } + + respBody, err := utils.FlattenResponse(resp) + if err != nil { + return diag.FromErr(err) + } + + certificatesResp := utils.PathSearch("items", respBody, make([]interface{}, 0)).([]interface{}) + if len(certificatesResp) == 0 { + break + } + + totalCertificates = append(totalCertificates, certificatesResp...) + currentPage++ + } + + dataSourceId, err := uuid.GenerateUUID() + if err != nil { + return diag.Errorf("unable to generate ID: %s", err) + } + d.SetId(dataSourceId) + + mErr = multierror.Append( + mErr, + d.Set("region", region), + d.Set("certificates", flattenWafCertificatesResp(totalCertificates)), + ) + + return diag.FromErr(mErr.ErrorOrNil()) +} + +func flattenDataSourceCertificateExpirationStatus(certificateResp interface{}) string { + expStatus := utils.PathSearch("exp_status", certificateResp, nil) + if expStatus == nil { + return "" + } + return fmt.Sprintf("%v", expStatus) +} + +func flattenDataSourceCertificateBindHost(bindHosts []interface{}) []interface{} { + rst := make([]interface{}, 0, len(bindHosts)) + for _, v := range bindHosts { + rst = append(rst, map[string]interface{}{ + "id": utils.PathSearch("id", v, nil), + "domain": utils.PathSearch("hostname", v, nil), + "mode": utils.PathSearch("mode", v, nil), + "waf_type": utils.PathSearch("waf_type", v, nil), + }) + } + return rst +} + +func flattenWafCertificatesResp(totalCertificates []interface{}) []interface{} { + rst := make([]interface{}, 0, len(totalCertificates)) + for _, v := range totalCertificates { + expireTimestamp := utils.PathSearch("expire_time", v, float64(0)).(float64) + createTimestamp := utils.PathSearch("timestamp", v, float64(0)).(float64) + bindHosts := utils.PathSearch("bind_host", v, make([]interface{}, 0)).([]interface{}) + rst = append(rst, map[string]interface{}{ + "id": utils.PathSearch("id", v, nil), + "name": utils.PathSearch("name", v, nil), + "expiration_status": flattenDataSourceCertificateExpirationStatus(v), + "created_at": utils.FormatTimeStampRFC3339(int64(createTimestamp)/1000, true), + "expired_at": utils.FormatTimeStampRFC3339(int64(expireTimestamp)/1000, true), + "bind_host": flattenDataSourceCertificateBindHost(bindHosts), + }) + } + return rst +}