diff --git a/docs/resources/live_disable_push_stream.md b/docs/resources/live_disable_push_stream.md new file mode 100644 index 0000000000..c2c99d2a5a --- /dev/null +++ b/docs/resources/live_disable_push_stream.md @@ -0,0 +1,64 @@ +--- +subcategory: "Live" +layout: "huaweicloud" +page_title: "HuaweiCloud: huaweicloud_live_disable_push_stream" +description: |- + Manages a disable push stream resource within HuaweiCloud. +--- + +# huaweicloud_live_disable_push_stream + +Manages a disable push stream resource within HuaweiCloud. + +-> Creating the resource indicates disable a push stream, deleting the resource indicates resume a push stream. + +## Example Usage + +```hcl +variable "domain_name" {} +variable "app_name" {} +variable "stream_name" {} + +resource "huaweicloud_live_disable_push_stream" "test" { + domain_name = var.domain_name + app_name = var.app_name + stream_name = var.stream_name +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Optional, String, ForceNew) Specifies the region in which to create the resource. + If omitted, the provider-level region will be used. + Changing this parameter will create a new resource. + +* `domain_name` - (Required, String, ForceNew) Specifies the ingest domain name of the disabling push stream. + Changing this parameter will create a new resource. + +* `app_name` - (Required, String, ForceNew) Specifies the application name of the disabling push stream. + Changing this parameter will create a new resource. + +* `stream_name` - (Required, String, ForceNew) Specifies the stream name of the disabling push stream. + The stream name is not allowed to be `*`. + Changing this parameter will create a new resource. + +* `resume_time` - (Optional, String) Specifies the time of resuming push stream. + The time is in UTC, the format is **yyyy-mm-ddThh:mm:ssZ**. e.g. **2024-06-01T15:03:01Z** + If this parameter is not specified, the default value is `7` days. The maximum value is `90` days. + The `resume_time` cannot be earlier than the current time. + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The resource ID, UUID format. + +## Import + +The resource can be imported using `domain_name`, `app_name` and `stream_name`, separated by slashes (/), e.g. + +```bash +$ terraform import huaweicloud_live_disable_push_stream.test // +``` diff --git a/huaweicloud/provider.go b/huaweicloud/provider.go index 2e7e4bf8b6..388792b140 100644 --- a/huaweicloud/provider.go +++ b/huaweicloud/provider.go @@ -1829,6 +1829,7 @@ func Provider() *schema.Provider { "huaweicloud_lb_whitelist": lb.ResourceWhitelistV2(), "huaweicloud_live_bucket_authorization": live.ResourceLiveBucketAuthorization(), + "huaweicloud_live_disable_push_stream": live.ResourceDisablePushStream(), "huaweicloud_live_domain": live.ResourceDomain(), "huaweicloud_live_record_callback": live.ResourceRecordCallback(), "huaweicloud_live_recording": live.ResourceRecording(), diff --git a/huaweicloud/services/acceptance/live/resource_huaweicloud_live_disable_push_stream_test.go b/huaweicloud/services/acceptance/live/resource_huaweicloud_live_disable_push_stream_test.go new file mode 100644 index 0000000000..ae51eac70f --- /dev/null +++ b/huaweicloud/services/acceptance/live/resource_huaweicloud_live_disable_push_stream_test.go @@ -0,0 +1,130 @@ +package live + +import ( + "fmt" + "testing" + "time" + + "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/live" +) + +func getDisablePushStreamFunc(cfg *config.Config, state *terraform.ResourceState) (interface{}, error) { + region := acceptance.HW_REGION_NAME + client, err := cfg.NewServiceClient("live", region) + if err != nil { + return nil, fmt.Errorf("error creating Live client: %s", err) + } + + getRespBody, err := live.GetDisablePushStream(client, state.Primary.Attributes["domain_name"], + state.Primary.Attributes["app_name"], state.Primary.Attributes["stream_name"]) + if err != nil { + return nil, fmt.Errorf("error retrieving disabled push stream: %s", err) + } + + return getRespBody, nil +} + +func TestAccDisablePushStream_basic(t *testing.T) { + var ( + disablePushStreamObj interface{} + rName = "huaweicloud_live_disable_push_stream.test" + domainName = fmt.Sprintf("%s.huaweicloud.com", acceptance.RandomAccResourceNameWithDash()) + createTime = time.Now().UTC().Add(24 * time.Hour).Format("2006-01-02T15:04:05Z") + updateTime = time.Now().UTC().Add(48 * time.Hour).Format("2006-01-02T15:04:05Z") + ) + + rc := acceptance.InitResourceCheck( + rName, + &disablePushStreamObj, + getDisablePushStreamFunc, + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acceptance.TestAccPreCheck(t) + }, + ProviderFactories: acceptance.TestAccProviderFactories, + CheckDestroy: rc.CheckResourceDestroy(), + Steps: []resource.TestStep{ + { + Config: testAccDisablePushStream_basic(domainName, createTime), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttrPair(rName, "domain_name", "huaweicloud_live_domain.test", "name"), + resource.TestCheckResourceAttr(rName, "app_name", "live"), + resource.TestCheckResourceAttr(rName, "stream_name", "tf-test"), + resource.TestCheckResourceAttr(rName, "resume_time", createTime), + ), + }, + { + Config: testAccDisablePushStream_update(domainName, updateTime), + Check: resource.ComposeTestCheckFunc( + rc.CheckResourceExists(), + resource.TestCheckResourceAttr(rName, "resume_time", updateTime), + ), + }, + { + ResourceName: rName, + ImportState: true, + ImportStateVerify: false, + ImportStateIdFunc: testAccDisablePushStreamImportState(rName), + }, + }, + }) +} + +func testAccDisablePushStream_basic(name, nowTime string) string { + return fmt.Sprintf(` +resource "huaweicloud_live_domain" "test" { + name = "%[1]s" + type = "push" +} + +resource "huaweicloud_live_disable_push_stream" "test" { + domain_name = huaweicloud_live_domain.test.name + app_name = "live" + stream_name = "tf-test" + resume_time = "%[2]s" +} +`, name, nowTime) +} + +func testAccDisablePushStream_update(name, updateTime string) string { + return fmt.Sprintf(` +resource "huaweicloud_live_domain" "test" { + name = "%[1]s" + type = "push" +} + +resource "huaweicloud_live_disable_push_stream" "test" { + domain_name = huaweicloud_live_domain.test.name + app_name = "live" + stream_name = "tf-test" + resume_time = "%[2]s" +} +`, name, updateTime) +} + +func testAccDisablePushStreamImportState(rName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + var domainName, appName, streamName string + rs, ok := s.RootModule().Resources[rName] + if !ok { + return "", fmt.Errorf("resource (%s) not found", rName) + } + + domainName = rs.Primary.Attributes["domain_name"] + appName = rs.Primary.Attributes["app_name"] + streamName = rs.Primary.Attributes["stream_name"] + if domainName == "" || appName == "" || streamName == "" { + return "", fmt.Errorf("invalid format specified for import ID, "+ + "want '//',but got '%s/%s/%s'", domainName, appName, streamName) + } + return fmt.Sprintf("%s/%s/%s", domainName, appName, streamName), nil + } +} diff --git a/huaweicloud/services/live/resource_huaweicloud_live_disable_push_stream.go b/huaweicloud/services/live/resource_huaweicloud_live_disable_push_stream.go new file mode 100644 index 0000000000..bca5dcb1b5 --- /dev/null +++ b/huaweicloud/services/live/resource_huaweicloud_live_disable_push_stream.go @@ -0,0 +1,277 @@ +package live + +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/common" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/config" + "github.com/huaweicloud/terraform-provider-huaweicloud/huaweicloud/utils" +) + +// @API Live POST /v1/{project_id}/stream/blocks +// @API Live GET /v1/{project_id}/stream/blocks +// @API Live PUT /v1/{project_id}/stream/blocks +// @API Live DELETE /v1/{project_id}/stream/blocks +func ResourceDisablePushStream() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceDisablePushStreamCreate, + ReadContext: resourceDisablePushStreamRead, + UpdateContext: resourceDisablePushStreamUpdate, + DeleteContext: resourceDisablePushStreamDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceDisablePushStreamImportState, + }, + + Schema: map[string]*schema.Schema{ + "region": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "domain_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `Specifies the ingest domain name of the disabling push stream.`, + }, + "app_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `Specifies the application name of the disabling push stream.`, + }, + "stream_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `Specifies the stream name of the disabling push stream.`, + }, + "resume_time": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: `Specifies the time of the resuming push stream.`, + }, + }, + } +} + +func resourceDisablePushStreamCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + ) + + client, err := cfg.NewServiceClient("live", region) + if err != nil { + return diag.Errorf("error creating Live client: %s", err) + } + + createHttpUrl := "v1/{project_id}/stream/blocks" + createPath := client.Endpoint + createHttpUrl + createPath = strings.ReplaceAll(createPath, "{project_id}", client.ProjectID) + + createOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + OkCodes: []int{ + 204, + }, + JSONBody: utils.RemoveNil(buildDisablePushStreamBodyParams(d)), + } + + _, err = client.Request("POST", createPath, &createOpt) + if err != nil { + return diag.Errorf("error creating disabled push stream: %s", err) + } + + resourceId, err := uuid.GenerateUUID() + if err != nil { + return diag.Errorf("unable to generate ID: %s", err) + } + + d.SetId(resourceId) + + return resourceDisablePushStreamRead(ctx, d, meta) +} + +func buildDisablePushStreamBodyParams(d *schema.ResourceData) map[string]interface{} { + params := map[string]interface{}{ + "domain": d.Get("domain_name"), + "app_name": d.Get("app_name"), + "stream_name": d.Get("stream_name"), + "resume_time": utils.ValueIgnoreEmpty(d.Get("resume_time")), + } + + return params +} + +func resourceDisablePushStreamRead(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + domainName = d.Get("domain_name").(string) + appName = d.Get("app_name").(string) + streamName = d.Get("stream_name").(string) + ) + + client, err := cfg.NewServiceClient("live", region) + if err != nil { + return diag.Errorf("error creating Live client: %s", err) + } + + getResp, err := GetDisablePushStream(client, domainName, appName, streamName) + if err != nil { + return common.CheckDeletedDiag(d, err, "error retrieving disabled push stream information") + } + + mErr := multierror.Append( + d.Set("region", region), + d.Set("domain_name", domainName), + d.Set("app_name", utils.PathSearch("app_name", getResp, nil)), + d.Set("stream_name", utils.PathSearch("stream_name", getResp, nil)), + d.Set("resume_time", utils.PathSearch("resume_time", getResp, nil)), + ) + + return diag.FromErr(mErr.ErrorOrNil()) +} + +func GetDisablePushStream(client *golangsdk.ServiceClient, domainName, appName, streamName string) (interface{}, error) { + getHttpUrl := "v1/{project_id}/stream/blocks" + getPath := client.Endpoint + getHttpUrl + getPath = strings.ReplaceAll(getPath, "{project_id}", client.ProjectID) + getPath = fmt.Sprintf("%s?domain=%s&app_name=%s&stream_name=%s", getPath, domainName, appName, streamName) + getOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + } + + resp, err := client.Request("GET", getPath, &getOpt) + if err != nil { + return nil, common.ConvertExpected400ErrInto404Err(err, "error_code", domainNameNotExistsCode) + } + + getRespBody, err := utils.FlattenResponse(resp) + if err != nil { + return nil, err + } + + block := utils.PathSearch("blocks|[0]", getRespBody, nil) + if block == nil { + return nil, golangsdk.ErrDefault404{} + } + + return block, nil +} + +func resourceDisablePushStreamUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + ) + + client, err := cfg.NewServiceClient("live", region) + if err != nil { + return diag.Errorf("error creating Live client: %s", err) + } + + if d.HasChange("resume_time") { + updateHttpUrl := "v1/{project_id}/stream/blocks" + updatePath := client.Endpoint + updateHttpUrl + updatePath = strings.ReplaceAll(updatePath, "{project_id}", client.ProjectID) + + updateOpt := golangsdk.RequestOpts{ + KeepResponseBody: true, + OkCodes: []int{ + 204, + }, + JSONBody: utils.RemoveNil(buildDisablePushStreamBodyParams(d)), + } + + _, err := client.Request("PUT", updatePath, &updateOpt) + if err != nil { + return diag.Errorf("error updating disabled push stream information: %s", err) + } + } + + return resourceDisablePushStreamRead(ctx, d, meta) +} + +func resourceDisablePushStreamDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var ( + cfg = meta.(*config.Config) + region = cfg.GetRegion(d) + domainName = d.Get("domain_name").(string) + appName = d.Get("app_name").(string) + streamName = d.Get("stream_name").(string) + ) + + client, err := cfg.NewServiceClient("live", region) + if err != nil { + return diag.Errorf("error creating Live client: %s", err) + } + + deleteHttpUrl := "v1/{project_id}/stream/blocks" + deletePath := client.Endpoint + deleteHttpUrl + deletePath = strings.ReplaceAll(deletePath, "{project_id}", client.ProjectID) + deletePath = fmt.Sprintf("%s?domain=%s&app_name=%s&stream_name=%s", deletePath, domainName, appName, streamName) + + deleteOpts := golangsdk.RequestOpts{ + KeepResponseBody: true, + } + + // Before deleting, call the query API, if query no result , then process `CheckDeleted` logic. + _, err = GetDisablePushStream(client, domainName, appName, streamName) + if err != nil { + return common.CheckDeletedDiag(d, err, "error retrieving disabled push stream information") + } + + // Call the deletion API + _, err = client.Request("DELETE", deletePath, &deleteOpts) + if err != nil { + return common.CheckDeletedDiag(d, common.ConvertExpected403ErrInto404Err(err, "error_code", domainNameNotExistsCode), + "error deleting disabled push stream") + } + + // Successful deletion API call does not guarantee that the resource is successfully deleted. + // Call the query API to confirm that the resource has been successfully deleted. + _, err = GetDisablePushStream(client, domainName, appName, streamName) + if err == nil { + return diag.Errorf("error deleting disabled push stream: the disabled push stream still exists") + } + + return nil +} + +func resourceDisablePushStreamImportState(_ context.Context, d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, + error) { + importedId := d.Id() + parts := strings.Split(importedId, "/") + if len(parts) != 3 { + return nil, fmt.Errorf("invalid format specified for import ID, want '//', but got '%s'", + importedId) + } + + resourceId, err := uuid.GenerateUUID() + if err != nil { + return nil, fmt.Errorf("unable to generate ID: %s", err) + } + + d.SetId(resourceId) + + mErr := multierror.Append(nil, + d.Set("domain_name", parts[0]), + d.Set("app_name", parts[1]), + d.Set("stream_name", parts[2]), + ) + + return []*schema.ResourceData{d}, mErr.ErrorOrNil() +}