From 77efcaf8125f9d7f8c6e9b5f304a982e77653d4e Mon Sep 17 00:00:00 2001 From: Lance52259 Date: Sat, 29 Nov 2025 17:45:37 +0800 Subject: [PATCH] feat(cdn): add new resource to manage statistic subscription task --- .../cdn_statistic_subscription_task.md | 85 +++++ huaweicloud/provider.go | 1 + ...ud_cdn_statistic_subscription_task_test.go | 131 +++++++ ...eicloud_cdn_statistic_subscription_task.go | 336 ++++++++++++++++++ 4 files changed, 553 insertions(+) create mode 100644 docs/resources/cdn_statistic_subscription_task.md create mode 100644 huaweicloud/services/acceptance/cdn/resource_huaweicloud_cdn_statistic_subscription_task_test.go create mode 100644 huaweicloud/services/cdn/resource_huaweicloud_cdn_statistic_subscription_task.go diff --git a/docs/resources/cdn_statistic_subscription_task.md b/docs/resources/cdn_statistic_subscription_task.md new file mode 100644 index 00000000000..60541dc26f6 --- /dev/null +++ b/docs/resources/cdn_statistic_subscription_task.md @@ -0,0 +1,85 @@ +--- +subcategory: "Content Delivery Network (CDN)" +layout: "huaweicloud" +page_title: "HuaweiCloud: huaweicloud_cdn_statistic_subscription_task" +description: |- + Manages a CDN statistic subscription task resource within HuaweiCloud. +--- + +# huaweicloud_cdn_statistic_subscription_task + +Manages a CDN statistic subscription task resource within HuaweiCloud. + +## Example Usage + +```hcl +variable "task_name" {} + +resource "huaweicloud_cdn_statistic_subscription_task" "test" { + name = var.task_name + period_type = 0 + emails = "user1@example.com,user2@example.com" + domain_name = "www.example.com,www.example2.com" + report_type = "0,1,2,3,4,5" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required, String) Specifies the name of the subscription task. + The name can contain word characters, hyphens, and Chinese characters, with a maximum length of 32 characters. + +* `period_type` - (Required, Int) Specifies the type of the subscription task. + The valid values are as follows: + + **0**: Daily report + + **1**: Weekly report + + **2**: Monthly report + +* `emails` - (Required, String) Specifies the email addresses to receive the operation reports. + Multiple email addresses are separated by commas (,). + +* `domain_name` - (Required, String) Specifies the list of domain names to subscribe. + Multiple domain names are separated by commas (,). + If set to **all**, all domain names under the account will be subscribed. + +* `report_type` - (Required, String) Specifies the type of the operation report. + Multiple report types are separated by commas (,). + The valid values are as follows: + + **0**: Access area distribution + + **1**: Country distribution + + **2**: Carrier distribution + + **3**: Domain ranking (by traffic) + + **4**: Popular URLs (by traffic) + + **5**: Popular URLs (by request count) + + **6**: Popular Referer (by traffic) + + **7**: Popular Referer (by request count) + + **10**: Origin popular URLs (by traffic) + + **11**: Origin popular URLs (by request count) + + **13**: Popular UA (by traffic) + + **14**: Popular UA (by request count) + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The resource ID. + +* `create_time` - The creation time of the subscription task, in RFC3339 format. + +* `update_time` - The last update time of the subscription task, in RFC3339 format. + +## Import + +The subscription task can be imported using the `id` or `name`, e.g. + +```bash +$ terraform import huaweicloud_cdn_statistic_subscription_task.test +``` + +or + +```bash +$ terraform import huaweicloud_cdn_statistic_subscription_task.test +``` diff --git a/huaweicloud/provider.go b/huaweicloud/provider.go index 07c059067a7..286ac8ec4f2 100644 --- a/huaweicloud/provider.go +++ b/huaweicloud/provider.go @@ -2624,6 +2624,7 @@ func Provider() *schema.Provider { "huaweicloud_cdn_domain_template": cdn.ResourceDomainTemplate(), "huaweicloud_cdn_rule_engine_rule": cdn.ResourceRuleEngineRule(), "huaweicloud_cdn_statistic_configuration": cdn.ResourceStatisticConfiguration(), + "huaweicloud_cdn_statistic_subscription_task": cdn.ResourceStatisticSubscriptionTask(), "huaweicloud_ces_alarmrule": ces.ResourceAlarmRule(), "huaweicloud_ces_alarm_template": ces.ResourceCesAlarmTemplate(), diff --git a/huaweicloud/services/acceptance/cdn/resource_huaweicloud_cdn_statistic_subscription_task_test.go b/huaweicloud/services/acceptance/cdn/resource_huaweicloud_cdn_statistic_subscription_task_test.go new file mode 100644 index 00000000000..ff6df7cdf79 --- /dev/null +++ b/huaweicloud/services/acceptance/cdn/resource_huaweicloud_cdn_statistic_subscription_task_test.go @@ -0,0 +1,131 @@ +package cdn + +import ( + "errors" + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/acceptance" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/services/cdn" +) + +func getStatisticSubscriptionTaskResourceFunc(cfg *config.Config, state *terraform.ResourceState) (interface{}, error) { + client, err := cfg.NewServiceClient("cdn", "") + if err != nil { + return nil, fmt.Errorf("error creating CDN client: %s", err) + } + + return cdn.GetStatisticSubscriptionTaskById(client, state.Primary.ID) +} + +func TestAccStatisticSubscriptionTask_basic(t *testing.T) { + var ( + obj interface{} + + rName = "huaweicloud_cdn_statistic_subscription_task.test" + rc = acceptance.InitResourceCheck(rName, &obj, getStatisticSubscriptionTaskResourceFunc) + + name = acceptance.RandomAccResourceNameWithDash() + updateName = acceptance.RandomAccResourceNameWithDash() + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acceptance.TestAccPreCheck(t) + acceptance.TestAccPreCheckCdnDomainName(t) + }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: rc.CheckResourceDestroy(), + Steps: []resource.TestStep{ + { + Config: testAccStatisticSubscriptionTask_basic_step1(name), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttr(rName, "name", name), + resource.TestCheckResourceAttr(rName, "period_type", "0"), + resource.TestCheckResourceAttrSet(rName, "emails"), + resource.TestCheckResourceAttrSet(rName, "domain_name"), + resource.TestCheckResourceAttrSet(rName, "report_type"), + resource.TestMatchResourceAttr(rName, "create_time", + regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}?(Z|([+-]\d{2}:\d{2}))$`)), + ), + }, + { + Config: testAccStatisticSubscriptionTask_basic_step2(updateName), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttr(rName, "name", updateName), + resource.TestCheckResourceAttr(rName, "period_type", "1"), + resource.TestCheckResourceAttrSet(rName, "emails"), + resource.TestCheckResourceAttrSet(rName, "domain_name"), + resource.TestCheckResourceAttrSet(rName, "report_type"), + resource.TestMatchResourceAttr(rName, "create_time", + regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}?(Z|([+-]\d{2}:\d{2}))$`)), + resource.TestMatchResourceAttr(rName, "update_time", + regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}?(Z|([+-]\d{2}:\d{2}))$`)), + ), + }, + { + ResourceName: rName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "emails", + }, + }, + { + ResourceName: rName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testStatisticSubscriptionTaskImportStateWithName(rName), + ImportStateVerifyIgnore: []string{ + "emails", + }, + }, + }, + }) +} + +func testStatisticSubscriptionTaskImportStateWithName(name string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[name] + if !ok { + return "", fmt.Errorf("resource (%s) not found", name) + } + + taskName := rs.Primary.Attributes["name"] + if taskName == "" { + return "", errors.New("the subscription task name is missing, want ''") + } + return taskName, nil + } +} + +func testAccStatisticSubscriptionTask_basic_step1(name string) string { + return fmt.Sprintf(` +resource "huaweicloud_cdn_statistic_subscription_task" "test" { + name = "%[1]s" + period_type = 0 + emails = "test@example.com" + domain_name = "%[2]s" + report_type = "0,1,2" +} +`, name, acceptance.HW_CDN_DOMAIN_NAME) +} + +func testAccStatisticSubscriptionTask_basic_step2(name string) string { + return fmt.Sprintf(` +resource "huaweicloud_cdn_statistic_subscription_task" "test" { + name = "%[1]s" + period_type = 1 + emails = "test@example.com,test2@example.com" + domain_name = "%[2]s" + report_type = "0,1,2,3,4,5" +} +`, name, acceptance.HW_CDN_DOMAIN_NAME) +} diff --git a/huaweicloud/services/cdn/resource_huaweicloud_cdn_statistic_subscription_task.go b/huaweicloud/services/cdn/resource_huaweicloud_cdn_statistic_subscription_task.go new file mode 100644 index 00000000000..fc4b7e0c640 --- /dev/null +++ b/huaweicloud/services/cdn/resource_huaweicloud_cdn_statistic_subscription_task.go @@ -0,0 +1,336 @@ +package cdn + +import ( + "context" + "fmt" + "log" + "strconv" + "strings" + + "github.com/hashicorp/go-multierror" + "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/common" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/utils" +) + +// @API CDN POST /v1/cdn/statistics/subscription-tasks +// @API CDN GET /v1/cdn/statistics/subscription-tasks +// @API CDN PUT /v1/cdn/statistics/subscription-tasks/{id} +// @API CDN DELETE /v1/cdn/statistics/subscription-tasks/{id} +func ResourceStatisticSubscriptionTask() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceStatisticSubscriptionTaskCreate, + ReadContext: resourceStatisticSubscriptionTaskRead, + UpdateContext: resourceStatisticSubscriptionTaskUpdate, + DeleteContext: resourceStatisticSubscriptionTaskDelete, + + Importer: &schema.ResourceImporter{ + StateContext: resourceStatisticSubscriptionTaskImportState, + }, + + Schema: map[string]*schema.Schema{ + // Required parameters. + "name": { + Type: schema.TypeString, + Required: true, + Description: `The name of the subscription task.`, + }, + "period_type": { + Type: schema.TypeInt, + Required: true, + Description: `The type of the subscription task.`, + }, + "emails": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: `The email addresses to receive the operation reports.`, + }, + "domain_name": { + Type: schema.TypeString, + Required: true, + Description: `The list of domain names to subscribe.`, + }, + "report_type": { + Type: schema.TypeString, + Required: true, + Description: `The type of the operation report.`, + }, + + // Attributes. + "create_time": { + Type: schema.TypeString, + Computed: true, + Description: `The creation time of the subscription task, in RFC3339 format.`, + }, + "update_time": { + Type: schema.TypeString, + Computed: true, + Description: `The last update time of the subscription task, in RFC3339 format.`, + }, + }, + } +} + +func buildStatisticSubscriptionTaskBodyParams(d *schema.ResourceData) map[string]interface{} { + return map[string]interface{}{ + "name": d.Get("name"), + "period_type": d.Get("period_type"), + "emails": d.Get("emails"), + "domain_name": d.Get("domain_name"), + "report_type": d.Get("report_type"), + } +} + +func createStatisticSubscriptionTask(client *golangsdk.ServiceClient, bodyParams map[string]interface{}) (interface{}, error) { + httpUrl := "v1/cdn/statistics/subscription-tasks" + createPath := client.Endpoint + httpUrl + + createOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + MoreHeaders: map[string]string{ + "Content-Type": "application/json", + }, + JSONBody: bodyParams, + OkCodes: []int{200, 204}, + } + + requestResp, err := client.Request("POST", createPath, &createOpt) + if err != nil { + return nil, err + } + return utils.FlattenResponse(requestResp) +} + +func listStatisticSubscriptionTasks(client *golangsdk.ServiceClient) ([]interface{}, error) { + var ( + httpUrl = "v1/cdn/statistics/subscription-tasks?limit={limit}" + limit = 100 + offset = 0 + result = make([]interface{}, 0) + ) + + listPath := client.Endpoint + httpUrl + listPath = strings.ReplaceAll(listPath, "{limit}", strconv.Itoa(limit)) + + opt := golangsdk.RequestOpts{ + KeepResponseBody: true, + MoreHeaders: map[string]string{ + "Content-Type": "application/json", + }, + } + + for { + listPathWithOffset := listPath + fmt.Sprintf("&offset=%d", offset) + requestResp, err := client.Request("GET", listPathWithOffset, &opt) + if err != nil { + return nil, err + } + respBody, err := utils.FlattenResponse(requestResp) + if err != nil { + return nil, err + } + tasks := utils.PathSearch("data", respBody, make([]interface{}, 0)).([]interface{}) + result = append(result, tasks...) + if len(tasks) < limit { + break + } + offset += len(tasks) + } + + return result, nil +} + +func getStatisticSubscriptionTaskByName(client *golangsdk.ServiceClient, taskName string) (interface{}, error) { + tasks, err := listStatisticSubscriptionTasks(client) + if err != nil { + return nil, err + } + + task := utils.PathSearch(fmt.Sprintf("[?name == '%s']|[0]", taskName), tasks, nil) + if task == nil { + return nil, golangsdk.ErrDefault404{ + ErrUnexpectedResponseCode: golangsdk.ErrUnexpectedResponseCode{ + Method: "GET", + URL: "/v1/cdn/statistics/subscription-tasks", + RequestId: "NONE", + Body: []byte(fmt.Sprintf("the subscription task with name '%s' has been removed", taskName)), + }, + } + } + return task, nil +} + +func GetStatisticSubscriptionTaskById(client *golangsdk.ServiceClient, taskId string) (interface{}, error) { + tasks, err := listStatisticSubscriptionTasks(client) + if err != nil { + return nil, err + } + + task := utils.PathSearch(fmt.Sprintf("[?id == `%s`]|[0]", taskId), tasks, nil) + log.Printf("[Lance] The queried task by ID (%s): %+v", taskId, task) + if task == nil { + return nil, golangsdk.ErrDefault404{ + ErrUnexpectedResponseCode: golangsdk.ErrUnexpectedResponseCode{ + Method: "GET", + URL: "/v1/cdn/statistics/subscription-tasks", + RequestId: "NONE", + Body: []byte(fmt.Sprintf("the subscription task with ID '%s' has been removed", taskId)), + }, + } + } + return task, nil +} + +func resourceStatisticSubscriptionTaskCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + cfg := meta.(*config.Config) + client, err := cfg.NewServiceClient("cdn", "") + if err != nil { + return diag.Errorf("error creating CDN client: %s", err) + } + + bodyParams := buildStatisticSubscriptionTaskBodyParams(d) + task, err := createStatisticSubscriptionTask(client, bodyParams) + if err != nil { + return diag.Errorf("error creating CDN statistic subscription task: %s", err) + } + + taskId := utils.PathSearch("id", task, float64(0)).(float64) + d.SetId(strconv.Itoa(int(taskId))) + + return resourceStatisticSubscriptionTaskRead(ctx, d, meta) +} + +func resourceStatisticSubscriptionTaskRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + taskId = d.Id() + ) + + client, err := cfg.NewServiceClient("cdn", "") + if err != nil { + return diag.Errorf("error creating CDN client: %s", err) + } + + task, err := GetStatisticSubscriptionTaskById(client, taskId) + if err != nil { + return common.CheckDeletedDiag(d, err, fmt.Sprintf("error getting subscription task (%s)", taskId)) + } + + mErr := multierror.Append(nil, + d.Set("name", utils.PathSearch("name", task, nil)), + d.Set("period_type", utils.PathSearch("period_type", task, nil)), + d.Set("domain_name", utils.PathSearch("domain_name", task, nil)), + d.Set("report_type", utils.PathSearch("report_type", task, nil)), + d.Set("create_time", utils.FormatTimeStampRFC3339(int64(utils.PathSearch("create_time", task, float64(0)).(float64))/1000, false)), + d.Set("update_time", utils.FormatTimeStampRFC3339(int64(utils.PathSearch("update_time", task, float64(0)).(float64))/1000, false)), + ) + + return diag.FromErr(mErr.ErrorOrNil()) +} + +func updateStatisticSubscriptionTask(client *golangsdk.ServiceClient, taskId string, bodyParams map[string]interface{}) error { + httpUrl := "v1/cdn/statistics/subscription-tasks/{id}" + updatePath := client.Endpoint + httpUrl + updatePath = strings.ReplaceAll(updatePath, "{id}", taskId) + + updateOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + MoreHeaders: map[string]string{ + "Content-Type": "application/json", + }, + JSONBody: bodyParams, + OkCodes: []int{200}, + } + + _, err := client.Request("PUT", updatePath, &updateOpt) + return err +} + +func resourceStatisticSubscriptionTaskUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + taskId = d.Id() + ) + + client, err := cfg.NewServiceClient("cdn", "") + if err != nil { + return diag.Errorf("error creating CDN client: %s", err) + } + + bodyParams := buildStatisticSubscriptionTaskBodyParams(d) + if err := updateStatisticSubscriptionTask(client, taskId, bodyParams); err != nil { + return diag.Errorf("error updating subscription task (%s): %s", taskId, err) + } + + return resourceStatisticSubscriptionTaskRead(ctx, d, meta) +} + +func deleteStatisticSubscriptionTask(client *golangsdk.ServiceClient, taskId string) error { + httpUrl := "v1/cdn/statistics/subscription-tasks/{id}" + deletePath := client.Endpoint + httpUrl + deletePath = strings.ReplaceAll(deletePath, "{id}", taskId) + + deleteOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + OkCodes: []int{204}, + } + + _, err := client.Request("DELETE", deletePath, &deleteOpt) + return err +} + +func resourceStatisticSubscriptionTaskDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + taskId = d.Id() + statisticSubscriptionTaskErrCodes = []string{ + "CDN.0001", + } + ) + + client, err := cfg.NewServiceClient("cdn", "") + if err != nil { + return diag.Errorf("error creating CDN client: %s", err) + } + + err = deleteStatisticSubscriptionTask(client, taskId) + if err != nil { + return common.CheckDeletedDiag(d, common.ConvertExpected400ErrInto404Err(err, "error.error_code", statisticSubscriptionTaskErrCodes...), + fmt.Sprintf("error deleting subscription task (%s)", taskId)) + } + + return nil +} + +func resourceStatisticSubscriptionTaskImportState(_ context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + importedId := d.Id() + + // Check if it's a numeric ID (task ID) or a name + if _, err := strconv.ParseInt(importedId, 10, 64); err == nil { + // It's a numeric ID + d.SetId(importedId) + return []*schema.ResourceData{d}, nil + } + + // It's a name, need to query by name + cfg := meta.(*config.Config) + client, err := cfg.NewServiceClient("cdn", "") + if err != nil { + return nil, fmt.Errorf("error creating CDN client: %s", err) + } + + task, err := getStatisticSubscriptionTaskByName(client, importedId) + if err != nil { + return nil, err + } + taskId := utils.PathSearch("id", task, float64(0)).(float64) + d.SetId(strconv.FormatFloat(taskId, 'f', 0, 64)) + + return []*schema.ResourceData{d}, nil +}