diff --git a/.changelog/27726.txt b/.changelog/27726.txt new file mode 100644 index 00000000000..94a16e3bb66 --- /dev/null +++ b/.changelog/27726.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_ivs_channel +``` \ No newline at end of file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index b4a7ea8f475..11627637747 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1658,6 +1658,7 @@ func New(_ context.Context) (*schema.Provider, error) { "aws_iot_topic_rule": iot.ResourceTopicRule(), "aws_iot_topic_rule_destination": iot.ResourceTopicRuleDestination(), + "aws_ivs_channel": ivs.ResourceChannel(), "aws_ivs_playback_key_pair": ivs.ResourcePlaybackKeyPair(), "aws_ivs_recording_configuration": ivs.ResourceRecordingConfiguration(), diff --git a/internal/service/ivs/channel.go b/internal/service/ivs/channel.go new file mode 100644 index 00000000000..0bef8ae3a5c --- /dev/null +++ b/internal/service/ivs/channel.go @@ -0,0 +1,272 @@ +package ivs + +import ( + "context" + "errors" + "log" + "regexp" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ivs" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func ResourceChannel() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceChannelCreate, + ReadWithoutTimeout: resourceChannelRead, + UpdateWithoutTimeout: resourceChannelUpdate, + DeleteWithoutTimeout: resourceChannelDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "authorized": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "ingest_endpoint": { + Type: schema.TypeString, + Computed: true, + }, + "latency_mode": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice(ivs.ChannelLatencyMode_Values(), false), + }, + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringMatch(regexp.MustCompile(`^[a-zA-Z0-9-_]{0,128}$`), "must contain only alphanumeric characters, hyphen, or underscore and at most 128 characters"), + }, + "playback_url": { + Type: schema.TypeString, + Computed: true, + }, + "recording_configuration_arn": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: verify.ValidARN, + }, + "tags": tftags.TagsSchema(), + "tags_all": tftags.TagsSchemaComputed(), + "type": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice(ivs.ChannelType_Values(), false), + }, + }, + + CustomizeDiff: verify.SetTagsDiff, + } +} + +const ( + ResNameChannel = "Channel" +) + +func resourceChannelCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).IVSConn + + in := &ivs.CreateChannelInput{} + + if v, ok := d.GetOk("authorized"); ok { + in.Authorized = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("latency_mode"); ok { + in.LatencyMode = aws.String(v.(string)) + } + + if v, ok := d.GetOk("name"); ok { + in.Name = aws.String(v.(string)) + } + + if v, ok := d.GetOk("recording_configuration_arn"); ok { + in.RecordingConfigurationArn = aws.String(v.(string)) + } + + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) + + if len(tags) > 0 { + in.Tags = Tags(tags.IgnoreAWS()) + } + + if v, ok := d.GetOk("type"); ok { + in.Type = aws.String(v.(string)) + } + + out, err := conn.CreateChannelWithContext(ctx, in) + if err != nil { + return create.DiagError(names.IVS, create.ErrActionCreating, ResNameChannel, d.Get("name").(string), err) + } + + if out == nil || out.Channel == nil { + return create.DiagError(names.IVS, create.ErrActionCreating, ResNameChannel, d.Get("name").(string), errors.New("empty output")) + } + + d.SetId(aws.StringValue(out.Channel.Arn)) + + if _, err := waitChannelCreated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + return create.DiagError(names.IVS, create.ErrActionWaitingForCreation, ResNameChannel, d.Id(), err) + } + + return resourceChannelRead(ctx, d, meta) +} + +func resourceChannelRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).IVSConn + + out, err := FindChannelByID(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] IVS Channel (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return create.DiagError(names.IVS, create.ErrActionReading, ResNameChannel, d.Id(), err) + } + + d.Set("arn", out.Arn) + d.Set("authorized", out.Authorized) + d.Set("ingest_endpoint", out.IngestEndpoint) + d.Set("latency_mode", out.LatencyMode) + d.Set("name", out.Name) + d.Set("playback_url", out.PlaybackUrl) + d.Set("recording_configuration_arn", out.RecordingConfigurationArn) + d.Set("type", out.Type) + + tags, err := ListTags(conn, d.Id()) + if err != nil { + return create.DiagError(names.IVS, create.ErrActionReading, ResNameChannel, d.Id(), err) + } + + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + tags = tags.IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return create.DiagError(names.IVS, create.ErrActionSetting, ResNameChannel, d.Id(), err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return create.DiagError(names.IVS, create.ErrActionSetting, ResNameChannel, d.Id(), err) + } + + return nil +} + +func resourceChannelUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).IVSConn + + update := false + + arn := d.Id() + in := &ivs.UpdateChannelInput{ + Arn: aws.String(arn), + } + + if d.HasChanges("authorized") { + in.Authorized = aws.Bool(d.Get("authorized").(bool)) + update = true + } + + if d.HasChanges("latency_mode") { + in.LatencyMode = aws.String(d.Get("latency_mode").(string)) + update = true + } + + if d.HasChanges("name") { + in.Name = aws.String(d.Get("name").(string)) + update = true + } + + if d.HasChanges("recording_configuration_arn") { + in.RecordingConfigurationArn = aws.String(d.Get("recording_configuration_arn").(string)) + update = true + } + + if d.HasChanges("type") { + in.Type = aws.String(d.Get("type").(string)) + update = true + } + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + + if err := UpdateTags(conn, arn, o, n); err != nil { + return create.DiagError(names.IVS, create.ErrActionUpdating, ResNameChannel, d.Id(), err) + } + } + + if !update { + return nil + } + + log.Printf("[DEBUG] Updating IVS Channel (%s): %#v", d.Id(), in) + + out, err := conn.UpdateChannelWithContext(ctx, in) + if err != nil { + return create.DiagError(names.IVS, create.ErrActionUpdating, ResNameChannel, d.Id(), err) + } + + if _, err := waitChannelUpdated(ctx, conn, *out.Channel.Arn, d.Timeout(schema.TimeoutUpdate), in); err != nil { + return create.DiagError(names.IVS, create.ErrActionWaitingForUpdate, ResNameChannel, d.Id(), err) + } + + return resourceChannelRead(ctx, d, meta) +} + +func resourceChannelDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).IVSConn + + log.Printf("[INFO] Deleting IVS Channel %s", d.Id()) + + _, err := conn.DeleteChannelWithContext(ctx, &ivs.DeleteChannelInput{ + Arn: aws.String(d.Id()), + }) + + if err != nil { + if tfawserr.ErrCodeEquals(err, ivs.ErrCodeResourceNotFoundException) { + return nil + } + + return create.DiagError(names.IVS, create.ErrActionDeleting, ResNameChannel, d.Id(), err) + } + + if _, err := waitChannelDeleted(ctx, conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil { + return create.DiagError(names.IVS, create.ErrActionWaitingForDeletion, ResNameChannel, d.Id(), err) + } + + return nil +} diff --git a/internal/service/ivs/channel_test.go b/internal/service/ivs/channel_test.go new file mode 100644 index 00000000000..51de12115a2 --- /dev/null +++ b/internal/service/ivs/channel_test.go @@ -0,0 +1,349 @@ +package ivs_test + +import ( + "context" + "errors" + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ivs" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" + tfivs "github.com/hashicorp/terraform-provider-aws/internal/service/ivs" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccIVSChannel_basic(t *testing.T) { + var channel ivs.Channel + + resourceName := "aws_ivs_channel.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(names.IVS, t) + testAccChannelPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, ivs.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckChannelDestroy, + Steps: []resource.TestStep{ + { + Config: testAccChannelConfig_basic(), + Check: resource.ComposeTestCheckFunc( + testAccCheckChannelExists(resourceName, &channel), + resource.TestCheckResourceAttrSet(resourceName, "ingest_endpoint"), + resource.TestCheckResourceAttrSet(resourceName, "playback_url"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "0"), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "ivs", regexp.MustCompile(`channel/.+`)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccIVSChannel_tags(t *testing.T) { + var channel ivs.Channel + + resourceName := "aws_ivs_channel.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(names.IVS, t) + testAccChannelPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, ivs.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckChannelDestroy, + Steps: []resource.TestStep{ + { + Config: testAccChannelConfig_tags1("key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckChannelExists(resourceName, &channel), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccChannelConfig_tags2("key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckChannelExists(resourceName, &channel), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccChannelConfig_tags1("key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckChannelExists(resourceName, &channel), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func TestAccIVSChannel_update(t *testing.T) { + var v1, v2 ivs.Channel + + resourceName := "aws_ivs_channel.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + authorized := "true" + latencyMode := "NORMAL" + channelType := "BASIC" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(names.IVS, t) + testAccChannelPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, ivs.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckChannelDestroy, + Steps: []resource.TestStep{ + { + Config: testAccChannelConfig_basic(), + Check: resource.ComposeTestCheckFunc( + testAccCheckChannelExists(resourceName, &v1), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccChannelConfig_update(rName, authorized, latencyMode, channelType), + Check: resource.ComposeTestCheckFunc( + testAccCheckChannelExists(resourceName, &v2), + testAccCheckChannelNotRecreated(&v1, &v2), + resource.TestCheckResourceAttr(resourceName, "authorized", authorized), + resource.TestCheckResourceAttr(resourceName, "latency_mode", latencyMode), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "type", channelType), + ), + }, + }, + }) +} + +func TestAccIVSChannel_disappears(t *testing.T) { + var channel ivs.Channel + + resourceName := "aws_ivs_channel.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(ivs.EndpointsID, t) + testAccChannelPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, ivs.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckChannelDestroy, + Steps: []resource.TestStep{ + { + Config: testAccChannelConfig_basic(), + Check: resource.ComposeTestCheckFunc( + testAccCheckChannelExists(resourceName, &channel), + acctest.CheckResourceDisappears(acctest.Provider, tfivs.ResourceChannel(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccIVSChannel_recordingConfiguration(t *testing.T) { + var channel ivs.Channel + bucketName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_ivs_channel.test" + recordingConfigurationResourceName := "aws_ivs_recording_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(ivs.EndpointsID, t) + testAccChannelPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, ivs.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckChannelDestroy, + Steps: []resource.TestStep{ + { + Config: testAccChannelConfig_recordingConfiguration(bucketName), + Check: resource.ComposeTestCheckFunc( + testAccCheckChannelExists(resourceName, &channel), + resource.TestCheckResourceAttrPair(resourceName, "recording_configuration_arn", recordingConfigurationResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckChannelDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).IVSConn + ctx := context.Background() + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_ivs_channel" { + continue + } + + input := &ivs.GetChannelInput{ + Arn: aws.String(rs.Primary.ID), + } + _, err := conn.GetChannelWithContext(ctx, input) + if err != nil { + if tfawserr.ErrCodeEquals(err, ivs.ErrCodeResourceNotFoundException) { + return nil + } + + return err + } + + return create.Error(names.IVS, create.ErrActionCheckingDestroyed, tfivs.ResNameChannel, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil +} + +func testAccCheckChannelExists(name string, channel *ivs.Channel) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + ctx := context.Background() + + if !ok { + return create.Error(names.IVS, create.ErrActionCheckingExistence, tfivs.ResNameChannel, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.IVS, create.ErrActionCheckingExistence, tfivs.ResNameChannel, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).IVSConn + + output, err := tfivs.FindChannelByID(ctx, conn, rs.Primary.ID) + + if err != nil { + return create.Error(names.IVS, create.ErrActionCheckingExistence, tfivs.ResNameChannel, rs.Primary.ID, err) + } + + *channel = *output + + return nil + } +} + +func testAccChannelPreCheck(t *testing.T) { + conn := acctest.Provider.Meta().(*conns.AWSClient).IVSConn + ctx := context.Background() + + input := &ivs.ListChannelsInput{} + _, err := conn.ListChannelsWithContext(ctx, input) + + if acctest.PreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccCheckChannelNotRecreated(before, after *ivs.Channel) resource.TestCheckFunc { + return func(s *terraform.State) error { + if before, after := aws.StringValue(before.Arn), aws.StringValue(after.Arn); before != after { + return create.Error(names.IVS, create.ErrActionCheckingNotRecreated, tfivs.ResNameChannel, before, errors.New("recreated")) + } + + return nil + } +} + +func testAccChannelConfig_basic() string { + return ` +resource "aws_ivs_channel" "test" { +} +` +} + +func testAccChannelConfig_update(rName, authorized, latencyMode, channelType string) string { + return fmt.Sprintf(` +resource "aws_ivs_channel" "test" { + name = %[1]q + authorized = %[2]s + latency_mode = %[3]q + type = %[4]q +} +`, rName, authorized, latencyMode, channelType) +} + +func testAccChannelConfig_recordingConfiguration(bucketName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q + force_destroy = true +} + +resource "aws_ivs_recording_configuration" "test" { + destination_configuration { + s3 { + bucket_name = aws_s3_bucket.test.id + } + } +} + +resource "aws_ivs_channel" "test" { + recording_configuration_arn = aws_ivs_recording_configuration.test.id +} +`, bucketName) +} + +func testAccChannelConfig_tags1(tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_ivs_channel" "test" { + tags = { + %[1]q = %[2]q + } +} +`, tagKey1, tagValue1) +} + +func testAccChannelConfig_tags2(tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_ivs_channel" "test" { + tags = { + %[1]q = %[2]q + %[3]q = %[4]q + } +} +`, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/internal/service/ivs/find.go b/internal/service/ivs/find.go index ead0d8e5bc1..ba5062becfa 100644 --- a/internal/service/ivs/find.go +++ b/internal/service/ivs/find.go @@ -55,3 +55,26 @@ func FindRecordingConfigurationByID(ctx context.Context, conn *ivs.IVS, id strin return out.RecordingConfiguration, nil } + +func FindChannelByID(ctx context.Context, conn *ivs.IVS, arn string) (*ivs.Channel, error) { + in := &ivs.GetChannelInput{ + Arn: aws.String(arn), + } + out, err := conn.GetChannelWithContext(ctx, in) + if err != nil { + if tfawserr.ErrCodeEquals(err, ivs.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + return nil, err + } + + if out == nil || out.Channel == nil { + return nil, tfresource.NewEmptyResultError(in) + } + + return out.Channel, nil +} diff --git a/internal/service/ivs/status.go b/internal/service/ivs/status.go index 1be191ff768..7f3766e7faa 100644 --- a/internal/service/ivs/status.go +++ b/internal/service/ivs/status.go @@ -10,7 +10,9 @@ import ( ) const ( - statusNormal = "Normal" + statusNormal = "Normal" + statusChangePending = "Pending" + statusUpdated = "Updated" ) func statusPlaybackKeyPair(ctx context.Context, conn *ivs.IVS, id string) resource.StateRefreshFunc { @@ -42,3 +44,29 @@ func statusRecordingConfiguration(ctx context.Context, conn *ivs.IVS, id string) return out, aws.StringValue(out.State), nil } } + +func statusChannel(ctx context.Context, conn *ivs.IVS, arn string, updateDetails *ivs.UpdateChannelInput) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + out, err := FindChannelByID(ctx, conn, arn) + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + if updateDetails == nil { + return out, statusNormal, nil + } else { + if (updateDetails.Authorized != nil && aws.BoolValue(updateDetails.Authorized) == aws.BoolValue(out.Authorized)) || + (updateDetails.LatencyMode != nil && aws.StringValue(updateDetails.LatencyMode) == aws.StringValue(out.LatencyMode)) || + (updateDetails.Name != nil && aws.StringValue(updateDetails.Name) == aws.StringValue(out.Name)) || + (updateDetails.RecordingConfigurationArn != nil && aws.StringValue(updateDetails.RecordingConfigurationArn) == aws.StringValue(out.RecordingConfigurationArn)) || + (updateDetails.Type != nil && aws.StringValue(updateDetails.Type) == aws.StringValue(out.Type)) { + return out, statusUpdated, nil + } + return out, statusChangePending, nil + } + } +} diff --git a/internal/service/ivs/wait.go b/internal/service/ivs/wait.go index 27cf858b57a..f8e853e7835 100644 --- a/internal/service/ivs/wait.go +++ b/internal/service/ivs/wait.go @@ -75,3 +75,55 @@ func waitRecordingConfigurationDeleted(ctx context.Context, conn *ivs.IVS, id st return nil, err } + +func waitChannelCreated(ctx context.Context, conn *ivs.IVS, id string, timeout time.Duration) (*ivs.Channel, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{}, + Target: []string{statusNormal}, + Refresh: statusChannel(ctx, conn, id, nil), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*ivs.Channel); ok { + return out, err + } + + return nil, err +} + +func waitChannelUpdated(ctx context.Context, conn *ivs.IVS, id string, timeout time.Duration, updateDetails *ivs.UpdateChannelInput) (*ivs.Channel, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{statusChangePending}, + Target: []string{statusUpdated}, + Refresh: statusChannel(ctx, conn, id, updateDetails), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*ivs.Channel); ok { + return out, err + } + + return nil, err +} + +func waitChannelDeleted(ctx context.Context, conn *ivs.IVS, id string, timeout time.Duration) (*ivs.Channel, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{statusNormal}, + Target: []string{}, + Refresh: statusChannel(ctx, conn, id, nil), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*ivs.Channel); ok { + return out, err + } + + return nil, err +} diff --git a/website/docs/r/ivs_channel.html.markdown b/website/docs/r/ivs_channel.html.markdown new file mode 100644 index 00000000000..08c7ec32398 --- /dev/null +++ b/website/docs/r/ivs_channel.html.markdown @@ -0,0 +1,57 @@ +--- +subcategory: "IVS (Interactive Video)" +layout: "aws" +page_title: "AWS: aws_ivs_channel" +description: |- + Terraform resource for managing an AWS IVS (Interactive Video) Channel. +--- + +# Resource: aws_ivs_channel + +Terraform resource for managing an AWS IVS (Interactive Video) Channel. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_ivs_channel" "example" { + name = "channel-1" +} +``` + +## Argument Reference + +The following arguments are optional: + +* `authorized` - (Optional) If `true`, channel is private (enabled for playback authorization). +* `latency_mode` - (Optional) Channel latency mode. Valid values: `NORMAL`, `LOW`. +* `name` - (Optional) Channel name. +* `recording_configuration_arn` - (Optional) Recording configuration ARN. +* `tags` - (Optional) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. +* `type` - (Optional) Channel type, which determines the allowable resolution and bitrate. Valid values: `STANDARD`, `BASIC`. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - ARN of the Channel. +* `ingest_endpoint` - Channel ingest endpoint, part of the definition of an ingest server, used when setting up streaming software. +* `playback_url` - Channel playback URL. +* `tags_all` - Map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). + +## Timeouts + +[Configuration options](https://www.terraform.io/docs/configuration/blocks/resources/syntax.html#operation-timeouts): + +* `create` - (Default `5m`) +* `update` - (Default `5m`) +* `delete` - (Default `5m`) + +## Import + +IVS (Interactive Video) Channel can be imported using the ARN, e.g., + +``` +$ terraform import aws_ivs_channel.example arn:aws:ivs:us-west-2:326937407773:channel/0Y1lcs4U7jk5 +```