diff --git a/.changelog/27800.txt b/.changelog/27800.txt new file mode 100644 index 000000000000..7319a04aa889 --- /dev/null +++ b/.changelog/27800.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_scheduler_schedule_group +``` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index c2e0e840d4ff..6868bca7ed6c 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -165,6 +165,7 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/service/s3control" "github.com/hashicorp/terraform-provider-aws/internal/service/s3outposts" "github.com/hashicorp/terraform-provider-aws/internal/service/sagemaker" + "github.com/hashicorp/terraform-provider-aws/internal/service/scheduler" "github.com/hashicorp/terraform-provider-aws/internal/service/schemas" "github.com/hashicorp/terraform-provider-aws/internal/service/secretsmanager" "github.com/hashicorp/terraform-provider-aws/internal/service/securityhub" @@ -2034,6 +2035,8 @@ func New(_ context.Context) (*schema.Provider, error) { "aws_sagemaker_workforce": sagemaker.ResourceWorkforce(), "aws_sagemaker_workteam": sagemaker.ResourceWorkteam(), + "aws_scheduler_schedule_group": scheduler.ResourceScheduleGroup(), + "aws_schemas_discoverer": schemas.ResourceDiscoverer(), "aws_schemas_registry": schemas.ResourceRegistry(), "aws_schemas_schema": schemas.ResourceSchema(), diff --git a/internal/service/scheduler/find.go b/internal/service/scheduler/find.go new file mode 100644 index 000000000000..da4873c97f4a --- /dev/null +++ b/internal/service/scheduler/find.go @@ -0,0 +1,36 @@ +package scheduler + +import ( + "context" + "errors" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/scheduler" + "github.com/aws/aws-sdk-go-v2/service/scheduler/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func findScheduleGroupByName(ctx context.Context, conn *scheduler.Client, name string) (*scheduler.GetScheduleGroupOutput, error) { + in := &scheduler.GetScheduleGroupInput{ + Name: aws.String(name), + } + out, err := conn.GetScheduleGroup(ctx, in) + if err != nil { + var nfe *types.ResourceNotFoundException + if errors.As(err, &nfe) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + return nil, err + } + + if out == nil || out.Arn == nil { + return nil, tfresource.NewEmptyResultError(in) + } + + return out, nil +} diff --git a/internal/service/scheduler/generate.go b/internal/service/scheduler/generate.go new file mode 100644 index 000000000000..1ead4ff033a6 --- /dev/null +++ b/internal/service/scheduler/generate.go @@ -0,0 +1,4 @@ +//go:generate go run ../../generate/tags/main.go -AWSSDKVersion=2 -ListTags -UpdateTags -ServiceTagsSlice +// ONLY generate directives and package declaration! Do not add anything else to this file. + +package scheduler diff --git a/internal/service/scheduler/schedule_group.go b/internal/service/scheduler/schedule_group.go new file mode 100644 index 000000000000..d752ed42760b --- /dev/null +++ b/internal/service/scheduler/schedule_group.go @@ -0,0 +1,205 @@ +package scheduler + +import ( + "context" + "errors" + "log" + "regexp" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/scheduler" + "github.com/aws/aws-sdk-go-v2/service/scheduler/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "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 ResourceScheduleGroup() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceScheduleGroupCreate, + ReadWithoutTimeout: resourceScheduleGroupRead, + UpdateWithoutTimeout: resourceScheduleGroupUpdate, + DeleteWithoutTimeout: resourceScheduleGroupDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + + CustomizeDiff: verify.SetTagsDiff, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "creation_date": { + Type: schema.TypeString, + Computed: true, + }, + "last_modification_date": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"name_prefix"}, + ValidateDiagFunc: validation.ToDiagFunc(validation.All( + validation.StringLenBetween(1, 64), + validation.StringMatch(regexp.MustCompile(`^[0-9a-zA-Z-_.]+$`), `The name must consist of alphanumerics, hyphens, and underscores.`), + )), + }, + "name_prefix": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"name"}, + ValidateDiagFunc: validation.ToDiagFunc(validation.All( + validation.StringLenBetween(1, 64-resource.UniqueIDSuffixLength), + validation.StringMatch(regexp.MustCompile(`^[0-9a-zA-Z-_.]+$`), `The name must consist of alphanumerics, hyphens, and underscores.`), + )), + }, + "state": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tftags.TagsSchema(), + "tags_all": tftags.TagsSchemaComputed(), + }, + } +} + +const ( + ResNameScheduleGroup = "Schedule Group" +) + +func resourceScheduleGroupCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).SchedulerClient + + name := create.Name(d.Get("name").(string), d.Get("name_prefix").(string)) + + in := &scheduler.CreateScheduleGroupInput{ + Name: aws.String(name), + } + + 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()) + } + + out, err := conn.CreateScheduleGroup(ctx, in) + if err != nil { + return create.DiagError(names.Scheduler, create.ErrActionCreating, ResNameScheduleGroup, name, err) + } + + if out == nil || out.ScheduleGroupArn == nil { + return create.DiagError(names.Scheduler, create.ErrActionCreating, ResNameScheduleGroup, name, errors.New("empty output")) + } + + d.SetId(name) + + if _, err := waitScheduleGroupActive(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + return create.DiagError(names.Scheduler, create.ErrActionWaitingForCreation, ResNameScheduleGroup, d.Id(), err) + } + + return resourceScheduleGroupRead(ctx, d, meta) +} + +func resourceScheduleGroupRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).SchedulerClient + + out, err := findScheduleGroupByName(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] EventBridge Scheduler Schedule Group (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return create.DiagError(names.Scheduler, create.ErrActionReading, ResNameScheduleGroup, d.Id(), err) + } + + d.Set("arn", out.Arn) + d.Set("creation_date", aws.ToTime(out.CreationDate).Format(time.RFC3339)) + d.Set("last_modification_date", aws.ToTime(out.LastModificationDate).Format(time.RFC3339)) + d.Set("name", out.Name) + d.Set("name_prefix", create.NamePrefixFromName(aws.ToString(out.Name))) + d.Set("state", out.State) + + tags, err := ListTags(ctx, conn, aws.ToString(out.Arn)) + if err != nil { + return create.DiagError(names.Scheduler, create.ErrActionReading, ResNameScheduleGroup, 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.Scheduler, create.ErrActionSetting, ResNameScheduleGroup, d.Id(), err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return create.DiagError(names.Scheduler, create.ErrActionSetting, ResNameScheduleGroup, d.Id(), err) + } + + return nil +} + +func resourceScheduleGroupUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).SchedulerClient + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + + if err := UpdateTags(ctx, conn, d.Get("arn").(string), o, n); err != nil { + return diag.Errorf("error updating EventBridge Scheduler Schedule Group (%s) tags: %s", d.Id(), err) + } + } + + return resourceScheduleGroupRead(ctx, d, meta) +} + +func resourceScheduleGroupDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).SchedulerClient + + log.Printf("[INFO] Deleting EventBridge Scheduler ScheduleGroup %s", d.Id()) + + _, err := conn.DeleteScheduleGroup(ctx, &scheduler.DeleteScheduleGroupInput{ + Name: aws.String(d.Id()), + }) + + if err != nil { + var nfe *types.ResourceNotFoundException + if errors.As(err, &nfe) { + return nil + } + + return create.DiagError(names.Scheduler, create.ErrActionDeleting, ResNameScheduleGroup, d.Id(), err) + } + + if _, err := waitScheduleGroupDeleted(ctx, conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil { + return create.DiagError(names.Scheduler, create.ErrActionWaitingForDeletion, ResNameScheduleGroup, d.Id(), err) + } + + return nil +} diff --git a/internal/service/scheduler/schedule_group_test.go b/internal/service/scheduler/schedule_group_test.go new file mode 100644 index 000000000000..17917136d85c --- /dev/null +++ b/internal/service/scheduler/schedule_group_test.go @@ -0,0 +1,332 @@ +package scheduler_test + +import ( + "context" + "errors" + "fmt" + "regexp" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/scheduler" + "github.com/aws/aws-sdk-go-v2/service/scheduler/types" + 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" + tfscheduler "github.com/hashicorp/terraform-provider-aws/internal/service/scheduler" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccSchedulerScheduleGroup_basic(t *testing.T) { + var scheduleGroup scheduler.GetScheduleGroupOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_scheduler_schedule_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(names.SchedulerEndpointID, t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.SchedulerEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckScheduleGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccScheduleGroupConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckScheduleGroupExists(resourceName, &scheduleGroup), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "scheduler", regexp.MustCompile(regexp.QuoteMeta(`schedule-group/`+rName))), + resource.TestCheckResourceAttrWith(resourceName, "creation_date", func(actual string) error { + expect := scheduleGroup.CreationDate.Format(time.RFC3339) + if actual != expect { + return fmt.Errorf("expected value to be a formatted date") + } + return nil + }), + resource.TestCheckResourceAttrWith(resourceName, "last_modification_date", func(actual string) error { + expect := scheduleGroup.LastModificationDate.Format(time.RFC3339) + if actual != expect { + return fmt.Errorf("expected value to be a formatted date") + } + return nil + }), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "state", "ACTIVE"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccSchedulerScheduleGroup_disappears(t *testing.T) { + var scheduleGroup scheduler.GetScheduleGroupOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_scheduler_schedule_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(names.SchedulerEndpointID, t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.SchedulerEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckScheduleGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccScheduleGroupConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckScheduleGroupExists(resourceName, &scheduleGroup), + acctest.CheckResourceDisappears(acctest.Provider, tfscheduler.ResourceScheduleGroup(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccSchedulerScheduleGroup_nameGenerated(t *testing.T) { + var scheduleGroup scheduler.GetScheduleGroupOutput + resourceName := "aws_scheduler_schedule_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(names.SchedulerEndpointID, t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.SchedulerEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckScheduleGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccScheduleGroupConfig_nameGenerated, + Check: resource.ComposeTestCheckFunc( + testAccCheckScheduleGroupExists(resourceName, &scheduleGroup), + acctest.CheckResourceAttrNameGenerated(resourceName, "name"), + resource.TestCheckResourceAttr(resourceName, "name_prefix", resource.UniqueIdPrefix), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccSchedulerScheduleGroup_namePrefix(t *testing.T) { + var scheduleGroup scheduler.GetScheduleGroupOutput + resourceName := "aws_scheduler_schedule_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(names.SchedulerEndpointID, t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.SchedulerEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckScheduleGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccScheduleGroupConfig_namePrefix("tf-acc-test-prefix-"), + Check: resource.ComposeTestCheckFunc( + testAccCheckScheduleGroupExists(resourceName, &scheduleGroup), + acctest.CheckResourceAttrNameFromPrefix(resourceName, "name", "tf-acc-test-prefix-"), + resource.TestCheckResourceAttr(resourceName, "name_prefix", "tf-acc-test-prefix-"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccSchedulerScheduleGroup_tags(t *testing.T) { + var scheduleGroup scheduler.GetScheduleGroupOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_scheduler_schedule_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(names.SchedulerEndpointID, t) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.SchedulerEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckScheduleGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccScheduleGroupConfig_tags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckScheduleGroupExists(resourceName, &scheduleGroup), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccScheduleGroupConfig_tags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckScheduleGroupExists(resourceName, &scheduleGroup), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccScheduleGroupConfig_tags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckScheduleGroupExists(resourceName, &scheduleGroup), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckScheduleGroupDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).SchedulerClient + ctx := context.Background() + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_scheduler_schedule_group" { + continue + } + + _, err := conn.GetScheduleGroup(ctx, &scheduler.GetScheduleGroupInput{ + Name: aws.String(rs.Primary.ID), + }) + if err != nil { + var nfe *types.ResourceNotFoundException + if errors.As(err, &nfe) { + return nil + } + return err + } + + return create.Error(names.Scheduler, create.ErrActionCheckingDestroyed, tfscheduler.ResNameScheduleGroup, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil +} + +func testAccCheckScheduleGroupExists(name string, schedulegroup *scheduler.GetScheduleGroupOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.Scheduler, create.ErrActionCheckingExistence, tfscheduler.ResNameScheduleGroup, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.Scheduler, create.ErrActionCheckingExistence, tfscheduler.ResNameScheduleGroup, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).SchedulerClient + ctx := context.Background() + resp, err := conn.GetScheduleGroup(ctx, &scheduler.GetScheduleGroupInput{ + Name: aws.String(rs.Primary.ID), + }) + + if err != nil { + return create.Error(names.Scheduler, create.ErrActionCheckingExistence, tfscheduler.ResNameScheduleGroup, rs.Primary.ID, err) + } + + *schedulegroup = *resp + + return nil + } +} + +func testAccPreCheck(t *testing.T) { + conn := acctest.Provider.Meta().(*conns.AWSClient).SchedulerClient + ctx := context.Background() + + input := &scheduler.ListScheduleGroupsInput{} + _, err := conn.ListScheduleGroups(ctx, input) + + if acctest.PreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccScheduleGroupConfig_basic(rName string) string { + return fmt.Sprintf(` +resource "aws_scheduler_schedule_group" "test" { + name = %[1]q +} +`, rName) +} + +const testAccScheduleGroupConfig_nameGenerated = ` +resource "aws_scheduler_schedule_group" "test" {} +` + +func testAccScheduleGroupConfig_namePrefix(namePrefix string) string { + return fmt.Sprintf(` +resource "aws_scheduler_schedule_group" "test" { + name_prefix = %[1]q +} +`, namePrefix) +} + +func testAccScheduleGroupConfig_tags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_scheduler_schedule_group" "test" { + name = %[1]q + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccScheduleGroupConfig_tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_scheduler_schedule_group" "test" { + name = %[1]q + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/internal/service/scheduler/status.go b/internal/service/scheduler/status.go new file mode 100644 index 000000000000..4d8a6dccd4bb --- /dev/null +++ b/internal/service/scheduler/status.go @@ -0,0 +1,29 @@ +package scheduler + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/scheduler" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +const ( + scheduleGroupStatusActive = "ACTIVE" + scheduleGroupStatusDeleting = "DELETING" +) + +func statusScheduleGroup(ctx context.Context, conn *scheduler.Client, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + out, err := findScheduleGroupByName(ctx, conn, name) + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return out, string(out.State), nil + } +} diff --git a/internal/service/scheduler/sweep.go b/internal/service/scheduler/sweep.go new file mode 100644 index 000000000000..2abe515bb60d --- /dev/null +++ b/internal/service/scheduler/sweep.go @@ -0,0 +1,73 @@ +//go:build sweep +// +build sweep + +package scheduler + +import ( + "context" + "fmt" + "log" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/scheduler" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/sweep" +) + +func init() { + resource.AddTestSweepers("aws_scheduler_schedule_group", &resource.Sweeper{ + Name: "aws_scheduler_schedule_group", + F: sweepScheduleGroups, + }) +} + +func sweepScheduleGroups(region string) error { + client, err := sweep.SharedRegionalSweepClient(region) + + if err != nil { + return fmt.Errorf("getting client: %w", err) + } + + conn := client.(*conns.AWSClient).SchedulerClient + sweepResources := make([]sweep.Sweepable, 0) + var errs *multierror.Error + + paginator := scheduler.NewListScheduleGroupsPaginator(conn, &scheduler.ListScheduleGroupsInput{}) + + for paginator.HasMorePages() { + page, err := paginator.NextPage(context.Background()) + + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("listing Schedule Groups for %s: %w", region, err)) + break + } + + for _, it := range page.ScheduleGroups { + name := aws.ToString(it.Name) + + if name == "default" { + // Can't delete the default schedule group. + continue + } + + r := ResourceScheduleGroup() + d := r.Data(nil) + d.SetId(name) + + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) + } + } + + if err := sweep.SweepOrchestrator(sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("sweeping Schedule Group for %s: %w", region, err)) + } + + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping Schedule Group sweep for %s: %s", region, errs) + return nil + } + + return errs.ErrorOrNil() +} diff --git a/internal/service/scheduler/tags_gen.go b/internal/service/scheduler/tags_gen.go new file mode 100644 index 000000000000..a1de134f9c7f --- /dev/null +++ b/internal/service/scheduler/tags_gen.go @@ -0,0 +1,94 @@ +// Code generated by internal/generate/tags/main.go; DO NOT EDIT. +package scheduler + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/scheduler" + "github.com/aws/aws-sdk-go-v2/service/scheduler/types" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" +) + +// ListTags lists scheduler service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func ListTags(ctx context.Context, conn *scheduler.Client, identifier string) (tftags.KeyValueTags, error) { + input := &scheduler.ListTagsForResourceInput{ + ResourceArn: aws.String(identifier), + } + + output, err := conn.ListTagsForResource(ctx, input) + + if err != nil { + return tftags.New(nil), err + } + + return KeyValueTags(output.Tags), nil +} + +// []*SERVICE.Tag handling + +// Tags returns scheduler service tags. +func Tags(tags tftags.KeyValueTags) []types.Tag { + result := make([]types.Tag, 0, len(tags)) + + for k, v := range tags.Map() { + tag := types.Tag{ + Key: aws.String(k), + Value: aws.String(v), + } + + result = append(result, tag) + } + + return result +} + +// KeyValueTags creates tftags.KeyValueTags from scheduler service tags. +func KeyValueTags(tags []types.Tag) tftags.KeyValueTags { + m := make(map[string]*string, len(tags)) + + for _, tag := range tags { + m[aws.ToString(tag.Key)] = tag.Value + } + + return tftags.New(m) +} + +// UpdateTags updates scheduler service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func UpdateTags(ctx context.Context, conn *scheduler.Client, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { + oldTags := tftags.New(oldTagsMap) + newTags := tftags.New(newTagsMap) + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input := &scheduler.UntagResourceInput{ + ResourceArn: aws.String(identifier), + TagKeys: removedTags.IgnoreAWS().Keys(), + } + + _, err := conn.UntagResource(ctx, input) + + if err != nil { + return fmt.Errorf("untagging resource (%s): %w", identifier, err) + } + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input := &scheduler.TagResourceInput{ + ResourceArn: aws.String(identifier), + Tags: Tags(updatedTags.IgnoreAWS()), + } + + _, err := conn.TagResource(ctx, input) + + if err != nil { + return fmt.Errorf("tagging resource (%s): %w", identifier, err) + } + } + + return nil +} diff --git a/internal/service/scheduler/wait.go b/internal/service/scheduler/wait.go new file mode 100644 index 000000000000..66f92467f16c --- /dev/null +++ b/internal/service/scheduler/wait.go @@ -0,0 +1,43 @@ +package scheduler + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/service/scheduler" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func waitScheduleGroupActive(ctx context.Context, conn *scheduler.Client, name string, timeout time.Duration) (*scheduler.GetScheduleGroupOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{}, + Target: []string{scheduleGroupStatusActive}, + Refresh: statusScheduleGroup(ctx, conn, name), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*scheduler.GetScheduleGroupOutput); ok { + return out, err + } + + return nil, err +} + +func waitScheduleGroupDeleted(ctx context.Context, conn *scheduler.Client, name string, timeout time.Duration) (*scheduler.GetScheduleGroupOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{scheduleGroupStatusDeleting, scheduleGroupStatusActive}, + Target: []string{}, + Refresh: statusScheduleGroup(ctx, conn, name), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*scheduler.GetScheduleGroupOutput); ok { + return out, err + } + + return nil, err +} diff --git a/internal/sweep/sweep_test.go b/internal/sweep/sweep_test.go index 2df023824f47..23eb5a827eff 100644 --- a/internal/sweep/sweep_test.go +++ b/internal/sweep/sweep_test.go @@ -117,6 +117,7 @@ import ( _ "github.com/hashicorp/terraform-provider-aws/internal/service/s3" _ "github.com/hashicorp/terraform-provider-aws/internal/service/s3control" _ "github.com/hashicorp/terraform-provider-aws/internal/service/sagemaker" + _ "github.com/hashicorp/terraform-provider-aws/internal/service/scheduler" _ "github.com/hashicorp/terraform-provider-aws/internal/service/schemas" _ "github.com/hashicorp/terraform-provider-aws/internal/service/secretsmanager" _ "github.com/hashicorp/terraform-provider-aws/internal/service/servicecatalog" diff --git a/website/docs/r/scheduler_schedule_group.html.markdown b/website/docs/r/scheduler_schedule_group.html.markdown new file mode 100644 index 000000000000..9ce99b6cdd11 --- /dev/null +++ b/website/docs/r/scheduler_schedule_group.html.markdown @@ -0,0 +1,57 @@ +--- +subcategory: "EventBridge Scheduler" +layout: "aws" +page_title: "AWS: aws_scheduler_schedule_group" +description: |- + Provides an EventBridge Scheduler Schedule Group resource. +--- + +# Resource: aws_scheduler_schedule_group + +Provides an EventBridge Scheduler Schedule Group resource. + +You can find out more about EventBridge Scheduler in the [User Guide](https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html). + +~> **Note:** EventBridge was formerly known as CloudWatch Events. The functionality is identical. + +## Example Usage + +```terraform +resource "aws_scheduler_schedule_group" "example" { + name = "my-schedule-group" +} +``` + +## Argument Reference + +The following arguments are optional: + +* `name` - (Optional, Forces new resource) Name of the schedule group. If omitted, Terraform will assign a random, unique name. Conflicts with `name_prefix`. +* `name_prefix` - (Optional, Forces new resource) Creates a unique name beginning with the specified prefix. Conflicts with `name`. +* `tags` - (Optional) Key-value mapping of resource tags. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - Name of the schedule group. +* `arn` - ARN of the schedule group. +* `creation_date` - Time at which the schedule group was created. +* `last_modification_date` - Time at which the schedule group was last modified. +* `state` - State of the schedule group. Can be `ACTIVE` or `DELETING`. +* `tags_all` - Map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block). + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +- `create` - (Default `5m`) +- `delete` - (Default `5m`) + +## Import + +Schedule groups can be imported using the `name`. For example: + +``` +$ terraform import aws_scheduler_schedule_group.example my-schedule-group +```