diff --git a/.changelog/37528.txt b/.changelog/37528.txt new file mode 100644 index 00000000000..5a5c51a7fe8 --- /dev/null +++ b/.changelog/37528.txt @@ -0,0 +1,11 @@ +```release-note:new-resource +aws_ec2_capacity_block_reservation +``` + +```release-note:new-data-source +aws_ec2_capacity_block_offering +``` + +```release-note:note +resource/aws_ec2_capacity_block_reservation: Because we cannot easily test this functionality, it is best effort and we ask for community help in testing +``` \ No newline at end of file diff --git a/internal/service/ec2/ec2_capacity_block_offering_data_source.go b/internal/service/ec2/ec2_capacity_block_offering_data_source.go new file mode 100644 index 00000000000..9c36a9ccab7 --- /dev/null +++ b/internal/service/ec2/ec2_capacity_block_offering_data_source.go @@ -0,0 +1,145 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ec2 + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + awstypes "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkDataSource(name="Capacity Block Offering") +func newDataSourceCapacityBlockOffering(_ context.Context) (datasource.DataSourceWithConfigure, error) { + d := &dataSourceCapacityBlockOffering{} + + return d, nil +} + +type dataSourceCapacityBlockOffering struct { + framework.DataSourceWithConfigure +} + +func (d *dataSourceCapacityBlockOffering) Metadata(_ context.Context, _ datasource.MetadataRequest, response *datasource.MetadataResponse) { + response.TypeName = "aws_ec2_capacity_block_offering" +} + +func (d *dataSourceCapacityBlockOffering) Schema(_ context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrAvailabilityZone: schema.StringAttribute{ + Computed: true, + }, + "capacity_block_offering_id": framework.IDAttribute(), + "capacity_duration_hours": schema.Int64Attribute{ + Required: true, + }, + "currency_code": schema.StringAttribute{ + Computed: true, + }, + "end_date_range": schema.StringAttribute{ + CustomType: timetypes.RFC3339Type{}, + Optional: true, + Computed: true, + }, + names.AttrInstanceCount: schema.Int64Attribute{ + Required: true, + }, + names.AttrInstanceType: schema.StringAttribute{ + Required: true, + }, + "start_date_range": schema.StringAttribute{ + CustomType: timetypes.RFC3339Type{}, + Optional: true, + Computed: true, + }, + "tenancy": schema.StringAttribute{ + Computed: true, + }, + "upfront_fee": schema.StringAttribute{ + Computed: true, + }, + }, + } +} + +const ( + DSNameCapacityBlockOffering = "Capacity Block Offering" +) + +func (d *dataSourceCapacityBlockOffering) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { + conn := d.Meta().EC2Client(ctx) + var data dataSourceCapacityBlockOfferingData + + response.Diagnostics.Append(request.Config.Get(ctx, &data)...) + + if response.Diagnostics.HasError() { + return + } + + input := &ec2.DescribeCapacityBlockOfferingsInput{} + response.Diagnostics.Append(fwflex.Expand(ctx, data, input)...) + + if response.Diagnostics.HasError() { + return + } + + output, err := findCapacityBLockOffering(ctx, conn, input) + + if err != nil { + response.Diagnostics.AddError( + create.ProblemStandardMessage(names.EC2, create.ErrActionReading, DSNameCapacityBlockOffering, data.InstanceType.String(), err), + err.Error(), + ) + return + } + + response.Diagnostics.Append(fwflex.Flatten(ctx, output, &data)...) + + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +type dataSourceCapacityBlockOfferingData struct { + AvailabilityZone types.String `tfsdk:"availability_zone"` + CapacityDurationHours types.Int64 `tfsdk:"capacity_duration_hours"` + CurrencyCode types.String `tfsdk:"currency_code"` + EndDateRange timetypes.RFC3339 `tfsdk:"end_date_range"` + CapacityBlockOfferingID types.String `tfsdk:"capacity_block_offering_id"` + InstanceCount types.Int64 `tfsdk:"instance_count"` + InstanceType types.String `tfsdk:"instance_type"` + StartDateRange timetypes.RFC3339 `tfsdk:"start_date_range"` + Tenancy types.String `tfsdk:"tenancy"` + UpfrontFee types.String `tfsdk:"upfront_fee"` +} + +func findCapacityBLockOffering(ctx context.Context, conn *ec2.Client, in *ec2.DescribeCapacityBlockOfferingsInput) (*awstypes.CapacityBlockOffering, error) { + output, err := conn.DescribeCapacityBlockOfferings(ctx, in) + + if err != nil { + return nil, err + } + + if output == nil || len(output.CapacityBlockOfferings) == 0 { + return nil, tfresource.NewEmptyResultError(in) + } + + if len(output.CapacityBlockOfferings) > 1 { + return nil, tfresource.NewTooManyResultsError(len(output.CapacityBlockOfferings), in) + } + + return tfresource.AssertSingleValueResult(output.CapacityBlockOfferings) +} diff --git a/internal/service/ec2/ec2_capacity_block_offering_data_source_test.go b/internal/service/ec2/ec2_capacity_block_offering_data_source_test.go new file mode 100644 index 00000000000..e018d24ec07 --- /dev/null +++ b/internal/service/ec2/ec2_capacity_block_offering_data_source_test.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ec2_test + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccEC2CapacityBlockOfferingDataSource_basic(t *testing.T) { + ctx := acctest.Context(t) + dataSourceName := "data.aws_ec2_capacity_block_offering.test" + startDate := time.Now().UTC().Add(25 * time.Hour).Format(time.RFC3339) + endDate := time.Now().UTC().Add(720 * time.Hour).Format(time.RFC3339) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ErrorCheck: acctest.ErrorCheck(t, names.EC2), + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: testAccCapacityBlockOfferingDataSourceConfig_basic(startDate, endDate), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(dataSourceName, names.AttrAvailabilityZone), + resource.TestCheckResourceAttr(dataSourceName, "capacity_duration_hours", "24"), + resource.TestCheckResourceAttr(dataSourceName, names.AttrInstanceCount, acctest.Ct1), + resource.TestCheckResourceAttr(dataSourceName, names.AttrInstanceType, "p4d.24xlarge"), + resource.TestCheckResourceAttrSet(dataSourceName, "capacity_block_offering_id"), + resource.TestCheckResourceAttr(dataSourceName, "tenancy", "default"), + resource.TestCheckResourceAttrSet(dataSourceName, "upfront_fee"), + ), + }, + }, + }) +} + +func testAccCapacityBlockOfferingDataSourceConfig_basic(startDate, endDate string) string { + return fmt.Sprintf(` +data "aws_ec2_capacity_block_offering" "test" { + instance_type = "p4d.24xlarge" + capacity_duration_hours = 24 + instance_count = 1 + start_date_range = %[1]q + end_date_range = %[2]q +} +`, startDate, endDate) +} diff --git a/internal/service/ec2/ec2_capacity_block_reservation.go b/internal/service/ec2/ec2_capacity_block_reservation.go new file mode 100644 index 00000000000..eecf8cf1a99 --- /dev/null +++ b/internal/service/ec2/ec2_capacity_block_reservation.go @@ -0,0 +1,366 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ec2 + +import ( + "context" + "errors" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2" + awstypes "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/hashicorp/aws-sdk-go-base/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/enum" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource("aws_ec2_capacity_block_reservation",name="Capacity Block Reservation") +// @Tags(identifierAttribute="id") +// @Testing(tagsTest=false) +func newResourceCapacityBlockReservation(context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceCapacityBlockReservation{} + r.SetDefaultCreateTimeout(40 * time.Minute) + + return r, nil +} + +type resourceCapacityBlockReservation struct { + framework.ResourceWithConfigure + framework.WithTimeouts + framework.WithImportByID + framework.WithNoOpUpdate[resourceCapacityBlockReservationData] + framework.WithNoOpDelete +} + +func (r *resourceCapacityBlockReservation) Metadata(_ context.Context, _ resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "aws_ec2_capacity_block_reservation" +} + +func (r *resourceCapacityBlockReservation) Schema(ctx context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { + s := schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrARN: schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + names.AttrAvailabilityZone: schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "capacity_block_offering_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrCreatedDate: schema.StringAttribute{ + CustomType: timetypes.RFC3339Type{}, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "ebs_optimized": schema.BoolAttribute{ + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.UseStateForUnknown(), + }, + }, + "end_date": schema.StringAttribute{ + CustomType: timetypes.RFC3339Type{}, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "end_date_type": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.EndDateType](), + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + names.AttrID: framework.IDAttribute(), + names.AttrInstanceCount: schema.Int64Attribute{ + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, + }, + "instance_platform": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.CapacityReservationInstancePlatform](), + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrInstanceType: schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "outpost_arn": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "placement_group_arn": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "reservation_type": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.CapacityReservationType](), + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "start_date": schema.StringAttribute{ + CustomType: timetypes.RFC3339Type{}, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + "tenancy": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + Blocks: map[string]schema.Block{ + names.AttrTimeouts: timeouts.Block(ctx, timeouts.Opts{ + Create: true, + }), + }, + } + + response.Schema = s +} + +const ( + ResNameCapacityBlockReservation = "Capacity Block Reservation" +) + +func (r *resourceCapacityBlockReservation) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + conn := r.Meta().EC2Client(ctx) + var plan resourceCapacityBlockReservationData + + response.Diagnostics.Append(request.Plan.Get(ctx, &plan)...) + + if response.Diagnostics.HasError() { + return + } + + input := &ec2.PurchaseCapacityBlockInput{} + response.Diagnostics.Append(fwflex.Expand(ctx, plan, input)...) + + if response.Diagnostics.HasError() { + return + } + + input.TagSpecifications = getTagSpecificationsInV2(ctx, awstypes.ResourceTypeCapacityReservation) + + output, err := conn.PurchaseCapacityBlock(ctx, input) + if err != nil { + response.Diagnostics.AddError( + create.ProblemStandardMessage(names.EC2, create.ErrActionCreating, ResNameCapacityBlockReservation, plan.CapacityBlockOfferingID.String(), err), + err.Error(), + ) + return + } + + if output == nil || output.CapacityReservation == nil { + response.Diagnostics.AddError( + create.ProblemStandardMessage(names.EC2, create.ErrActionCreating, ResNameCapacityBlockReservation, plan.CapacityBlockOfferingID.String(), nil), + errors.New("empty output").Error(), + ) + return + } + + cp := output.CapacityReservation + state := plan + state.ID = fwflex.StringToFramework(ctx, cp.CapacityReservationId) + + createTimeout := r.CreateTimeout(ctx, plan.Timeouts) + out, err := waitCapacityBlockReservationActive(ctx, conn, createTimeout, state.ID.ValueString()) + + if err != nil { + response.Diagnostics.AddError( + create.ProblemStandardMessage(names.EC2, create.ErrActionWaitingForCreation, ResNameCapacityBlockReservation, state.ID.String(), err), + err.Error(), + ) + return + } + + response.Diagnostics.Append(fwflex.Flatten(ctx, out, &state)...) + + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &state)...) +} + +func (r *resourceCapacityBlockReservation) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + conn := r.Meta().EC2Client(ctx) + var data resourceCapacityBlockReservationData + + response.Diagnostics.Append(request.State.Get(ctx, &data)...) + + if response.Diagnostics.HasError() { + return + } + + output, err := findCapacityBlockReservationByID(ctx, conn, data.ID.ValueString()) + + if tfresource.NotFound(err) { + response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + response.State.RemoveResource(ctx) + return + } + + response.Diagnostics.Append(fwflex.Flatten(ctx, output, &data)...) + + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.State.Set(ctx, &data)...) +} + +func (r *resourceCapacityBlockReservation) ModifyPlan(ctx context.Context, request resource.ModifyPlanRequest, response *resource.ModifyPlanResponse) { + r.SetTagsAll(ctx, request, response) +} + +type resourceCapacityBlockReservationData struct { + ARN types.String `tfsdk:"arn"` + AvailabilityZone types.String `tfsdk:"availability_zone"` + CapacityBlockOfferingID types.String `tfsdk:"capacity_block_offering_id"` + EbsOptimized types.Bool `tfsdk:"ebs_optimized"` + EndDate timetypes.RFC3339 `tfsdk:"end_date"` + EndDateType fwtypes.StringEnum[awstypes.EndDateType] `tfsdk:"end_date_type"` + ID types.String `tfsdk:"id"` + InstanceCount types.Int64 `tfsdk:"instance_count"` + InstancePlatform fwtypes.StringEnum[awstypes.CapacityReservationInstancePlatform] `tfsdk:"instance_platform"` + InstanceType types.String `tfsdk:"instance_type"` + OutpostARN types.String `tfsdk:"outpost_arn"` + PlacementGroupARN types.String `tfsdk:"placement_group_arn"` + ReservationType fwtypes.StringEnum[awstypes.CapacityReservationType] `tfsdk:"reservation_type"` + StartDate timetypes.RFC3339 `tfsdk:"start_date"` + Tags types.Map `tfsdk:"tags"` + TagsAll types.Map `tfsdk:"tags_all"` + Tenancy types.String `tfsdk:"tenancy"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +func findCapacityBlockReservationByID(ctx context.Context, conn *ec2.Client, id string) (*awstypes.CapacityReservation, error) { + input := &ec2.DescribeCapacityReservationsInput{ + CapacityReservationIds: []string{id}, + } + + output, err := conn.DescribeCapacityReservations(ctx, input) + + if tfawserr.ErrCodeEquals(err, errCodeInvalidReservationNotFound, errCodeInvalidCapacityReservationIdNotFound) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || len(output.CapacityReservations) == 0 { + return nil, tfresource.NewEmptyResultError(input) + } + + reservation, err := tfresource.AssertSingleValueResult(output.CapacityReservations) + + if err != nil { + return nil, err + } + + // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/capacity-reservations-using.html#capacity-reservations-view. + if state := reservation.State; state == awstypes.CapacityReservationStateCancelled || state == awstypes.CapacityReservationStateExpired { + return nil, &retry.NotFoundError{ + Message: string(state), + LastRequest: input, + } + } + + // Eventual consistency check. + if aws.ToString(reservation.CapacityReservationId) != id { + return nil, &retry.NotFoundError{ + LastRequest: input, + } + } + + return reservation, nil +} + +func waitCapacityBlockReservationActive(ctx context.Context, conn *ec2.Client, timeout time.Duration, id string) (*awstypes.CapacityReservation, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.CapacityReservationStatePaymentPending), + Target: enum.Slice(awstypes.CapacityReservationStateActive, awstypes.CapacityReservationStateScheduled), + Refresh: statusCapacityBlockReservation(ctx, conn, id), + Timeout: timeout, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*awstypes.CapacityReservation); ok { + return output, err + } + + return nil, err +} + +func statusCapacityBlockReservation(ctx context.Context, conn *ec2.Client, id string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := findCapacityBlockReservationByID(ctx, conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, string(output.State), nil + } +} diff --git a/internal/service/ec2/ec2_capacity_block_reservation_test.go b/internal/service/ec2/ec2_capacity_block_reservation_test.go new file mode 100644 index 00000000000..49f38a20c2f --- /dev/null +++ b/internal/service/ec2/ec2_capacity_block_reservation_test.go @@ -0,0 +1,105 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ec2_test + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/YakDriver/regexache" + awstypes "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfec2 "github.com/hashicorp/terraform-provider-aws/internal/service/ec2" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccEC2CapacityBlockReservation_basic(t *testing.T) { + ctx := acctest.Context(t) + key := "RUN_EC2_CAPACITY_BLOCK_RESERVATION_TESTS" + vifId := os.Getenv(key) + if vifId != acctest.CtTrue { + t.Skipf("Environment variable %s is not set to true", key) + } + + var reservation awstypes.CapacityReservation + resourceName := "aws_ec2_capacity_block_reservation.test" + dataSourceName := "data.aws_ec2_capacity_block_offering.test" + startDate := time.Now().UTC().Add(25 * time.Hour).Format(time.RFC3339) + endDate := time.Now().UTC().Add(720 * time.Hour).Format(time.RFC3339) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: nil, + ErrorCheck: acctest.ErrorCheck(t, names.EC2ServiceID), + Steps: []resource.TestStep{ + { + Config: testAccCapacityBlockReservationConfig_basic(startDate, endDate), + Check: resource.ComposeTestCheckFunc( + testAccCheckCapacityBlockReservationExists(ctx, resourceName, &reservation), + acctest.MatchResourceAttrRegionalARN(resourceName, names.AttrARN, "ec2", regexache.MustCompile(`capacity-reservation/cr-:.+`)), + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrAvailabilityZone, resourceName, names.AttrAvailabilityZone), + resource.TestCheckResourceAttrPair(dataSourceName, "capacity_block_offering_id", resourceName, "capacity_block_offering_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "start_date", resourceName, "start_date"), + resource.TestCheckResourceAttrPair(dataSourceName, "end_date", resourceName, "end_date"), + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrInstanceCount, resourceName, names.AttrInstanceCount), + resource.TestCheckResourceAttrPair(dataSourceName, "instance_platform", resourceName, "instance_platform"), + resource.TestCheckResourceAttrPair(dataSourceName, names.AttrInstanceType, resourceName, names.AttrInstanceType), + resource.TestCheckResourceAttrPair(dataSourceName, "tenancy", resourceName, "tenancy"), + ), + }, + }, + }) +} + +func testAccCheckCapacityBlockReservationExists(ctx context.Context, n string, v *awstypes.CapacityReservation) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No EC2 Capacity Reservation ID is set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Client(ctx) + + output, err := tfec2.FindCapacityReservationByID(ctx, conn, rs.Primary.ID) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccCapacityBlockReservationConfig_basic(startDate, endDate string) string { + return fmt.Sprintf(` +data "aws_ec2_capacity_block_offering" "test" { + instance_type = "p4d.24xlarge" + capacity_duration = 24 + instance_count = 1 + start_date = %[1]q + end_date = %[2]q +} + +resource "aws_ec2_capacity_block_reservation" "test" { + capacity_block_offering_id = data.aws_ec2_capacity_block_offering.test.id + instance_platform = "Linux/UNIX" + tags = { + "Environment" = "dev" + } +} +`, startDate, endDate) +} diff --git a/internal/service/ec2/errors.go b/internal/service/ec2/errors.go index 0c23ca0d694..1e980cf21be 100644 --- a/internal/service/ec2/errors.go +++ b/internal/service/ec2/errors.go @@ -82,6 +82,7 @@ const ( errCodeInvalidPrefixListIDNotFound = "InvalidPrefixListID.NotFound" errCodeInvalidPrefixListIdNotFound = "InvalidPrefixListId.NotFound" errCodeInvalidPublicIpv4PoolIDNotFound = "InvalidPublicIpv4PoolID.NotFound" // nosemgrep:ci.caps5-in-const-name,ci.caps5-in-var-name + errCodeInvalidReservationNotFound = "InvalidReservationID.NotFound" errCodeInvalidRouteNotFound = "InvalidRoute.NotFound" errCodeInvalidRouteTableIDNotFound = "InvalidRouteTableID.NotFound" errCodeInvalidRouteTableIdNotFound = "InvalidRouteTableId.NotFound" diff --git a/internal/service/ec2/service_package_gen.go b/internal/service/ec2/service_package_gen.go index c1a4d6af935..b88047d7565 100644 --- a/internal/service/ec2/service_package_gen.go +++ b/internal/service/ec2/service_package_gen.go @@ -14,6 +14,10 @@ type servicePackage struct{} func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*types.ServicePackageFrameworkDataSource { return []*types.ServicePackageFrameworkDataSource{ + { + Factory: newDataSourceCapacityBlockOffering, + Name: "Capacity Block Offering", + }, { Factory: newSecurityGroupRuleDataSource, Name: "Security Group Rule", @@ -46,6 +50,13 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.Servic Factory: newInstanceMetadataDefaultsResource, Name: "Instance Metadata Defaults", }, + { + Factory: newResourceCapacityBlockReservation, + Name: "Capacity Block Reservation", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrID, + }, + }, { Factory: newResourceEndpointPrivateDNS, Name: "Endpoint Private DNS", diff --git a/website/docs/d/ec2_capacity_block_offering.html.markdown b/website/docs/d/ec2_capacity_block_offering.html.markdown new file mode 100644 index 00000000000..0abf08cc9b6 --- /dev/null +++ b/website/docs/d/ec2_capacity_block_offering.html.markdown @@ -0,0 +1,44 @@ +--- +subcategory: "EC2 (Elastic Compute Cloud)" +layout: "aws" +page_title: "AWS: aws_ec2_capacity_block_offering" +description: |- + Information about a single EC2 Capacity Block Offering. +--- + +# Data Source: aws_ec2_capacity_block_offering + +Information about a single EC2 Capacity Block Offering. + +## Example Usage + +```terraform +data "aws_ec2_capacity_block_offering" "example" { + capacity_duration_hours = 24 + end_date_range = "2024-05-30T15:04:05Z" + instance_count = 1 + instance_platform = "Linux/UNIX" + instance_type = "p4d.24xlarge" + start_date_range = "2024-04-28T15:04:05Z" +} +``` + +## Argument Reference + +This resource supports the following arguments: + +* `capacity_duration_hours` - (Required) The amount of time of the Capacity Block reservation in hours. +* `end_date_range` - (Optional) The date and time at which the Capacity Block Reservation expires. When a Capacity Reservation expires, the reserved capacity is released and you can no longer launch instances into it. Valid values: [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`) +* `instance_count` - (Required) The number of instances for which to reserve capacity. +* `instance_type` - (Required) The instance type for which to reserve capacity. +* `start_date_range` - (Optional) The date and time at which the Capacity Block Reservation starts. Valid values: [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`) + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `availability_zone` - The Availability Zone in which to create the Capacity Reservation. +* `currency_code` - The currency of the payment for the Capacity Block. +* `capacity_block_offering_id` - The Capacity Block Reservation ID. +* `upfront_fee` - The total price to be paid up front. +* `tenancy` - Indicates the tenancy of the Capacity Reservation. Specify either `default` or `dedicated`. diff --git a/website/docs/r/ec2_capacity_block_reservation.html.markdown b/website/docs/r/ec2_capacity_block_reservation.html.markdown new file mode 100644 index 00000000000..05b09180428 --- /dev/null +++ b/website/docs/r/ec2_capacity_block_reservation.html.markdown @@ -0,0 +1,64 @@ +--- +subcategory: "EC2 (Elastic Compute Cloud)" +layout: "aws" +page_title: "AWS: aws_ec2_capacity_block_reservation" +description: |- + Provides an EC2 Capacity Block Reservation. This allows you to purchase capacity block for your Amazon EC2 instances in a specific Availability Zone for machine learning (ML) Workloads. +--- + +# Resource: aws_ec2_capacity_block_reservation + +Provides an EC2 Capacity Block Reservation. This allows you to purchase capacity block for your Amazon EC2 instances in a specific Availability Zone for machine learning (ML) Workloads. + +~> **NOTE:** Once created, a reservation is valid for the `duration` of the provided `capacity_block_offering_id` and cannot be deleted. Performing a `destroy` will only remove the resource from state. For more information see [EC2 Capacity Block Reservation Documentation](https://aws.amazon.com/ec2/instance-types/p5/) and [PurchaseReservedDBInstancesOffering](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/capacity-blocks-pricing-billing.html). + +~> **NOTE:** Due to the expense of testing this resource, we provide it as best effort. If you find it useful, and have the ability to help test or notice issues, consider reaching out to us on [GitHub](https://github.com/hashicorp/terraform-provider-aws). + +## Example Usage + +```terraform +data "aws_ec2_capacity_block_offering" "example" { + capacity_duration = 24 + end_date = "2024-05-30T15:04:05Z" + instance_count = 1 + instance_platform = "Linux/UNIX" + instance_type = "p4d.24xlarge" + start_date = "2024-04-28T15:04:05Z" +} + +resource "aws_ec2_capacity_block_reservation" "example" { + capacity_block_offering_id = data.aws_ec2_capacity_block_offering.test.id + instance_platform = "Linux/UNIX" + tags = { + "Environment" = "dev" + } +} +``` + +## Argument Reference + +This resource supports the following arguments: + +* `capacity_block_offering_id` - (Required) The Capacity Block Reservation ID. +* `instance_platform` - (Required) The type of operating system for which to reserve capacity. Valid options are `Linux/UNIX`, `Red Hat Enterprise Linux`, `SUSE Linux`, `Windows`, `Windows with SQL Server`, `Windows with SQL Server Enterprise`, `Windows with SQL Server Standard` or `Windows with SQL Server Web`. +* `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. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `arn` - The ARN of the reservation. +* `availability_zone` - The Availability Zone in which to create the Capacity Block Reservation. +* `created_date` - The date and time at which the Capacity Block Reservation was created. +* `ebs_optimized` - Indicates whether the Capacity Reservation supports EBS-optimized instances. +* `end_date` - The date and time at which the Capacity Block Reservation expires. When a Capacity Block Reservation expires, the reserved capacity is released and you can no longer launch instances into it. Valid values: [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`) +* `end_date_type` - Indicates the way in which the Capacity Reservation ends. +* `id` - The ID of the Capacity Block Reservation. +* `instance_count` - The number of instances for which to reserve capacity. +* `instance_type` - The instance type for which to reserve capacity. +* `outpost_arn` - The ARN of the Outpost on which to create the Capacity Block Reservation. +* `placement_group_arn` - The ARN of the placement group in which to create the Capacity Block Reservation. +* `reservation_type` - The type of Capacity Reservation. +* `start_date` - The date and time at which the Capacity Block Reservation starts. Valid values: [RFC3339 time string](https://tools.ietf.org/html/rfc3339#section-5.8) (`YYYY-MM-DDTHH:MM:SSZ`) +* `tenancy` - Indicates the tenancy of the Capacity Block Reservation. Specify either `default` or `dedicated`. +* `tags_all` - A 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)