diff --git a/internal/service/ec2/ec2_instance_metadata_defaults.go b/internal/service/ec2/ec2_instance_metadata_defaults.go new file mode 100644 index 00000000000..c1ceca0ac20 --- /dev/null +++ b/internal/service/ec2/ec2_instance_metadata_defaults.go @@ -0,0 +1,266 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ec2 + +import ( + // TIP: ==== IMPORTS ==== + // This is a common set of imports but not customized to your code since + // your code hasn't been written yet. Make sure you, your IDE, or + // goimports -w fixes these imports. + // + // The provider linter wants your imports to be in two groups: first, + // standard library (i.e., "fmt" or "strings"), second, everything else. + // + // Also, AWS Go SDK v2 may handle nested structures differently than v1, + // using the services/ec2/types package. If so, you'll + // need to import types and reference the nested types, e.g., as + // awstypes.. + "context" + "errors" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-provider-aws/internal/enum" + "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/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource(name="Instance Metadata Defaults") +func newResourceEC2InstanceMetadataDefaults(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceEC2InstanceMetadataDefaults{} + + r.SetDefaultCreateTimeout(10 * time.Minute) + r.SetDefaultUpdateTimeout(10 * time.Minute) + r.SetDefaultDeleteTimeout(10 * time.Minute) + + return r, nil +} + +const ( + ResNameEC2InstanceMetadataDefaults = "EC2 Instance Metadata Defaults" +) + +type resourceEC2InstanceMetadataDefaults struct { + framework.ResourceWithConfigure + framework.WithTimeouts +} + +type resourceEC2InstanceMetadataDefaultsData struct { + HttpEndpoint types.String `tfsdk:"http_endpoint"` + HttpPutResponseHopLimit types.Int64 `tfsdk:"http_put_response_hop_limit"` + HttpTokens types.String `tfsdk:"http_tokens"` + InstanceMetadataTags types.String `tfsdk:"instance_metadata_tags"` +} + +func (r *resourceEC2InstanceMetadataDefaults) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "aws_ec2_instance_metadata_defaults" +} + +func (r *resourceEC2InstanceMetadataDefaults) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "http_endpoint": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + enum.FrameworkValidate[awstypes.DefaultInstanceMetadataEndpointState](), + }, + }, + "http_put_response_hop_limit": schema.Int64Attribute{ + Optional: true, + Validators: []validator.Int64{ + int64validator.Between(-1, 64), + }, + }, + "http_tokens": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + enum.FrameworkValidate[awstypes.MetadataDefaultHttpTokensState](), + }, + }, + "instance_metadata_tags": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + enum.FrameworkValidate[awstypes.DefaultInstanceMetadataTagsState](), + }, + }, + }, + Blocks: map[string]schema.Block{}, + } +} + +func (r *resourceEC2InstanceMetadataDefaults) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + meta := r.Meta() + conn := meta.EC2Client(ctx) + + var plan resourceEC2InstanceMetadataDefaultsData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + in := &ec2.ModifyInstanceMetadataDefaultsInput{} + + if !plan.HttpEndpoint.IsNull() { + in.HttpEndpoint = awstypes.DefaultInstanceMetadataEndpointState(plan.HttpEndpoint.ValueString()) + } + if !plan.HttpPutResponseHopLimit.IsNull() { + // When the value is -1, it means "no preference" + // In which case we don't set the argument and leave it to null + if httpPutResponseHopLimit := int32(plan.HttpPutResponseHopLimit.ValueInt64()); httpPutResponseHopLimit != -1 { + in.HttpPutResponseHopLimit = aws.Int32(httpPutResponseHopLimit) + } + } + if !plan.HttpTokens.IsNull() { + in.HttpTokens = awstypes.MetadataDefaultHttpTokensState(plan.HttpTokens.ValueString()) + } + if !plan.InstanceMetadataTags.IsNull() { + in.InstanceMetadataTags = awstypes.DefaultInstanceMetadataTagsState(plan.InstanceMetadataTags.ValueString()) + } + + out, err := conn.ModifyInstanceMetadataDefaults(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.EC2, create.ErrActionCreating, ResNameEC2InstanceMetadataDefaults, meta.Region, err), + err.Error(), + ) + return + } + if out == nil || out.Return == nil || *out.Return == false { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.EC2, create.ErrActionCreating, ResNameEC2InstanceMetadataDefaults, meta.Region, nil), + errors.New("empty output").Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *resourceEC2InstanceMetadataDefaults) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + meta := r.Meta() + conn := meta.EC2Client(ctx) + + var state resourceEC2InstanceMetadataDefaultsData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + out, err := conn.GetInstanceMetadataDefaults(ctx, &ec2.GetInstanceMetadataDefaultsInput{}) + + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.EC2, create.ErrActionSetting, ResNameEC2InstanceMetadataDefaults, meta.Region, err), + err.Error(), + ) + return + } + + if out.AccountLevel.HttpEndpoint != "" { + state.HttpEndpoint = types.StringValue(string(out.AccountLevel.HttpEndpoint)) + } + + if out.AccountLevel.HttpPutResponseHopLimit == nil { + state.HttpPutResponseHopLimit = types.Int64Value(-1) + } else { + state.HttpPutResponseHopLimit = types.Int64Value(int64(*out.AccountLevel.HttpPutResponseHopLimit)) + } + + if out.AccountLevel.HttpTokens != "" { + state.HttpTokens = types.StringValue(string(out.AccountLevel.HttpTokens)) + } + + if out.AccountLevel.InstanceMetadataTags != "" { + state.InstanceMetadataTags = types.StringValue(string(out.AccountLevel.InstanceMetadataTags)) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceEC2InstanceMetadataDefaults) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state resourceEC2InstanceMetadataDefaultsData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + // TODO: deduplicate + meta := r.Meta() + conn := meta.EC2Client(ctx) + + in := &ec2.ModifyInstanceMetadataDefaultsInput{} + + if !plan.HttpEndpoint.IsNull() { + in.HttpEndpoint = awstypes.DefaultInstanceMetadataEndpointState(plan.HttpEndpoint.ValueString()) + } + + if plan.HttpPutResponseHopLimit.IsNull() { + in.HttpPutResponseHopLimit = aws.Int32(-1) + } else { + in.HttpPutResponseHopLimit = aws.Int32(int32(plan.HttpPutResponseHopLimit.ValueInt64())) + } + + if !plan.HttpTokens.IsNull() { + in.HttpTokens = awstypes.MetadataDefaultHttpTokensState(plan.HttpTokens.ValueString()) + } + + if !plan.InstanceMetadataTags.IsNull() { + in.InstanceMetadataTags = awstypes.DefaultInstanceMetadataTagsState(plan.InstanceMetadataTags.ValueString()) + } + + out, err := conn.ModifyInstanceMetadataDefaults(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.EC2, create.ErrActionCreating, ResNameEC2InstanceMetadataDefaults, meta.Region, err), + err.Error(), + ) + return + } + if out == nil || out.Return == nil || *out.Return == false { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.EC2, create.ErrActionCreating, ResNameEC2InstanceMetadataDefaults, meta.Region, nil), + errors.New("empty output").Error(), + ) + return + } + + // END todo + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *resourceEC2InstanceMetadataDefaults) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + meta := r.Meta() + conn := meta.EC2Client(ctx) + + var state resourceEC2InstanceMetadataDefaultsData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + out, err := conn.ModifyInstanceMetadataDefaults(context.Background(), &ec2.ModifyInstanceMetadataDefaultsInput{ + HttpEndpoint: awstypes.DefaultInstanceMetadataEndpointStateNoPreference, + HttpPutResponseHopLimit: aws.Int32(-1), // -1 means "no preference" + HttpTokens: awstypes.MetadataDefaultHttpTokensStateNoPreference, + InstanceMetadataTags: awstypes.DefaultInstanceMetadataTagsStateNoPreference, + }) + + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.EC2, create.ErrActionDeleting, ResNameEC2InstanceMetadataDefaults, meta.Region, err), + err.Error(), + ) + return + } + if out == nil || out.Return == nil || *out.Return == false { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.EC2, create.ErrActionCreating, ResNameEC2InstanceMetadataDefaults, meta.Region, nil), + errors.New("empty output").Error(), + ) + return + } +} diff --git a/internal/service/ec2/ec2_instance_metadata_defaults_test.go b/internal/service/ec2/ec2_instance_metadata_defaults_test.go new file mode 100644 index 00000000000..60798f8bdfd --- /dev/null +++ b/internal/service/ec2/ec2_instance_metadata_defaults_test.go @@ -0,0 +1,191 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ec2_test + +import ( + // TIP: ==== IMPORTS ==== + // This is a common set of imports but not customized to your code since + // your code hasn't been written yet. Make sure you, your IDE, or + // goimports -w fixes these imports. + // + // The provider linter wants your imports to be in two groups: first, + // standard library (i.e., "fmt" or "strings"), second, everything else. + // + // Also, AWS Go SDK v2 may handle nested structures differently than v1, + // using the services/ec2/types package. If so, you'll + // need to import types and reference the nested types, e.g., as + // types.. + "context" + "errors" + "fmt" + awstypes "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/aws/aws-sdk-go/aws" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/ec2" + "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" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/names" + + // TIP: You will often need to import the package that this test file lives + // in. Since it is in the "test" context, it must import the package to use + // any normal context constants, variables, or functions. + tfec2 "github.com/hashicorp/terraform-provider-aws/internal/service/ec2" +) + +func testAccInstanceMetadataDefaultsConfig_basic() string { + return fmt.Sprintf(` +resource "aws_ec2_instance_metadata_defaults" "test" { + http_tokens = "required" # non-default + instance_metadata_tags = "disabled" + http_endpoint = "enabled" + http_put_response_hop_limit = 1 +} +`) +} + +func testAccInstanceMetadataDefaultsConfig_partial() string { + return fmt.Sprintf(` +resource "aws_ec2_instance_metadata_defaults" "test-partial" { + http_tokens = "required" # non-default + http_put_response_hop_limit = 2 # non-default +} +`) +} + +func TestAccEC2InstanceMetadataDefaults_basic(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + resourceName := "aws_ec2_instance_metadata_defaults.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, "ec2") + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.EC2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckInstanceMetadataDefaultsDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccInstanceMetadataDefaultsConfig_basic(), + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceMetadataDefaultsExists(ctx, resourceName, &awstypes.InstanceMetadataDefaultsResponse{ + HttpTokens: awstypes.HttpTokensState(awstypes.MetadataDefaultHttpTokensStateRequired), + HttpPutResponseHopLimit: aws.Int32(1), + HttpEndpoint: awstypes.InstanceMetadataEndpointStateEnabled, + InstanceMetadataTags: awstypes.InstanceMetadataTagsStateDisabled, + }), + resource.TestCheckResourceAttr(resourceName, "http_tokens", "required"), + resource.TestCheckResourceAttr(resourceName, "instance_metadata_tags", "disabled"), + resource.TestCheckResourceAttr(resourceName, "http_endpoint", "enabled"), + resource.TestCheckResourceAttr(resourceName, "http_put_response_hop_limit", "1"), + ), + }, + }, + }) +} + +func TestAccEC2InstanceMetadataDefaults_partial(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + resourceName := "aws_ec2_instance_metadata_defaults.test-partial" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, "ec2") + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.EC2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckInstanceMetadataDefaultsDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccInstanceMetadataDefaultsConfig_partial(), + Check: resource.ComposeTestCheckFunc( + testAccCheckInstanceMetadataDefaultsExists(ctx, resourceName, &awstypes.InstanceMetadataDefaultsResponse{ + HttpTokens: awstypes.HttpTokensState(awstypes.MetadataDefaultHttpTokensStateRequired), + HttpPutResponseHopLimit: aws.Int32(2), + HttpEndpoint: "", + InstanceMetadataTags: "", + }), + resource.TestCheckResourceAttr(resourceName, "http_tokens", "required"), + resource.TestCheckResourceAttr(resourceName, "http_put_response_hop_limit", "2"), + ), + }, + }, + }) +} + +func testAccCheckInstanceMetadataDefaultsDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + result, err := acctest.Provider.Meta().(*conns.AWSClient).EC2Client(ctx).GetInstanceMetadataDefaults(ctx, &ec2.GetInstanceMetadataDefaultsInput{}) + if err != nil { + return fmt.Errorf("unable to describe instance metadata defaults: %w", err) + } + if string(result.AccountLevel.HttpEndpoint) != "" || string(result.AccountLevel.HttpTokens) != "" || result.AccountLevel.HttpPutResponseHopLimit != nil || string(result.AccountLevel.InstanceMetadataTags) != "" { + return errors.New("expected instance metadata defaults to be reset") + } + return nil + } +} + +func testAccCheckInstanceMetadataDefaultsExists(ctx context.Context, name string, expectations *awstypes.InstanceMetadataDefaultsResponse) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.EC2, create.ErrActionCheckingExistence, tfec2.ResNameEC2InstanceMetadataDefaults, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.EC2, create.ErrActionCheckingExistence, tfec2.ResNameEC2InstanceMetadataDefaults, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Client(ctx) + resp, err := conn.GetInstanceMetadataDefaults(ctx, &ec2.GetInstanceMetadataDefaultsInput{}) + + if err != nil { + return create.Error(names.EC2, create.ErrActionCheckingExistence, tfec2.ResNameEC2InstanceMetadataDefaults, rs.Primary.ID, err) + } + + // check assertions + if resp.AccountLevel.HttpTokens != expectations.HttpTokens { + return fmt.Errorf("expected HttpTokens to be '%s', got '%s'", expectations.HttpTokens, resp.AccountLevel.HttpTokens) + } + if aws.Int32Value(resp.AccountLevel.HttpPutResponseHopLimit) != aws.Int32Value(expectations.HttpPutResponseHopLimit) { + return fmt.Errorf("expected HttpPutResponseHopLimit to be '%d', got '%d'", aws.Int32Value(expectations.HttpPutResponseHopLimit), aws.Int32Value(resp.AccountLevel.HttpPutResponseHopLimit)) + } + if resp.AccountLevel.HttpEndpoint != expectations.HttpEndpoint { + return fmt.Errorf("expected HttpEndpoint to be '%s', got '%s'", expectations.HttpEndpoint, resp.AccountLevel.HttpEndpoint) + } + if resp.AccountLevel.InstanceMetadataTags != expectations.InstanceMetadataTags { + return fmt.Errorf("expected InstanceMetadataTags to be '%s', got '%s'", expectations.InstanceMetadataTags, resp.AccountLevel.InstanceMetadataTags) + } + + return nil + } +} +func testAccPreCheck(ctx context.Context, t *testing.T) { + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Client(ctx) + + _, err := conn.GetInstanceMetadataDefaults(ctx, &ec2.GetInstanceMetadataDefaultsInput{}) + + if acctest.PreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} diff --git a/internal/service/ec2/service_package_gen.go b/internal/service/ec2/service_package_gen.go index 51dcd5889b3..d05048205fa 100644 --- a/internal/service/ec2/service_package_gen.go +++ b/internal/service/ec2/service_package_gen.go @@ -34,6 +34,10 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.Servic Factory: newResourceEBSFastSnapshotRestore, Name: "EBS Fast Snapshot Restore", }, + { + Factory: newResourceEC2InstanceMetadataDefaults, + Name: "Instance Metadata Defaults", + }, { Factory: newResourceInstanceConnectEndpoint, Name: "Instance Connect Endpoint",